diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index d62ebd3b..fb28140a 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -16,14 +16,15 @@ FloatingPane { property int pluginStatus: Loader.Null property Item featuresViewer: null property var mfeatures: null - property var featureExtractionNode: null + property var mtracks: null + property var msfmdata: null ColumnLayout { // Header RowLayout { - // FeatureExtraction node name + // Node used to read features Label { - text: featureExtractionNode ? featureExtractionNode.label : "" + text: _reconstruction ? _reconstruction.activeNodes.get("featureProvider").node.label : "" Layout.fillWidth: true } // Settings menu @@ -157,7 +158,7 @@ FloatingPane { Layout.alignment: Qt.AlignRight from: -1 to: 50 - value: root.mfeatures.timeWindow + value: root.featuresViewer.timeWindow stepSize: 1 editable: true @@ -174,7 +175,7 @@ FloatingPane { } onValueChanged: { - root.mfeatures.timeWindow = timeWindowSB.value; + root.featuresViewer.timeWindow = timeWindowSB.value; } } } @@ -224,7 +225,7 @@ FloatingPane { ToolTip.text: "Display Tracks" onClicked: { featureType.viewer.displayTracks = tracksVisibilityButton.checked; - root.mfeatures.enableTimeWindow = tracksVisibilityButton.checked; + root.featuresViewer.enableTimeWindow = tracksVisibilityButton.checked; } font.pointSize: 10 } @@ -263,18 +264,13 @@ FloatingPane { } // Feature type name Label { - text: { - if(featureType.viewer.loadingFeatures) - return featureType.viewer.describerType; - return featureType.viewer.describerType + ": " + - ((featureExtractionNode && featureExtractionNode.isComputed) ? root.mfeatures.featuresInfo[featureType.viewer.describerType][root.mfeatures.currentViewId]['nbFeatures'] : " - ") + " / " + - (root.mfeatures.haveValidTracks ? root.mfeatures.featuresInfo[featureType.viewer.describerType][root.mfeatures.currentViewId]['nbTracks'] : " - ") + " / " + - (root.mfeatures.haveValidLandmarks ? root.mfeatures.featuresInfo[featureType.viewer.describerType][root.mfeatures.currentViewId]['nbLandmarks'] : " - "); - } + property string descType: featureType.viewer.describerType + property int viewId: root.featuresViewer.currentViewId + text: descType + ": " + ((root.mfeatures && root.mfeatures.status === MFeatures.Ready) ? root.mfeatures.nbFeatures(descType, viewId) : " - ") + " / " + ((root.mtracks && root.mtracks.status === MTracks.Ready) ? root.mtracks.nbMatches(descType, viewId) : " - ") + " / " + ((root.msfmdata && root.msfmdata.status === MSfMData.Ready) ? root.msfmdata.nbLandmarks(descType, viewId) : " - ") } // Feature loading status Loader { - active: (root.mfeatures.status === MFeatures.Loading) + active: (root.mfeatures && root.mfeatures.status === MFeatures.Loading) sourceComponent: BusyIndicator { padding: 0 implicitWidth: 12 diff --git a/meshroom/ui/qml/Viewer/FeaturesViewer.qml b/meshroom/ui/qml/Viewer/FeaturesViewer.qml index 63e8becd..c33538bc 100644 --- a/meshroom/ui/qml/Viewer/FeaturesViewer.qml +++ b/meshroom/ui/qml/Viewer/FeaturesViewer.qml @@ -10,34 +10,53 @@ import Utils 1.0 Repeater { id: root - /// Features to display + /// Features property var features + /// Tracks + property var tracks + /// SfMData + property var sfmData /// The list of describer types to load property alias describerTypes: root.model + /// List of available feature display modes readonly property var featureDisplayModes: ['Points', 'Squares', 'Oriented Squares'] /// Current feature display mode index property int featureDisplayMode: 2 + /// List of available track display modes readonly property var trackDisplayModes: ['Lines Only', 'Current Matches', 'All Matches'] /// Current track display mode index property int trackDisplayMode: 1 + // Minimum feature scale score to display property real featureMinScaleFilter: 0 // Maximum feature scale score to display property real featureMaxScaleFilter: 1 + /// Display 3d tracks property bool display3dTracks: false + /// Display only contiguous tracks property bool trackContiguousFilter: true + /// Display only tracks with at least one inlier property bool trackInliersFilter: false + /// Display track endpoints property bool displayTrackEndpoints: true + /// The list of colors used for displaying several describers property var colors: [Colors.blue, Colors.green, Colors.yellow, Colors.cyan, Colors.pink, Colors.lime] //, Colors.orange, Colors.red + /// Current view ID + property var currentViewId + + /// Time window + property bool enableTimeWindow: false + property int timeWindow: 1 + model: root.describerTypes // instantiate one FeaturesViewer by describer type @@ -56,6 +75,11 @@ Repeater { matchColor: Colors.orange landmarkColor: Colors.red describerType: modelData + currentViewId: root.currentViewId + enableTimeWindow: root.enableTimeWindow + timeWindow: root.timeWindow mfeatures: root.features + mtracks: root.tracks + msfmData: root.sfmData } } diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 48835f91..4bd1cade 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -71,33 +71,23 @@ FocusScope { return ""; var res = ""; if(imgContainer.image.status === Image.Loading) - res += " Image"; - if(featuresViewerLoader.status === Loader.Ready && featuresViewerLoader.item) { - for (var i = 0; i < featuresViewerLoader.item.count; ++i) { - if(featuresViewerLoader.item.itemAt(i).loadingFeatures) - { - res += " Features"; - break; - } - } + res += " Image"; } if(mfeaturesLoader.status === Loader.Ready) { - if(mfeaturesLoader.item.status === MFeatures.Loading) + if(mfeaturesLoader.item && mfeaturesLoader.item.status === MFeatures.Loading) res += " Features"; } if(mtracksLoader.status === Loader.Ready) { - if(mtracksLoader.item.status === MTracks.Loading) + if(mtracksLoader.item && mtracksLoader.item.status === MTracks.Loading) res += " Tracks"; } if(msfmDataLoader.status === Loader.Ready) { - if(msfmDataLoader.item != null && msfmDataLoader.item.status === MSfMData.Loading) - { + if(msfmDataLoader.item && msfmDataLoader.item.status === MSfMData.Loading) res += " SfMData"; - } } return res; } @@ -536,7 +526,7 @@ FocusScope { ExifOrientedViewer { id: featuresViewerLoader active: displayFeatures.checked - property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("FeatureExtraction").node : null + property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("featureProvider").node : null width: imgContainer.width height: imgContainer.height anchors.centerIn: parent @@ -550,7 +540,10 @@ FocusScope { // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource setSource("FeaturesViewer.qml", { 'model': Qt.binding(function() { return activeNode ? activeNode.attribute("describerTypes").value : ""; }), + 'currentViewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), 'features': Qt.binding(function() { return mfeaturesLoader.status === Loader.Ready ? mfeaturesLoader.item : null; }), + 'tracks': Qt.binding(function() { return mtracksLoader.status === Loader.Ready ? mtracksLoader.item : null; }), + 'sfmData': Qt.binding(function() { return msfmDataLoader.status === Loader.Ready ? msfmDataLoader.item : null; }), }) } else { // Force the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14 @@ -718,30 +711,48 @@ FocusScope { id: mfeaturesLoader property bool isUsed: displayFeatures.checked - property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get("FeatureExtraction").node : null + property var activeNode: { + if (!root.aliceVisionPluginAvailable) { + return null; + } + return _reconstruction ? _reconstruction.activeNodes.get("featureProvider").node : null; + } property bool isComputed: activeNode && activeNode.isComputed - active: false - - onIsUsedChanged: { - active = (!active && isUsed && isComputed); - } - onIsComputedChanged: { - active = (!active && isUsed && isComputed); - } - onActiveNodeChanged: { - active = (!active && isUsed && isComputed); - } + active: isUsed && isComputed onActiveChanged: { if(active) { // instantiate and initialize a MFeatures component dynamically using Loader.setSource // so it can fail safely if the c++ plugin is not available setSource("MFeatures.qml", { - 'currentViewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), - 'describerTypes': Qt.binding(function() { return activeNode ? activeNode.attribute("describerTypes").value : {}; }), - 'featureFolder': Qt.binding(function() { return activeNode ? Filepath.stringToUrl(activeNode.attribute("output").value) : ""; }), - 'mtracks': Qt.binding(function() { return mtracksLoader.status === Loader.Ready ? mtracksLoader.item : null; }), - 'msfmData': Qt.binding(function() { return msfmDataLoader.status === Loader.Ready ? msfmDataLoader.item : null; }), + 'describerTypes': Qt.binding(function() { + return activeNode ? activeNode.attribute("describerTypes").value : {}; + }), + 'featureFolders': Qt.binding(function() { + let result = []; + if (activeNode) { + if (activeNode.nodeType == "FeatureExtraction" && isComputed) { + result.push(activeNode.attribute("output").value); + } else if (activeNode.hasAttribute("featuresFolders")) { + for (let i = 0; i < activeNode.attribute("featuresFolders").value.count; i++) { + let attr = activeNode.attribute("featuresFolders").value.at(i); + result.push(attr.value); + } + } + } + return result; + }), + 'viewIds': Qt.binding(function() { + if (_reconstruction) { + let result = []; + for (let i = 0; i < _reconstruction.viewpoints.count; i++) { + let vp = _reconstruction.viewpoints.at(i); + result.push(vp.childAttribute("viewId").value); + } + return result; + } + return {}; + }), }) } else { @@ -753,74 +764,36 @@ FocusScope { Loader { id: msfmDataLoader - property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked - || displayPanoramaViewer.checked || displayLensDistortionViewer.checked + property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked || displayPanoramaViewer.checked || displayLensDistortionViewer.checked property var activeNode: { - if(! root.aliceVisionPluginAvailable){ - return null + if(!root.aliceVisionPluginAvailable){ + return null; } - // For lens distortion viewer: use all nodes creating a sfmData file - var nodeType = displayLensDistortionViewer.checked ? 'sfmData' : 'sfm' - var sfmNode = _reconstruction ? _reconstruction.activeNodes.get(nodeType).node : null + var nodeType = "sfm"; + if (displayLensDistortionViewer.checked) { + nodeType = "sfmData"; + } + var sfmNode = _reconstruction ? _reconstruction.activeNodes.get(nodeType).node : null; if(sfmNode === null){ - return null + return null; } if(displayPanoramaViewer.checked){ - sfmNode = _reconstruction.activeNodes.get('SfMTransform').node - var previousNode = sfmNode.attribute("input").rootLinkParam.node - return previousNode - } - else{ - return sfmNode + sfmNode = _reconstruction.activeNodes.get('SfMTransform').node; + var previousNode = sfmNode.attribute("input").rootLinkParam.node; + return previousNode; } + return sfmNode; } property bool isComputed: activeNode && activeNode.isComputed property string filepath: { - var sfmValue = "" - if(!isComputed){ - return Filepath.stringToUrl(sfmValue) - } - else{ - if(activeNode.hasAttribute("output")){ - sfmValue = activeNode.attribute("output").value - } - return Filepath.stringToUrl(sfmValue) + var sfmValue = ""; + if (isComputed && activeNode.hasAttribute("output")) { + sfmValue = activeNode.attribute("output").value; } + return Filepath.stringToUrl(sfmValue); } - active: false - // It takes time to load tracks, so keep them looaded, if we may use it again. - // If we load another node, we can trash them (to eventually load the new node data). - onIsUsedChanged: { - if(!active && isUsed && isComputed) - { - active = true; - } - } - onIsComputedChanged: { - if(!isComputed) - { - active = false; - } - else if(!active && isUsed) - { - active = true; - } - } - onActiveNodeChanged: { - if(!isUsed) - { - active = false; - } - else if(!isComputed) - { - active = false; - } - else - { - active = true; - } - } + active: isUsed && isComputed onActiveChanged: { if(active) { @@ -839,43 +812,35 @@ FocusScope { id: mtracksLoader property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked || displayPanoramaViewer.checked - property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get('FeatureMatching').node : null + property var activeNode: { + if (!root.aliceVisionPluginAvailable) { + return null; + } + return _reconstruction ? _reconstruction.activeNodes.get("matchProvider").node : null; + } property bool isComputed: activeNode && activeNode.isComputed - active: false - // It takes time to load tracks, so keep them looaded, if we may use it again. - // If we load another node, we can trash them (to eventually load the new node data). - onIsUsedChanged: { - if(!active && isUsed && isComputed) { - active = true; - } - } - onIsComputedChanged: { - if(!isComputed) { - active = false; - } - else if(!active && isUsed) { - active = true; - } - } - onActiveNodeChanged: { - if(!isUsed) { - active = false; - } - else if(!isComputed) { - active = false; - } - else { - active = true; - } - } + active: isUsed && isComputed onActiveChanged: { if(active) { // instantiate and initialize a SfmStatsView component dynamically using Loader.setSource // so it can fail safely if the c++ plugin is not available setSource("MTracks.qml", { - 'matchingFolder': Qt.binding(function() { return Filepath.stringToUrl(isComputed ? activeNode.attribute("output").value : ""); }), + 'matchingFolders': Qt.binding(function() { + let result = []; + if (activeNode) { + if (activeNode.nodeType == "FeatureMatching" && isComputed) { + result.push(activeNode.attribute("output").value); + } else if (activeNode.hasAttribute("matchesFolders")) { + for (let i = 0; i < activeNode.attribute("matchesFolders").value.count; i++) { + let attr = activeNode.attribute("matchesFolders").value.at(i); + result.push(attr.value); + } + } + } + return result; + }), }) } else { // Force the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14 @@ -930,10 +895,11 @@ FocusScope { active: root.aliceVisionPluginAvailable && displayFeatures.checked && featuresViewerLoader.status === Loader.Ready sourceComponent: FeaturesInfoOverlay { - featureExtractionNode: _reconstruction.activeNodes.get('FeatureExtraction').node pluginStatus: featuresViewerLoader.status featuresViewer: featuresViewerLoader.item mfeatures: mfeaturesLoader.item + mtracks: mtracksLoader.item + msfmdata: msfmDataLoader.item } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index ca7b9d23..29fec43d 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -408,6 +408,10 @@ class Reconstruction(UIGraph): "SfMAlignment"], # All nodes generating depth map files "allDepthMap": ["DepthMap", "DepthMapFilter"], + # Nodes that can be used to provide features folders to the UI + "featureProvider": ["FeatureExtraction", "FeatureMatching", "StructureFromMotion"], + # Nodes that can be used to provide matches folders to the UI + "matchProvider": ["FeatureMatching", "StructureFromMotion"] } def __init__(self, undoStack, taskManager, defaultPipeline='', parent=None):