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 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/__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() diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 377dd675..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 } } @@ -224,12 +224,15 @@ 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")) { + 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) } @@ -238,20 +241,22 @@ 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) } 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] @@ -260,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 } @@ -270,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 } @@ -282,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 @@ -322,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 } @@ -399,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 } } @@ -444,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 @@ -467,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 @@ -515,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 @@ -689,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) { @@ -711,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 @@ -738,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) } } } @@ -1087,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 diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index c331a97f..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 @@ -1158,7 +1160,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): @@ -1217,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() @@ -1234,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)