From 2bcf9d432c482ac789ecae8c60558aaaa5b42b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 27 Jun 2024 15:04:53 +0200 Subject: [PATCH 1/5] [core] Graph: Remove `selectedViewpoint` property The information that this property conveys will be accessible at an upper level and is thus redundant here. --- meshroom/core/graph.py | 8 -------- meshroom/ui/reconstruction.py | 1 - 2 files changed, 9 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 23df54f2..e1f05787 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -226,8 +226,6 @@ class Graph(BaseObject): self._fileDateVersion = 0 self.header = {} - self._selectedViewpoint = None - def clear(self): self.header.clear() self._compatibilityNodes.clear() @@ -1566,12 +1564,6 @@ class Graph(BaseObject): self.updateStatusFromCache(force=True) self.cacheDirChanged.emit() - @property - def selectedViewpoint(self): - """ Return the attribute describing the viewpoint that is - currently set as the 'selected viewpoint'. """ - return self._selectedViewpoint - @property def fileDateVersion(self): return self._fileDateVersion diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index c331a97f..2e3f70c5 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -1158,7 +1158,6 @@ class Reconstruction(UIGraph): # Reconstruction has ownership of Viewpoint object - destroy it when not needed anymore self._selectedViewpoint.deleteLater() self._selectedViewpoint = ViewpointWrapper(viewpointAttribute, self) if viewpointAttribute else None - self._graph._selectedViewpoint = self._selectedViewpoint.attribute if viewpointAttribute else None self.selectedViewpointChanged.emit() def setPickedViewId(self, viewId): From 5bcbc84f2a9fe2d2e920aff589df064796fcbe97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 27 Jun 2024 15:46:48 +0200 Subject: [PATCH 2/5] [ui] Add an `activeProject` property and expose the UI to the core --- meshroom/ui/__main__.py | 8 +++++--- meshroom/ui/app.py | 24 +++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/meshroom/ui/__main__.py b/meshroom/ui/__main__.py index 7cede123..f3da61c9 100644 --- a/meshroom/ui/__main__.py +++ b/meshroom/ui/__main__.py @@ -6,6 +6,8 @@ from meshroom.common import Backend meshroom.setupEnvironment(backend=Backend.PYSIDE) signal.signal(signal.SIGINT, signal.SIG_DFL) -from meshroom.ui.app import MeshroomApp -app = MeshroomApp(sys.argv) -app.exec_() +import meshroom.ui +import meshroom.ui.app + +meshroom.ui.uiInstance = meshroom.ui.app.MeshroomApp(sys.argv) +meshroom.ui.uiInstance.exec_() diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 531f0293..41d0d3a6 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -4,13 +4,14 @@ import re import argparse from PySide2 import QtCore -from PySide2.QtCore import Qt, QUrl, Slot, QJsonValue, Property, Signal, qInstallMessageHandler, QtMsgType, QSettings +from PySide2.QtCore import Qt, QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings from PySide2.QtGui import QIcon from PySide2.QtWidgets import QApplication import meshroom from meshroom.core import nodesDesc from meshroom.core.taskManager import TaskManager +from meshroom.common import Property, Variant, Signal, Slot from meshroom.ui import components from meshroom.ui.components.clipboard import ClipboardHelper @@ -143,9 +144,9 @@ class MeshroomApp(QApplication): # instantiate Reconstruction object self._undoStack = commands.UndoStack(self) self._taskManager = TaskManager(self) - r = Reconstruction(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self) - r.setSubmitLabel(args.submitLabel) - self.engine.rootContext().setContextProperty("_reconstruction", r) + self._activeProject = Reconstruction(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self) + self._activeProject.setSubmitLabel(args.submitLabel) + self.engine.rootContext().setContextProperty("_reconstruction", self._activeProject) # those helpers should be available from QML Utils module as singletons, but: # - qmlRegisterUncreatableType is not yet available in PySide2 @@ -162,7 +163,7 @@ class MeshroomApp(QApplication): self.engine.rootContext().setContextProperty("MeshroomApp", self) # request any potential computation to stop on exit - self.aboutToQuit.connect(r.stopChildThreads) + self.aboutToQuit.connect(self._activeProject.stopChildThreads) if args.project and not os.path.isfile(args.project): raise RuntimeError( @@ -171,17 +172,17 @@ class MeshroomApp(QApplication): if args.project: args.project = os.path.abspath(args.project) - r.load(args.project) + self._activeProject.load(args.project) self.addRecentProjectFile(args.project) else: - r.new() + self._activeProject.new() # import is a python keyword, so we have to access the attribute by a string if getattr(args, "import", None): - r.importImagesFromFolder(getattr(args, "import"), recursive=False) + self._activeProject.importImagesFromFolder(getattr(args, "import"), recursive=False) if args.importRecursive: - r.importImagesFromFolder(args.importRecursive, recursive=True) + self._activeProject.importImagesFromFolder(args.importRecursive, recursive=True) if args.save: if os.path.isfile(args.save): @@ -195,7 +196,7 @@ class MeshroomApp(QApplication): "Meshroom Command Line Error: Cannot save the new Meshroom project file (.mg) as the parent of the folder does not exists.\n" "Invalid value: '{}'".format(args.save)) os.mkdir(projectFolder) - r.saveAs(args.save) + self._activeProject.saveAs(args.save) self.addRecentProjectFile(args.save) self.engine.load(os.path.normpath(url)) @@ -459,7 +460,8 @@ class MeshroomApp(QApplication): def _default8bitViewerEnabled(self): return bool(os.environ.get("MESHROOM_USE_8BIT_VIEWER", False)) - + activeProjectChanged = Signal() + activeProject = Property(Variant, lambda self: self._activeProject, notify=activeProjectChanged) changelogModel = Property("QVariantList", _changelogModel, constant=True) licensesModel = Property("QVariantList", _licensesModel, constant=True) pipelineTemplateFilesChanged = Signal() From cfb44d8b549faa5ea3068cd0d1654ab4120cfcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 27 Jun 2024 16:16:11 +0200 Subject: [PATCH 3/5] [ui] Add `currentViewPath` property Add a property that contains the path of the image that is currently displayed in the viewer. If no image is displayed, the property is set with an empty string. The path information is set directly from the Viewer2D when an image is loaded to be displayed. --- meshroom/ui/qml/Viewer/Viewer2D.qml | 3 +++ meshroom/ui/reconstruction.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 377dd675..0060af3c 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -224,12 +224,14 @@ FocusScope { // Entry point for getting the image file from the gallery let vp = getViewpoint(_reconstruction.pickedViewId) let path = vp ? vp.childAttribute("path").value : "" + _reconstruction.currentViewPath = path return Filepath.stringToUrl(path) } if (_reconstruction && displayedNode && displayedNode.hasSequenceOutput && displayedAttr && (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) { // Entry point for getting the image file from a sequence defined by an output attribute var path = sequence[currentFrame-frameRange.min] + _reconstruction.currentViewPath = path return Filepath.stringToUrl(path) } @@ -238,6 +240,7 @@ FocusScope { let vp = getViewpoint(_reconstruction.pickedViewId) let path = displayedAttr ? displayedAttr.value : "" let resolved = vp ? Filepath.resolve(path, vp) : path + _reconstruction.currentViewPath = resolved return Filepath.stringToUrl(resolved) } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 2e3f70c5..b78841f9 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -464,6 +464,8 @@ class Reconstruction(UIGraph): self._pickedViewId = None self._liveSfmManager = LiveSfmManager(self) + self._currentViewPath = "" + self._workerThreads = ThreadPool(processes=1) # react to internal graph changes to update those variables @@ -1216,6 +1218,12 @@ class Reconstruction(UIGraph): return R, T + def setCurrentViewPath(self, path): + if self._currentViewPath == path: + return + self._currentViewPath = path + self.currentViewPathChanged.emit() + selectedViewIdChanged = Signal() selectedViewId = Property(str, lambda self: self._selectedViewId, setSelectedViewId, notify=selectedViewIdChanged) selectedViewpointChanged = Signal() @@ -1233,6 +1241,12 @@ class Reconstruction(UIGraph): nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) + # Provides the path of the image that is currently displayed + # This is an alternative to "selectedViewpoint.attribute.path.value" for images that are displayed + # but not part of the list of viewpoints of a CameraInit node (i.e. "sequence" node outputs) + currentViewPathChanged = Signal() + currentViewPath = Property(str, lambda self: self._currentViewPath, setCurrentViewPath, notify=currentViewPathChanged) + # Signals to propagate high-level messages error = Signal(Message) warning = Signal(Message) From 9af65092b9e881c828430f54a73fb4522bc1e370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 27 Jun 2024 16:48:49 +0200 Subject: [PATCH 4/5] [Viewer] Clean-up: Harmonize syntax for the Viewer2D --- meshroom/ui/qml/Viewer/Viewer2D.qml | 59 +++++++++++++++++------------ 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 0060af3c..223a5ff1 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -137,8 +137,8 @@ FocusScope { if (Math.min(imgContainer.width, imgContainer.image.height) * imgContainer.scale * zoomFactor < 10) return var point = mapToItem(imgContainer, wheel.x, wheel.y) - imgContainer.x += (1-zoomFactor) * point.x * imgContainer.scale - imgContainer.y += (1-zoomFactor) * point.y * imgContainer.scale + imgContainer.x += (1 - zoomFactor) * point.x * imgContainer.scale + imgContainer.y += (1 - zoomFactor) * point.y * imgContainer.scale imgContainer.scale *= zoomFactor } } @@ -228,9 +228,10 @@ FocusScope { return Filepath.stringToUrl(path) } - if (_reconstruction && displayedNode && displayedNode.hasSequenceOutput && displayedAttr && (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) { + if (_reconstruction && displayedNode && displayedNode.hasSequenceOutput && displayedAttr && + (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) { // Entry point for getting the image file from a sequence defined by an output attribute - var path = sequence[currentFrame-frameRange.min] + var path = sequence[currentFrame - frameRange.min] _reconstruction.currentViewPath = path return Filepath.stringToUrl(path) } @@ -247,14 +248,15 @@ FocusScope { return undefined } - function buildOrderedSequence(path_template) { + function buildOrderedSequence(pathTemplate) { // Resolve the path template on the sequence of viewpoints // ordered by path let objs = [] - if (displayedNode && displayedNode.hasSequenceOutput && displayedAttr && (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) { - let sequence = Filepath.resolveSequence(path_template) + if (displayedNode && displayedNode.hasSequenceOutput && displayedAttr && + (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) { + let sequence = Filepath.resolveSequence(pathTemplate) let ids = sequence[0] let resolved = sequence[1] @@ -263,7 +265,7 @@ FocusScope { // concat in one array all sequences in resolved resolved = [].concat.apply([], resolved) frameRange.min = 0 - frameRange.max = resolved.length-1 + frameRange.max = resolved.length - 1 currentFrame = 0 } @@ -273,7 +275,7 @@ FocusScope { resolved = resolved[0] ids = ids[0] frameRange.min = ids[0] - frameRange.max = ids[ids.length-1] + frameRange.max = ids[ids.length - 1] currentFrame = frameRange.min } @@ -285,11 +287,11 @@ FocusScope { objs.sort((a, b) => { return a.childAttribute("path").value < b.childAttribute("path").value ? -1 : 1; }) let seq = []; for (let i = 0; i < objs.length; i++) { - seq.push(Filepath.resolve(path_template, objs[i])) + seq.push(Filepath.resolve(pathTemplate, objs[i])) } frameRange.min = 0 - frameRange.max = seq.length-1 + frameRange.max = seq.length - 1 currentFrame = 0 return seq @@ -325,13 +327,15 @@ FocusScope { // store attr name for output attributes that represent images for (var i = 0; i < displayedNode.attributes.count; i++) { var attr = displayedNode.attributes.at(i) - if (attr.isOutput && (attr.desc.semantic === "image" || attr.desc.semantic === "sequence" || attr.desc.semantic === "imageList") && attr.enabled) { + if (attr.isOutput && (attr.desc.semantic === "image" || attr.desc.semantic === "sequence" || + attr.desc.semantic === "imageList") && attr.enabled) { names.push(attr.name) } } } - if (!displayedNode || displayedNode.isComputable) names.push("gallery") + if (!displayedNode || displayedNode.isComputable) + names.push("gallery") outputAttribute.names = names } @@ -402,7 +406,8 @@ FocusScope { if (floatImageViewerLoader.item.containsMouse === false) { return null } - var pix = floatImageViewerLoader.item.pixelValueAt(Math.floor(floatImageViewerLoader.item.mouseX), Math.floor(floatImageViewerLoader.item.mouseY)) + var pix = floatImageViewerLoader.item.pixelValueAt(Math.floor(floatImageViewerLoader.item.mouseX), + Math.floor(floatImageViewerLoader.item.mouseY)) return pix } } @@ -447,7 +452,8 @@ FocusScope { // qtAliceVision Image Viewer ExifOrientedViewer { id: floatImageViewerLoader - active: root.aliceVisionPluginAvailable && (root.useFloatImageViewer || root.useLensDistortionViewer) && !panoramaViewerLoader.active + active: root.aliceVisionPluginAvailable && + (root.useFloatImageViewer || root.useLensDistortionViewer) && !panoramaViewerLoader.active visible: (floatImageViewerLoader.status === Loader.Ready) && active anchors.centerIn: parent orientationTag: imgContainer.orientationTag @@ -470,7 +476,8 @@ FocusScope { * opened, for example, and the images have a different size), then another auto-fit needs to be * performed */ if ((!fittedOnce && imgContainer.image && imgContainer.image.height > 0) || - (fittedOnce && ((width > 1 && previousWidth != width) || (height > 1 && previousHeight != height)))) { + (fittedOnce && ((width > 1 && previousWidth != width) || + (height > 1 && previousHeight != height)))) { fit() fittedOnce = true previousWidth = width @@ -518,7 +525,8 @@ FocusScope { // qtAliceVision Panorama Viewer Loader { id: panoramaViewerLoader - active: root.aliceVisionPluginAvailable && root.usePanoramaViewer && _reconstruction.activeNodes.get('sfm').node + active: root.aliceVisionPluginAvailable && root.usePanoramaViewer && + _reconstruction.activeNodes.get('sfm').node visible: (panoramaViewerLoader.status === Loader.Ready) && active anchors.centerIn: parent @@ -692,7 +700,7 @@ FocusScope { onJsonFolderChanged: { json = null - if(activeNode.attribute("autoDetect").value) { + if (activeNode.attribute("autoDetect").value) { // auto detection enabled var jsonPath = activeNode.attribute("output").value + "/detection.json" Request.get(Filepath.stringToUrl(jsonPath), function(xhr) { @@ -714,12 +722,12 @@ FocusScope { onNodeCircleRadiusChanged : { updateGizmo() } function updateGizmo() { - if(activeNode.attribute("autoDetect").value) { + if (activeNode.attribute("autoDetect").value) { // update gizmo from auto detection json file - if(json) { + if (json) { // json file found var data = json[currentViewId] - if(data && data[0]) { + if (data && data[0]) { // current view id found circleX = data[0].x circleY= data[0].y @@ -741,11 +749,13 @@ FocusScope { } onMoved: { - _reconstruction.setAttribute(activeNode.attribute("sphereCenter"), JSON.stringify([xoffset, yoffset])) + _reconstruction.setAttribute(activeNode.attribute("sphereCenter"), + JSON.stringify([xoffset, yoffset])) } onIncrementRadius: { - _reconstruction.setAttribute(activeNode.attribute("sphereRadius"), activeNode.attribute("sphereRadius").value + radiusOffset) + _reconstruction.setAttribute(activeNode.attribute("sphereRadius"), + activeNode.attribute("sphereRadius").value + radiusOffset) } } } @@ -1090,7 +1100,8 @@ FocusScope { left: parent.left margins: 2 } - active: root.aliceVisionPluginAvailable && displayFeatures.checked && featuresViewerLoader.status === Loader.Ready + active: root.aliceVisionPluginAvailable && displayFeatures.checked && + featuresViewerLoader.status === Loader.Ready sourceComponent: FeaturesInfoOverlay { pluginStatus: featuresViewerLoader.status From 79384905c598c7647b7182e27837a0f9b91abbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 27 Jun 2024 17:28:52 +0200 Subject: [PATCH 5/5] Add a clean-up commit to `.git-blame-ignore-revs` --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 402e93a2..4352ecae 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,5 @@ +# [Viewer] Clean-up: Harmonize syntax for the Viewer2D +9af65092b9e881c828430f54a73fb4522bc1e370 # [nodes] Harmonize the use of trailing commas across all the nodes 61a8dcd4e2878f80b2f320f2b1c3c9b41e999b82 # [nodes] Clean-up: Harmonize nodes' descriptions