diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 7cde3e26..7cf21eeb 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -881,7 +881,7 @@ class Graph(BaseObject): filterTypes (str list): (optional) only return the nodes of the given types (does not stop the visit, this is a post-process only) Returns: - The list of nodes from startNode to the graph leaves following edges. + The list of nodes and edges, from startNode to the graph leaves following edges. """ nodes = [] edges = [] diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml index c96a7a57..bc5beb11 100644 --- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml +++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml @@ -58,8 +58,9 @@ Item { } MenuItem { text: "Define As Center Image" - enabled: !root.readOnly && _viewpoint.viewId != -1 && _reconstruction && _reconstruction.sfmTransform - onClicked: _reconstruction.sfmTransform.attribute("transformation").value = _viewpoint.viewId.toString() + property var activeNode: _reconstruction.activeNodes.get("SfMTransform").node + enabled: !root.readOnly && _viewpoint.viewId != -1 && _reconstruction && activeNode + onClicked: activeNode.attribute("transformation").value = _viewpoint.viewId.toString() } } diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 1fe1e72a..d27b9716 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -16,7 +16,7 @@ Panel { property variant cameraInits property variant cameraInit - property variant hdrCameraInit + property variant tempCameraInit readonly property alias currentItem: grid.currentItem readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : "" readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined @@ -38,7 +38,7 @@ Panel { QtObject { id: m - property variant currentCameraInit: displayHDR.checked ? _reconstruction.hdrCameraInit : root.cameraInit + property variant currentCameraInit: _reconstruction.tempCameraInit ? _reconstruction.tempCameraInit : root.cameraInit property variant viewpoints: currentCameraInit ? currentCameraInit.attribute('viewpoints').value : undefined property bool readOnly: root.readOnly || displayHDR.checked } @@ -194,7 +194,7 @@ Panel { // Center of SfMTransform Loader { id: sfmTransformIndicator - active: (viewpoint.get("viewId").value == centerViewId) + active: viewpoint && (viewpoint.get("viewId").value == centerViewId) sourceComponent: ImageBadge { text: MaterialIcons.gamepad ToolTip.text: "Camera used to define the center of the scene." @@ -343,54 +343,114 @@ Panel { } footerContent: RowLayout { - - // Image count - RowLayout { - Layout.fillWidth: true - spacing: 8 - RowLayout { - MaterialLabel { text: MaterialIcons.image } - Label { text: grid.model.count } - } - RowLayout { - visible: _reconstruction.cameraInit && _reconstruction.nbCameras - MaterialLabel { text: MaterialIcons.videocam } - Label { text: _reconstruction.cameraInit ? _reconstruction.nbCameras : 0 } - } + // Images count + MaterialToolLabel { + ToolTip.text: grid.model.count + " Input Images" + iconText: MaterialIcons.image + label: grid.model.count.toString() + // enabled: grid.model.count > 0 + // margin: 4 + } + // cameras count + MaterialToolLabel { + ToolTip.text: label + " Estimated Cameras" + iconText: MaterialIcons.videocam + label: _reconstruction ? _reconstruction.nbCameras.toString() : "0" + // margin: 4 + // enabled: _reconstruction.cameraInit && _reconstruction.nbCameras } - MaterialToolButton { + Item { Layout.fillHeight: true; Layout.fillWidth: true } + + MaterialToolLabelButton { id: displayHDR - font.pointSize: 20 - padding: 0 - anchors.margins: 0 - implicitHeight: 14 - ToolTip.text: "Visualize HDR images: " + (_reconstruction.ldr2hdr ? _reconstruction.ldr2hdr.label : "No Node") - text: MaterialIcons.hdr_on - visible: _reconstruction.ldr2hdr - enabled: _reconstruction.ldr2hdr && _reconstruction.ldr2hdr.isComputed - property string nodeID: _reconstruction.ldr2hdr ? (_reconstruction.ldr2hdr.label + _reconstruction.ldr2hdr.isComputed) : "" + property var activeNode: _reconstruction.activeNodes.get("LdrToHdrMerge").node + ToolTip.text: "Visualize HDR images: " + (activeNode ? activeNode.label : "No Node") + iconText: MaterialIcons.filter + label: activeNode ? activeNode.attribute("nbBrackets").value : "" + // visible: activeNode + enabled: activeNode && activeNode.isComputed + property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : "" onNodeIDChanged: { - if(checked) - { - _reconstruction.setupLDRToHDRCameraInit(); + if(checked) { + open(); } } onEnabledChanged: { // Reset the toggle to avoid getting stuck // with the HDR node checked but disabled. - checked = false; + if(checked) { + checked = false; + close(); + } } checkable: true checked: false - onClicked: { _reconstruction.setupLDRToHDRCameraInit(); } + onClicked: { + if(checked) { + open(); + } else { + close(); + } + } + function open() { + if(imageProcessing.checked) + imageProcessing.checked = false; + _reconstruction.setupTempCameraInit(activeNode, "outSfMDataFilename"); + } + function close() { + _reconstruction.clearTempCameraInit(); + } } - Item { Layout.fillHeight: true; Layout.fillWidth: true } + MaterialToolButton { + id: imageProcessing + property var activeNode: _reconstruction.activeNodes.get("ImageProcessing").node + font.pointSize: 15 + padding: 0 + ToolTip.text: "Preprocessed Images: " + (activeNode ? activeNode.label : "No Node") + text: MaterialIcons.wallpaper + visible: activeNode && activeNode.attribute("outSfMData").value + enabled: activeNode && activeNode.isComputed + property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : "" + onNodeIDChanged: { + if(checked) { + open(); + } + } + onEnabledChanged: { + // Reset the toggle to avoid getting stuck + // with the HDR node checked but disabled. + if(checked) { + checked = false; + close(); + } + } + checkable: true + checked: false + onClicked: { + if(checked) { + open(); + } else { + close(); + } + } + function open() { + if(displayHDR.checked) + displayHDR.checked = false; + _reconstruction.setupTempCameraInit(activeNode, "outSfMData"); + } + function close() { + _reconstruction.clearTempCameraInit(); + } + } + + Item { Layout.fillHeight: true; width: 1 } // Thumbnail size icon and slider MaterialToolButton { text: MaterialIcons.photo_size_select_large + ToolTip.text: "Thumbnails Scale" padding: 0 anchors.margins: 0 font.pointSize: 11 @@ -404,5 +464,4 @@ Panel { implicitWidth: 70 } } - } diff --git a/meshroom/ui/qml/MaterialIcons/MLabel.qml b/meshroom/ui/qml/MaterialIcons/MLabel.qml new file mode 100644 index 00000000..2251a6b6 --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MLabel.qml @@ -0,0 +1,23 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.4 + + +/** + * MLabel is a standard Label. + * If ToolTip.text is set, it shows up a tooltip when hovered. + */ +Label { + padding: 4 + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + ToolTip.visible: mouseArea.containsMouse + ToolTip.delay: 500 + background: Rectangle { + anchors.fill: parent + color: mouseArea.containsMouse ? Qt.darker(parent.palette.base, 0.6) : "transparent" + } +} diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml index 2eea5411..b24e2ad4 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml @@ -1,5 +1,6 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 /** @@ -7,6 +8,7 @@ import QtQuick.Controls 2.3 * It also shows up its tooltip when hovered. */ ToolButton { + id: control font.family: MaterialIcons.fontFamily padding: 4 font.pointSize: 13 diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml new file mode 100644 index 00000000..b32df53d --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml @@ -0,0 +1,45 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + + +/** + * MaterialToolLabel is a Label with an icon (using MaterialIcons). + * It shows up its tooltip when hovered. + */ +Item { + id: control + property alias iconText: icon.text + property alias iconSize: icon.font.pointSize + property alias label: labelItem.text + width: childrenRect.width + height: childrenRect.height + + RowLayout { + Label { + id: icon + font.family: MaterialIcons.fontFamily + font.pointSize: 13 + padding: 0 + text: "" + color: palette.text + } + Label { + id: labelItem + text: "" + color: palette.text + } + Item { + width: 5 + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + ToolTip.visible: mouseArea.containsMouse + ToolTip.delay: 500 +} diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml new file mode 100644 index 00000000..6613dd51 --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml @@ -0,0 +1,51 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + + +/** + * MaterialToolButton is a standard ToolButton using MaterialIcons font. + * It also shows up its tooltip when hovered. + */ +ToolButton { + id: control + property alias iconText: icon.text + property alias iconSize: icon.font.pointSize + property alias label: labelItem.text + padding: 0 + ToolTip.visible: ToolTip.text && hovered + ToolTip.delay: 100 + width: childrenRect.width + height: childrenRect.height + contentItem: RowLayout { + Layout.margins: 0 + Label { + id: icon + font.family: MaterialIcons.fontFamily + font.pointSize: 13 + padding: 0 + text: "" + color: (checked ? palette.highlight : palette.text) + } + Label { + id: labelItem + text: "" + padding: 0 + color: (checked ? palette.highlight : palette.text) + } + } + background: Rectangle { + color: { + if(pressed || checked || hovered) + { + if(pressed || checked) + return Qt.darker(parent.palette.base, 1.3) + if(hovered) + return Qt.darker(parent.palette.base, 0.6) + } + return "transparent"; + } + + border.color: checked ? Qt.darker(parent.palette.base, 1.4) : "transparent" + } +} diff --git a/meshroom/ui/qml/MaterialIcons/qmldir b/meshroom/ui/qml/MaterialIcons/qmldir index c3d64e4b..41606081 100644 --- a/meshroom/ui/qml/MaterialIcons/qmldir +++ b/meshroom/ui/qml/MaterialIcons/qmldir @@ -1,4 +1,7 @@ module MaterialIcons singleton MaterialIcons 2.2 MaterialIcons.qml MaterialToolButton 2.2 MaterialToolButton.qml +MaterialToolLabelButton 2.2 MaterialToolLabelButton.qml +MaterialToolLabel 2.2 MaterialToolLabel.qml MaterialLabel 2.2 MaterialLabel.qml +MLabel 2.2 MLabel.qml diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 93dd3171..03b80089 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -104,10 +104,11 @@ FocusScope { } function getImageFile(type) { + var depthMapNode = _reconstruction.activeNodes.get('allDepthMap').node; if (type == "image") { return root.source; - } else if (_reconstruction.depthMap != undefined && _reconstruction.selectedViewId >= 0) { - return Filepath.stringToUrl(_reconstruction.depthMap.internalFolder+_reconstruction.selectedViewId+"_"+type+"Map.exr"); + } else if (depthMapNode != undefined && _reconstruction.selectedViewId >= 0) { + return Filepath.stringToUrl(depthMapNode.internalFolder+_reconstruction.selectedViewId+"_"+type+"Map.exr"); } return ""; } @@ -245,8 +246,8 @@ FocusScope { // note: requires QtAliceVision plugin - use a Loader to evaluate plugin availability at runtime Loader { id: featuresViewerLoader - active: displayFeatures.checked + property var activeNode: _reconstruction.activeNodes.get("FeatureExtraction").node // handle rotation/position based on available metadata rotation: { @@ -265,8 +266,8 @@ FocusScope { // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource setSource("FeaturesViewer.qml", { 'viewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), - 'model': Qt.binding(function() { return _reconstruction.featureExtraction ? _reconstruction.featureExtraction.attribute("describerTypes").value : ""; }), - 'featureFolder': Qt.binding(function() { return _reconstruction.featureExtraction ? Filepath.stringToUrl(_reconstruction.featureExtraction.attribute("output").value) : ""; }), + 'model': Qt.binding(function() { return activeNode ? activeNode.attribute("describerTypes").value : ""; }), + 'featureFolder': Qt.binding(function() { return activeNode ? Filepath.stringToUrl(activeNode.attribute("output").value) : ""; }), 'tracks': Qt.binding(function() { return mtracksLoader.status === Loader.Ready ? mtracksLoader.item : null; }), 'sfmData': Qt.binding(function() { return msfmDataLoader.status === Loader.Ready ? msfmDataLoader.item : null; }), }) @@ -281,7 +282,8 @@ FocusScope { // note: use a Loader to evaluate if a PanoramaInit node exist and displayFisheyeCircle checked at runtime Loader { anchors.centerIn: parent - active: (displayFisheyeCircleLoader.checked && _reconstruction.panoramaInit) + property var activeNode: _reconstruction.activeNodes.get("PanoramaInit").node + active: (displayFisheyeCircleLoader.checked && activeNode) // handle rotation/position based on available metadata rotation: { @@ -294,28 +296,28 @@ FocusScope { } sourceComponent: CircleGizmo { - property bool useAuto: _reconstruction.panoramaInit.attribute("estimateFisheyeCircle").value + property bool useAuto: activeNode.attribute("estimateFisheyeCircle").value readOnly: useAuto - visible: (!useAuto) || _reconstruction.panoramaInit.isComputed - property real userFisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value - property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(_reconstruction.panoramaInit) + visible: (!useAuto) || activeNode.isComputed + property real userFisheyeRadius: activeNode.attribute("fisheyeRadius").value + property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(activeNode) - x: useAuto ? fisheyeAutoParams.x : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value - y: useAuto ? fisheyeAutoParams.y : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value + x: useAuto ? fisheyeAutoParams.x : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value + y: useAuto ? fisheyeAutoParams.y : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value radius: useAuto ? fisheyeAutoParams.z : ((imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (userFisheyeRadius * 0.01)) border.width: Math.max(1, (3.0 / imgContainer.scale)) onMoved: { if(!useAuto) { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) + _reconstruction.setAttribute(activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) + _reconstruction.setAttribute(activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) } } onIncrementRadius: { if(!useAuto) { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeRadius"), _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radiusOffset) + _reconstruction.setAttribute(activeNode.attribute("fisheyeRadius"), activeNode.attribute("fisheyeRadius").value + radiusOffset) } } } @@ -352,8 +354,9 @@ FocusScope { // show which depthmap node is active Label { id: depthMapNodeName - visible: (_reconstruction.depthMap != undefined) && (imageType.type != "image") - text: (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "") + property var activeNode: _reconstruction.activeNodes.get("allDepthMap").node + visible: (activeNode != undefined) && (imageType.type != "image") + text: (activeNode != undefined ? activeNode.label : "") font.pointSize: 8 horizontalAlignment: TextInput.AlignLeft @@ -422,10 +425,9 @@ FocusScope { } Loader { id: mtracksLoader - // active: _reconstruction.featureMatching property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked - property var activeNode: _reconstruction.featureMatching + property var activeNode: _reconstruction.activeNodes.get('FeatureMatching').node property bool isComputed: activeNode && activeNode.isComputed active: false @@ -498,7 +500,7 @@ FocusScope { active: displayFeatures.checked && featuresViewerLoader.status === Loader.Ready sourceComponent: FeaturesInfoOverlay { - featureExtractionNode: _reconstruction.featureExtraction + featureExtractionNode: _reconstruction.activeNodes.get('FeatureExtraction').node pluginStatus: featuresViewerLoader.status featuresViewer: featuresViewerLoader.item } @@ -514,9 +516,8 @@ FocusScope { anchors.fill: parent // zoom label - Label { + MLabel { text: ((imgContainer.image && (imgContainer.image.status === Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" - state: "xsmall" MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton @@ -533,6 +534,7 @@ FocusScope { } } } + ToolTip.text: "Zoom" } MaterialToolButton { id: displayAlphaBackground @@ -566,31 +568,30 @@ FocusScope { } MaterialToolButton { id: displayFisheyeCircleLoader - ToolTip.text: "Display Fisheye Circle: " + (_reconstruction.panoramaInit ? _reconstruction.panoramaInit.label : "No Node") - text: MaterialIcons.panorama_fish_eye + property var activeNode: _reconstruction.activeNodes.get('PanoramaInit').node + ToolTip.text: "Display Fisheye Circle: " + (activeNode ? activeNode.label : "No Node") + text: MaterialIcons.vignette + // text: MaterialIcons.panorama_fish_eye font.pointSize: 11 Layout.minimumWidth: 0 checkable: true checked: false - enabled: _reconstruction.panoramaInit && _reconstruction.panoramaInit.attribute("useFisheye").value - visible: _reconstruction.panoramaInit + enabled: activeNode && activeNode.attribute("useFisheye").value + visible: activeNode } Label { id: resolutionLabel Layout.fillWidth: true - text: imgContainer.image ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" + text: (imgContainer.image && imgContainer.image.sourceSize.width > 0) ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter - /*Rectangle { - anchors.fill: parent - color: "blue" - }*/ } ComboBox { id: imageType + property var activeNode: _reconstruction.activeNodes.get('allDepthMap').node // set min size to 5 characters + one margin for the combobox clip: true Layout.minimumWidth: 0 @@ -601,12 +602,13 @@ FocusScope { property string type: enabled ? types[currentIndex] : types[0] model: types - enabled: _reconstruction.depthMap != undefined + enabled: activeNode } MaterialToolButton { - enabled: _reconstruction.depthMap != undefined - ToolTip.text: "View Depth Map in 3D (" + (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "No DepthMap Node Selected") + ")" + property var activeNode: _reconstruction.activeNodes.get('allDepthMap').node + enabled: activeNode + ToolTip.text: "View Depth Map in 3D (" + (activeNode ? activeNode.label : "No DepthMap Node Selected") + ")" text: MaterialIcons.input font.pointSize: 11 Layout.minimumWidth: 0 @@ -618,6 +620,7 @@ FocusScope { MaterialToolButton { id: displaySfmStatsView + property var activeNode: _reconstruction.activeNodes.get('sfm').node font.family: MaterialIcons.fontFamily text: MaterialIcons.assessment @@ -630,10 +633,9 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed && _reconstruction.selectedViewId >= 0 + enabled: activeNode && activeNode.isComputed && _reconstruction.selectedViewId >= 0 onCheckedChanged: { - if(checked == true) - { + if(checked == true) { displaySfmDataGlobalStats.checked = false metadataCB.checked = false } @@ -642,6 +644,7 @@ FocusScope { MaterialToolButton { id: displaySfmDataGlobalStats + property var activeNode: _reconstruction.activeNodes.get('sfm').node font.family: MaterialIcons.fontFamily text: MaterialIcons.language @@ -654,10 +657,9 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed + enabled: activeNode && activeNode.isComputed onCheckedChanged: { - if(checked == true) - { + if(checked == true) { displaySfmStatsView.checked = false metadataCB.checked = false } diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 3a5e80fb..4caafc02 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -65,7 +65,7 @@ Item { readOnly: root.readOnly cameraInits: root.cameraInits cameraInit: reconstruction.cameraInit - hdrCameraInit: reconstruction.hdrCameraInit + tempCameraInit: reconstruction.tempCameraInit currentIndex: reconstruction.cameraInitIndex onRemoveImageRequest: reconstruction.removeAttribute(attribute) onFilesDropped: reconstruction.handleFilesDrop(drop, augmentSfm ? null : cameraInit) @@ -191,7 +191,7 @@ Item { mediaLibrary: viewer3D.library camera: viewer3D.mainCamera uigraph: reconstruction - onNodeActivated: _reconstruction.setActiveNodeOfType(node) + onNodeActivated: _reconstruction.setActiveNode(node) } } } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index d82aea30..c4249fbd 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -703,7 +703,6 @@ ApplicationWindow { } } - GraphEditor { id: graphEditor @@ -713,7 +712,7 @@ ApplicationWindow { readOnly: graphLocked onNodeDoubleClicked: { - _reconstruction.setActiveNodeOfType(node); + _reconstruction.setActiveNode(node); let viewable = false; for(var i=0; i < node.attributes.count; ++i) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 2906da43..be17bd3d 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -8,6 +8,7 @@ from PySide2.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF from PySide2.QtGui import QMatrix4x4, QMatrix3x3, QQuaternion, QVector3D, QVector2D import meshroom.core +import meshroom.common from meshroom import multiview from meshroom.common.qt import QObjectListModel from meshroom.core import Version @@ -196,6 +197,7 @@ class ViewpointWrapper(QObject): self._reconstructed = False # PrepareDenseScene self._undistortedImagePath = '' + self._activeNode_PrepareDenseScene = self._reconstruction.activeNodes.get("PrepareDenseScene") # update internally cached variables self._updateInitialParams() @@ -205,7 +207,7 @@ class ViewpointWrapper(QObject): # trigger internal members updates when reconstruction members changes self._reconstruction.cameraInitChanged.connect(self._updateInitialParams) self._reconstruction.sfmReportChanged.connect(self._updateSfMParams) - self._reconstruction.prepareDenseSceneChanged.connect(self._updateDenseSceneParams) + self._activeNode_PrepareDenseScene.nodeChanged.connect(self._updateDenseSceneParams) def _updateInitialParams(self): """ Update internal members depending on CameraInit. """ @@ -235,11 +237,11 @@ class ViewpointWrapper(QObject): def _updateDenseSceneParams(self): """ Update internal members depending on PrepareDenseScene. """ # undistorted image path - if not self._reconstruction.prepareDenseScene: + if not self._activeNode_PrepareDenseScene.node: self._undistortedImagePath = '' else: - filename = "{}.{}".format(self._viewpoint.viewId.value, self._reconstruction.prepareDenseScene.outputFileType.value) - self._undistortedImagePath = os.path.join(self._reconstruction.prepareDenseScene.output.value, filename) + filename = "{}.{}".format(self._viewpoint.viewId.value, self._activeNode_PrepareDenseScene.node.outputFileType.value) + self._undistortedImagePath = os.path.join(self._activeNode_PrepareDenseScene.node.output.value, filename) self.denseSceneParamsChanged.emit() @Property(type=QObject, constant=True) @@ -388,37 +390,50 @@ def parseSfMJsonFile(sfmJsonFile): return views, poses, intrinsics -sfmHolderNodeTypes = ["StructureFromMotion", "GlobalSfM", "PanoramaEstimation", "SfMTransfer", "SfMTransform", "SfMAlignment"] +class ActiveNode(QObject): + """ + Hold one active node for a given NodeType. + """ + def __init__(self, nodeType, parent=None): + super(ActiveNode, self).__init__(parent) + self.nodeType = nodeType + self._node = None + + nodeChanged = Signal() + node = makeProperty(QObject, "_node", nodeChanged, resetOnDestroy=True) class Reconstruction(UIGraph): """ Specialization of a UIGraph designed to manage a 3D reconstruction. """ + activeNodeCategories = { + "sfm": ["StructureFromMotion", "GlobalSfM", "PanoramaEstimation", "SfMTransfer", "SfMTransform", + "SfMAlignment"], + "undistort": ["PrepareDenseScene", "PanoramaWarping"], + "allDepthMap": ["DepthMap", "DepthMapFilter"], + } def __init__(self, defaultPipeline='', parent=None): super(Reconstruction, self).__init__(parent) # initialize member variables for key steps of the 3D reconstruction pipeline + self._activeNodes = meshroom.common.DictModel(keyAttrName="nodeType") + self.initActiveNodes() + # - CameraInit self._cameraInit = None # current CameraInit node self._cameraInits = QObjectListModel(parent=self) # all CameraInit nodes self._buildingIntrinsics = False self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) - self._hdrCameraInit = None + self.cameraInitChanged.connect(self.onCameraInitChanged) + + self._tempCameraInit = None self.importImagesFailed.connect(self.onImportImagesFailed) - # - Feature Extraction - self._featureExtraction = None - self.cameraInitChanged.connect(self.updateFeatureExtraction) - - # - Feature Matching - self._featureMatching = None - self.cameraInitChanged.connect(self.updateFeatureMatching) - # - SfM self._sfm = None self._views = None @@ -428,28 +443,6 @@ class Reconstruction(UIGraph): self._selectedViewpoint = None self._liveSfmManager = LiveSfmManager(self) - # - Prepare Dense Scene (undistorted images) - self._prepareDenseScene = None - - # - Depth Map - self._depthMap = None - self.cameraInitChanged.connect(self.updateDepthMapNode) - - # - Texturing - self._texturing = None - - # - LDR2HDR - self._ldr2hdr = None - self.cameraInitChanged.connect(self.updateLdr2hdrNode) - - # - PanoramaInit - self._panoramaInit = None - self.cameraInitChanged.connect(self.updatePanoramaInitNode) - - # - PanoramaInit - self._sfmTransform = None - self.cameraInitChanged.connect(self.updateSfMTransformNode) - # react to internal graph changes to update those variables self.graphChanged.connect(self.onGraphChanged) @@ -458,6 +451,18 @@ class Reconstruction(UIGraph): def setDefaultPipeline(self, defaultPipeline): self._defaultPipeline = defaultPipeline + def initActiveNodes(self): + # Create all possible entries + for category, _ in self.activeNodeCategories.iteritems(): + self._activeNodes.add(ActiveNode(category, self)) + for nodeType, _ in meshroom.core.nodesDesc.iteritems(): + self._activeNodes.add(ActiveNode(nodeType, self)) + + def onCameraInitChanged(self): + # Update active nodes when CameraInit changes + nodes = self._graph.nodesFromNode(self._cameraInit)[0] + self.setActiveNodes(nodes) + @Slot() @Slot(str) def new(self, pipeline=None): @@ -528,16 +533,8 @@ class Reconstruction(UIGraph): """ React to the change of the internal graph. """ self._liveSfmManager.reset() self.selectedViewId = "-1" - self.featureExtraction = None - self.featureMatching = None self.sfm = None - self.prepareDenseScene = None - self.depthMap = None - self.texturing = None - self.ldr2hdr = None - self.hdrCameraInit = None - self.panoramaInit = None - self.sfmTransform = None + self.tempCameraInit = None self.updateCameraInits() if not self._graph: return @@ -578,35 +575,23 @@ class Reconstruction(UIGraph): camInit = self._cameraInits[idx] if self._cameraInits else None self.cameraInit = camInit - def updateFeatureExtraction(self): - """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.featureExtraction = self.lastNodeOfType(['FeatureExtraction'], self.cameraInit) if self.cameraInit else None - - def updateFeatureMatching(self): - """ Set the current FeatureMatching node based on the current CameraInit node. """ - self.featureMatching = self.lastNodeOfType(['FeatureMatching'], self.cameraInit) if self.cameraInit else None - - def updateDepthMapNode(self): - """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.depthMap = self.lastNodeOfType(['DepthMapFilter'], self.cameraInit) if self.cameraInit else None - - def updateLdr2hdrNode(self): - """ Set the current LDR2HDR node based on the current CameraInit node. """ - self.ldr2hdr = self.lastNodeOfType(['LdrToHdrMerge'], self.cameraInit) if self.cameraInit else None - @Slot() - def setupLDRToHDRCameraInit(self): - if not self.ldr2hdr: - self.hdrCameraInit = Node("CameraInit") + def clearTempCameraInit(self): + self.tempCameraInit = None + + @Slot(QObject, str) + def setupTempCameraInit(self, node, attrName): + if not node or not attrName: + self.tempCameraInit = None return - sfmFile = self.ldr2hdr.attribute("outSfMDataFilename").value + sfmFile = node.attribute(attrName).value if not sfmFile or not os.path.isfile(sfmFile): - self.hdrCameraInit = Node("CameraInit") + self.tempCameraInit = None return nodeDesc = meshroom.core.nodesDesc["CameraInit"]() views, intrinsics = nodeDesc.readSfMData(sfmFile) tmpCameraInit = Node("CameraInit", viewpoints=views, intrinsics=intrinsics) - self.hdrCameraInit = tmpCameraInit + self.tempCameraInit = tmpCameraInit @Slot(QObject, result=QVector3D) def getAutoFisheyeCircle(self, panoramaInit): @@ -633,17 +618,9 @@ class Reconstruction(UIGraph): float(intrinsic.get("fisheyeCircleRadius", 0.0))) return res - def updatePanoramaInitNode(self): - """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.panoramaInit = self.lastNodeOfType(["PanoramaInit"], self.cameraInit) if self.cameraInit else None - - def updateSfMTransformNode(self): - """ Set the current SfMTransform node based on the current CameraInit node. """ - self.sfmTransform = self.lastNodeOfType(["SfMTransform"], self.cameraInit) if self.cameraInit else None - def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ - return self.lastNodeOfType(sfmHolderNodeTypes, self._cameraInit, Status.SUCCESS) + return self.lastNodeOfType(self.activeNodeCategories['sfm'], self._cameraInit, Status.SUCCESS) def lastNodeOfType(self, nodeTypes, startNode, preferredStatus=None): """ @@ -938,10 +915,11 @@ class Reconstruction(UIGraph): self._buildingIntrinsics = value self.buildingIntrinsicsChanged.emit() + activeNodes = makeProperty(QObject, "_activeNodes", resetOnDestroy=True) cameraInitChanged = Signal() cameraInit = makeProperty(QObject, "_cameraInit", cameraInitChanged, resetOnDestroy=True) - hdrCameraInitChanged = Signal() - hdrCameraInit = makeProperty(QObject, "_hdrCameraInit", hdrCameraInitChanged, resetOnDestroy=True) + tempCameraInitChanged = Signal() + tempCameraInit = makeProperty(QObject, "_tempCameraInit", tempCameraInitChanged, resetOnDestroy=True) cameraInitIndex = Property(int, getCameraInitIndex, setCameraInitIndex, notify=cameraInitChanged) viewpoints = Property(QObject, getViewpoints, notify=cameraInitChanged) cameraInits = Property(QObject, lambda self: self._cameraInits, constant=True) @@ -952,27 +930,30 @@ class Reconstruction(UIGraph): liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) @Slot(QObject) - def setActiveNodeOfType(self, node): + def setActiveNode(self, node): """ Set node as the active node of its type. """ - if node.nodeType in sfmHolderNodeTypes: - self.sfm = node + for category, nodeTypes in self.activeNodeCategories.iteritems(): + if node.nodeType in nodeTypes: + self.activeNodes.get(category).node = node + if category == 'sfm': + self.setSfm(node) + self.activeNodes.get(node.nodeType).node = node - if node.nodeType == "FeatureExtraction": - self.featureExtraction = node - elif node.nodeType == "FeatureMatching": - self.featureMatching = node - elif node.nodeType == "CameraInit": - self.cameraInit = node - elif node.nodeType == "PrepareDenseScene": - self.prepareDenseScene = node - elif node.nodeType in ("DepthMap", "DepthMapFilter"): - self.depthMap = node - elif node.nodeType == "LdrToHdrMerge": - self.ldr2hdr = node - elif node.nodeType == "PanoramaInit": - self.panoramaInit = node - elif node.nodeType == "SfMTransform": - self.sfmTransform = node + @Slot(QObject) + def setActiveNodes(self, nodes): + """ Set node as the active node of its type. """ + # Setup the active node per category only once, on the last one + nodesByCategory = {} + for node in nodes: + for category, nodeTypes in self.activeNodeCategories.iteritems(): + if node.nodeType in nodeTypes: + nodesByCategory[category] = node + for category, node in nodesByCategory.iteritems(): + self.activeNodes.get(category).node = node + if category == 'sfm': + self.setSfm(node) + for node in nodes: + self.activeNodes.get(node.nodeType).node = node def updateSfMResults(self): """ @@ -1021,9 +1002,6 @@ class Reconstruction(UIGraph): self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) - self.texturing = self.lastNodeOfType(["Texturing"], self._sfm, Status.SUCCESS) - self.prepareDenseScene = self.lastNodeOfType(["PrepareDenseScene"], self._sfm, Status.SUCCESS) - @Slot(QObject, result=bool) def isInViews(self, viewpoint): if not viewpoint: @@ -1129,35 +1107,11 @@ class Reconstruction(UIGraph): sfmChanged = Signal() sfm = Property(QObject, getSfm, setSfm, notify=sfmChanged) - featureExtractionChanged = Signal() - featureExtraction = makeProperty(QObject, "_featureExtraction", featureExtractionChanged, resetOnDestroy=True) - - featureMatchingChanged = Signal() - featureMatching = makeProperty(QObject, "_featureMatching", featureMatchingChanged, resetOnDestroy=True) - sfmReportChanged = Signal() # convenient property for QML binding re-evaluation when sfm report changes sfmReport = Property(bool, lambda self: len(self._poses) > 0, notify=sfmReportChanged) sfmAugmented = Signal(Node, Node) - prepareDenseSceneChanged = Signal() - prepareDenseScene = makeProperty(QObject, "_prepareDenseScene", notify=prepareDenseSceneChanged, resetOnDestroy=True) - - depthMapChanged = Signal() - depthMap = makeProperty(QObject, "_depthMap", depthMapChanged, resetOnDestroy=True) - - texturingChanged = Signal() - texturing = makeProperty(QObject, "_texturing", notify=texturingChanged, resetOnDestroy=True) - - ldr2hdrChanged = Signal() - ldr2hdr = makeProperty(QObject, "_ldr2hdr", notify=ldr2hdrChanged, resetOnDestroy=True) - - panoramaInitChanged = Signal() - panoramaInit = makeProperty(QObject, "_panoramaInit", notify=panoramaInitChanged, resetOnDestroy=True) - - sfmTransformChanged = Signal() - sfmTransform = makeProperty(QObject, "_sfmTransform", notify=sfmTransformChanged, resetOnDestroy=True) - nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) # Signals to propagate high-level messages