From 00857a7a88a5a60d29f50181a9c05c403e70bc30 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:25:40 +0200 Subject: [PATCH 1/6] [ui] Reconstruction: rename 'endNode' to 'texturing' + use makeProperty helper --- meshroom/ui/qml/WorkspaceView.qml | 6 +++--- meshroom/ui/reconstruction.py | 23 +++++------------------ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index d0f2c12e..1a9dc3d3 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -133,8 +133,8 @@ Item { // Load reconstructed model Button { - readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null - readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS" + readonly property var outputAttribute: _reconstruction.texturing ? _reconstruction.texturing.attribute("outputMesh") : null + readonly property bool outputReady: outputAttribute && _reconstruction.texturing.globalStatus === "SUCCESS" readonly property int outputMediaIndex: viewer3D.library.find(outputAttribute) text: "Load Model" @@ -142,7 +142,7 @@ Item { anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter visible: outputReady && outputMediaIndex == -1 - onClicked: viewer3D.view(_reconstruction.endNode.attribute("outputMesh")) + onClicked: viewer3D.view(_reconstruction.texturing.attribute("outputMesh")) } } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b16c623a..a11b6784 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -164,7 +164,7 @@ class Reconstruction(UIGraph): self._buildingIntrinsics = False self._cameraInit = None self._cameraInits = QObjectListModel(parent=self) - self._endNode = None + self._texturing = None self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) self.graphChanged.connect(self.onGraphChanged) self._liveSfmManager = LiveSfmManager(self) @@ -212,7 +212,7 @@ class Reconstruction(UIGraph): """ React to the change of the internal graph. """ self._liveSfmManager.reset() self.sfm = None - self.endNode = None + self.texturing = None self.updateCameraInits() if not self._graph: return @@ -493,21 +493,8 @@ class Reconstruction(UIGraph): self._sfm.chunks[0].statusChanged.disconnect(self.updateViewsAndPoses) self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) - self.setEndNode(self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS)) - def setEndNode(self, node=None): - if self._endNode == node: - return - if self._endNode: - try: - self._endNode.destroyed.disconnect(self.setEndNode) - except RuntimeError: - # self._endNode might have been destroyed at this point, causing PySide2 to throw a RuntimeError - pass - self._endNode = node - if self._endNode: - self._endNode.destroyed.connect(self.setEndNode) - self.endNodeChanged.emit() + self.texturing = self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS) @Slot(QObject, result=bool) def isInViews(self, viewpoint): @@ -570,8 +557,8 @@ class Reconstruction(UIGraph): # 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) - endNodeChanged = Signal() - endNode = Property(QObject, lambda self: self._endNode, setEndNode, notify=endNodeChanged) + texturingChanged = Signal() + texturing = makeProperty(QObject, "_texturing", notify=texturingChanged) nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) From a358c4674840b39a7e10035c85dac5b4abf3226c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:31:43 +0200 Subject: [PATCH 2/6] [ui] Reconstruction: improve constructor readability --- meshroom/ui/reconstruction.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index a11b6784..3bda8def 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -161,19 +161,27 @@ class Reconstruction(UIGraph): def __init__(self, graphFilepath='', parent=None): super(Reconstruction, self).__init__(graphFilepath, parent) - self._buildingIntrinsics = False - self._cameraInit = None - self._cameraInits = QObjectListModel(parent=self) - self._texturing = None - self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) - self.graphChanged.connect(self.onGraphChanged) - self._liveSfmManager = LiveSfmManager(self) - # SfM result + # initialize member variables for key steps of the 3D reconstruction pipeline + + # - CameraInit + self._cameraInit = None # current CameraInit node + self._cameraInits = QObjectListModel(parent=self) # all CameraInit nodes + self._buildingIntrinsics = False + self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) + + # - SfM self._sfm = None self._views = None self._poses = None self._selectedViewId = None + self._liveSfmManager = LiveSfmManager(self) + + # - Texturing + self._texturing = None + + # react to internal graph changes to update those variables + self.graphChanged.connect(self.onGraphChanged) if graphFilepath: self.onGraphChanged() From 4435ba9e1bdfea361b56e463d21d09d0ed3b8320 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:36:10 +0200 Subject: [PATCH 3/6] [ui] Reconstruction: add 'featureExtraction' member * add featureExtraction property to keep track of current FeatureExtraction node * update it when current CameraInit node is changed * allow to set current CameraInit by double clicking on a CameraInit node from the Graph Editor --- meshroom/ui/qml/main.qml | 8 ++++++++ meshroom/ui/reconstruction.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 164dd224..989355aa 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -590,6 +590,14 @@ ApplicationWindow { { _reconstruction.sfm = node; } + else if(node.nodeType === "FeatureExtraction") + { + _reconstruction.featureExtraction = node; + } + else if(node.nodeType === "CameraInit") + { + _reconstruction.cameraInit = node; + } for(var i=0; i < node.attributes.count; ++i) { var attr = node.attributes.at(i) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3bda8def..9ef09dad 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -170,6 +170,10 @@ class Reconstruction(UIGraph): self._buildingIntrinsics = False self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) + # - Feature Extraction + self._featureExtraction = None + self.cameraInitChanged.connect(self.updateFeatureExtraction) + # - SfM self._sfm = None self._views = None @@ -219,6 +223,7 @@ class Reconstruction(UIGraph): def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() + self.featureExtraction = None self.sfm = None self.texturing = None self.updateCameraInits() @@ -257,6 +262,10 @@ 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 lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS) @@ -561,6 +570,10 @@ class Reconstruction(UIGraph): sfmChanged = Signal() sfm = Property(QObject, getSfm, setSfm, notify=sfmChanged) + + featureExtractionChanged = Signal() + featureExtraction = makeProperty(QObject, "_featureExtraction", featureExtractionChanged, 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) From 8dd0a4be22873c64e04ee49a81bfcf9e375661a6 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 4 Jul 2019 17:51:44 +0200 Subject: [PATCH 4/6] [ui] New FeaturesViewer component FeaturesViewer wraps QtAliceVision plugin's FeaturesViewer component to display the extracted feature points of a View. --- meshroom/ui/qml/Utils/Colors.qml | 3 ++ meshroom/ui/qml/Viewer/FeaturesViewer.qml | 40 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 meshroom/ui/qml/Viewer/FeaturesViewer.qml diff --git a/meshroom/ui/qml/Utils/Colors.qml b/meshroom/ui/qml/Utils/Colors.qml index 4dea34be..54f6fd66 100644 --- a/meshroom/ui/qml/Utils/Colors.qml +++ b/meshroom/ui/qml/Utils/Colors.qml @@ -14,4 +14,7 @@ QtObject { readonly property color yellow: "#FFEB3B" readonly property color red: "#F44336" readonly property color blue: "#03A9F4" + readonly property color cyan: "#00BCD4" + readonly property color pink: "#E91E63" + readonly property color lime: "#CDDC39" } diff --git a/meshroom/ui/qml/Viewer/FeaturesViewer.qml b/meshroom/ui/qml/Viewer/FeaturesViewer.qml new file mode 100644 index 00000000..6e21de14 --- /dev/null +++ b/meshroom/ui/qml/Viewer/FeaturesViewer.qml @@ -0,0 +1,40 @@ +import QtQuick 2.11 +import AliceVision 1.0 as AliceVision + +import Utils 1.0 + +/** + * FeaturesViewer displays the extracted feature points of a View. + * Requires QtAliceVision plugin. + */ +Repeater { + id: root + + /// ViewID to display the features of + property int viewId + /// Folder containing the features files + property string folder + /// The list of describer types to load + property alias describerTypes: root.model + /// List of available display modes + readonly property var displayModes: ['Points', 'Squares', 'Oriented Squares'] + /// Current display mode index + property int displayMode: 0 + /// The list of colors used for displaying several describers + property var colors: [Colors.blue, Colors.red, Colors.yellow, Colors.green, Colors.orange, Colors.cyan, Colors.pink, Colors.lime] + /// Offset the color list + property int colorOffset: 0 + + model: root.describerTypes + + // instantiate one FeaturesViewer by describer type + delegate: AliceVision.FeaturesViewer { + readonly property int colorIndex: (index+root.colorOffset)%root.colors.length + describerType: modelData + folder: root.folder + viewId: root.viewId + color: root.colors[colorIndex] + displayMode: root.displayMode + } + +} From 4563d32e3e3cad86604188f1566147aa9ccdd2d5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 4 Jul 2019 17:53:30 +0200 Subject: [PATCH 5/6] [ui] Viewer2D: add FeaturesViewer + dedicated overlay Make FeaturesViewer available in Viewer2D to display the feature points of the current view, based on the current FeatureExtraction node. * FeaturesInfoOverlay: overlay that displays info and provides controls over a FeaturesViewer component. * ColorChart: color picker based on a set of predefined colors. --- meshroom/ui/qml/Controls/ColorChart.qml | 66 +++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + .../ui/qml/Viewer/FeaturesInfoOverlay.qml | 111 ++++++++++++++++++ meshroom/ui/qml/Viewer/Viewer2D.qml | 53 +++++++++ 4 files changed, 231 insertions(+) create mode 100644 meshroom/ui/qml/Controls/ColorChart.qml create mode 100644 meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml diff --git a/meshroom/ui/qml/Controls/ColorChart.qml b/meshroom/ui/qml/Controls/ColorChart.qml new file mode 100644 index 00000000..82667f22 --- /dev/null +++ b/meshroom/ui/qml/Controls/ColorChart.qml @@ -0,0 +1,66 @@ +import QtQuick 2.10 +import QtQuick.Controls 2.10 + +import Utils 1.0 + +/** + * ColorChart is a color picker based on a set of predefined colors. + * It takes the form of a ToolButton that pops-up its palette when pressed. + */ +ToolButton { + id: root + + property var colors: ["red", "green", "blue"] + property int currentIndex: 0 + + signal colorPicked(var colorIndex) + + background: Rectangle { + color: root.colors[root.currentIndex] + border.width: hovered ? 1 : 0 + border.color: Colors.sysPalette.midlight + } + + onPressed: palettePopup.open() + + // Popup for the color palette + Popup { + id: palettePopup + + padding: 4 + // content width is missing side padding (hence the + padding*2) + implicitWidth: colorChart.contentItem.width + padding*2 + + // center the current color + y: - (root.height - padding) / 2 + x: - colorChart.currentItem.x - padding + + // Colors palette + ListView { + id: colorChart + implicitHeight: contentItem.childrenRect.height + implicitWidth: contentWidth + orientation: ListView.Horizontal + spacing: 2 + currentIndex: root.currentIndex + model: root.colors + // display each color as a ToolButton with a custom background + delegate: ToolButton { + padding: 0 + width: root.width + height: root.height + background: Rectangle { + color: modelData + // display border of current/selected item + border.width: hovered || index === colorChart.currentIndex ? 1 : 0 + border.color: Colors.sysPalette.midlight + } + + onClicked: { + colorPicked(index); + palettePopup.close(); + } + } + } + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 295947a2..00e89655 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -1,5 +1,6 @@ module Controls +ColorChart 1.0 ColorChart.qml FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml new file mode 100644 index 00000000..acc9415f --- /dev/null +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -0,0 +1,111 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.11 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 + +import Utils 1.0 +import Controls 1.0 + +/** + * FeaturesInfoOverlay is an overlay that displays info and + * provides controls over a FeaturesViewer component. + */ +FloatingPane { + id: root + + property int pluginStatus: Loader.Null + property Item featuresViewer: null + property var featureExtractionNode: null + + ColumnLayout { + + // Header + RowLayout { + // FeatureExtraction node name + Label { + text: featureExtractionNode.label + Layout.fillWidth: true + } + // Settings menu + Loader { + active: root.pluginStatus === Loader.Ready + sourceComponent: MaterialToolButton { + text: MaterialIcons.settings + font.pointSize: 10 + onClicked: settingsMenu.popup(width, 0) + Menu { + id: settingsMenu + padding: 4 + implicitWidth: 210 + + RowLayout { + Label { + text: "Display Mode:" + } + ComboBox { + id: displayModeCB + flat: true + Layout.fillWidth: true + model: featuresViewer.displayModes + onActivated: featuresViewer.displayMode = currentIndex + } + } + } + } + } + } + + // Error message if AliceVision plugin is unavailable + Label { + visible: root.pluginStatus === Loader.Error + text: "AliceVision plugin is required to display Features" + color: Colors.red + } + + // Feature types + ListView { + implicitHeight: contentHeight + implicitWidth: contentItem.childrenRect.width + + model: featuresViewer !== null ? featuresViewer.model : 0 + + delegate: RowLayout { + id: featureType + + property var viewer: featuresViewer.itemAt(index) + spacing: 4 + + // Visibility toogle + MaterialToolButton { + text: featureType.viewer.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off + onClicked: featureType.viewer.visible = !featureType.viewer.visible + font.pointSize: 10 + opacity: featureType.viewer.visible ? 1.0 : 0.6 + } + // ColorChart picker + ColorChart { + implicitWidth: 12 + implicitHeight: implicitWidth + colors: featuresViewer.colors + currentIndex: featureType.viewer.colorIndex + // offset FeaturesViewer color set when changing the color of one feature type + onColorPicked: featuresViewer.colorOffset = colorIndex - index + } + // Feature type name + Label { + text: featureType.viewer.describerType + (featureType.viewer.loading ? "" : ": " + featureType.viewer.features.length) + } + // Feature loading status + Loader { + active: featureType.viewer.loading + sourceComponent: BusyIndicator { + padding: 0 + implicitWidth: 12 + implicitHeight: 12 + running: true + } + } + } + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index d7954b19..1a4eb8c4 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -77,8 +77,39 @@ FocusScope { visible: image.status === Image.Loading } + + // FeatureViewer: display view extracted feature points + // note: requires QtAliceVision plugin - use a Loader to evaluate plugin avaibility at runtime + Loader { + id: featuresViewerLoader + + active: displayFeatures.checked + + // handle rotation/position based on available metadata + rotation: { + var orientation = metadata ? metadata["Orientation"] : 0 + switch(orientation) { + case "6": return 90; + case "8": return -90; + default: return 0; + } + } + x: rotation === 90 ? image.paintedWidth : 0 + y: rotation === -90 ? image.paintedHeight : 0 + + Component.onCompleted: { + // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource + setSource("FeaturesViewer.qml", { + 'active': Qt.binding(function() { return displayFeatures.checked; }), + 'viewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), + 'model': Qt.binding(function() { return _reconstruction.featureExtraction.attribute("describerTypes").value; }), + 'folder': Qt.binding(function() { return Filepath.stringToUrl(_reconstruction.featureExtraction.attribute("output").value); }), + }) + } + } } + // Busy indicator BusyIndicator { anchors.centerIn: parent @@ -147,6 +178,21 @@ FocusScope { metadata: visible ? root.metadata : {} } + + Loader { + id: featuresOverlay + anchors.bottom: bottomToolbar.top + anchors.left: parent.left + anchors.margins: 2 + active: displayFeatures.checked + + sourceComponent: FeaturesInfoOverlay { + featureExtractionNode: _reconstruction.featureExtraction + pluginStatus: featuresViewerLoader.status + featuresViewer: featuresViewerLoader.item + } + } + FloatingPane { id: bottomToolbar anchors.bottom: parent.bottom @@ -163,6 +209,13 @@ FocusScope { text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x" state: "xsmall" } + MaterialToolButton { + id: displayFeatures + font.pointSize: 11 + ToolTip.text: "Display Features" + checkable: true + text: MaterialIcons.scatter_plot + } Item { Layout.fillWidth: true From f5607cb1271138269f51cb7a350b990d357fea27 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 24 Jul 2019 18:13:23 +0200 Subject: [PATCH 6/6] [ui] downgrade QtQuick import version for retro-compatibility --- meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index acc9415f..37e930c3 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -1,5 +1,5 @@ -import QtQuick 2.11 -import QtQuick.Controls 2.11 +import QtQuick 2.9 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import MaterialIcons 2.2