diff --git a/meshroom/ui/qml/Charts/InteractiveChartView.qml b/meshroom/ui/qml/Charts/InteractiveChartView.qml new file mode 100644 index 0000000000..eef47765f9 --- /dev/null +++ b/meshroom/ui/qml/Charts/InteractiveChartView.qml @@ -0,0 +1,57 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import QtPositioning 5.8 +import QtLocation 5.9 + +import QtCharts 2.13 + +import Controls 1.0 +import Utils 1.0 + + +ChartView { + id: root + antialiasing: true + + Rectangle { + id: plotZone + x: root.plotArea.x + y: root.plotArea.y + width: root.plotArea.width + height: root.plotArea.height + color: "transparent" + + MouseArea { + anchors.fill: parent + + property double degreeToScale: 1.0 / 120.0 // default mouse scroll is 15 degree + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + // onWheel: { + // console.warn("root.plotArea before: " + root.plotArea) + // var zoomFactor = wheel.angleDelta.y > 0 ? 1.0 / (1.0 + wheel.angleDelta.y * degreeToScale) : (1.0 + Math.abs(wheel.angleDelta.y) * degreeToScale) + + // // var mouse_screen = Qt.point(wheel.x, wheel.y) + // var mouse_screen = mapToItem(root, wheel.x, wheel.y) + // var mouse_normalized = Qt.point(mouse_screen.x / plotZone.width, mouse_screen.y / plotZone.height) + // var mouse_plot = Qt.point(mouse_normalized.x * plotZone.width, mouse_normalized.y * plotZone.height) + + // // var p = mapToValue(mouse_screen, root.series(0)) + // // var pMin = mapToValue(mouse_screen, Qt.point(root.axisX().min, root.axisY().min)) + // // var pMax = mapToValue(mouse_screen, Qt.point(root.axisX().max, root.axisY().max)) + // // console.warn("p: " + p) + + // // Qt.rect() + // var r = Qt.rect(mouse_plot.x, mouse_plot.y, plotZone.width * zoomFactor, plotZone.height * zoomFactor) + // //var r = Qt.rect(pMin.x, pMin.y, (pMax.x-pMin.x) / 2, (pMax.y-pMin.y) / 2) + // root.zoomIn(r) + // } + onClicked: { + root.zoomReset(); + } + } + } + + +} diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir index 32ea2d3271..0b50d1ed53 100644 --- a/meshroom/ui/qml/Charts/qmldir +++ b/meshroom/ui/qml/Charts/qmldir @@ -2,3 +2,4 @@ module Charts ChartViewLegend 1.0 ChartViewLegend.qml ChartViewCheckBox 1.0 ChartViewCheckBox.qml +InteractiveChartView 1.0 InteractiveChartView.qml diff --git a/meshroom/ui/qml/Controls/Panel.qml b/meshroom/ui/qml/Controls/Panel.qml index a340bdf318..6cadad9338 100644 --- a/meshroom/ui/qml/Controls/Panel.qml +++ b/meshroom/ui/qml/Controls/Panel.qml @@ -18,6 +18,8 @@ Page { property alias headerBar: headerLayout.data property alias footerContent: footerLayout.data property alias icon: iconPlaceHolder.data + property alias loading: loadingIndicator.running + property alias loadingText: loadingLabal.text clip: true @@ -46,18 +48,37 @@ Page { width: childrenRect.width height: childrenRect.height Layout.alignment: Qt.AlignVCenter - visible: icon != "" + visible: icon !== "" } // Title Label { text: root.title - Layout.fillWidth: true elide: Text.ElideRight topPadding: m.vPadding bottomPadding: m.vPadding } - // + Item { + width: 10 + } + // Feature loading status + BusyIndicator { + id: loadingIndicator + padding: 0 + implicitWidth: 12 + implicitHeight: 12 + running: false + } + Label { + id: loadingLabal + text: "" + font.italic: true + } + Item { + Layout.fillWidth: true + } + + // Header menu Row { id: headerLayout } } } diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 6ebf29c92b..4674e23a71 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -6,6 +6,7 @@ import Utils 1.0 import Charts 1.0 import MaterialIcons 2.2 + Item { id: root @@ -364,7 +365,7 @@ Item { } } - ChartView { + InteractiveChartView { id: cpuChart Layout.fillWidth: true @@ -419,7 +420,7 @@ Item { ColumnLayout { - ChartView { + InteractiveChartView { id: ramChart margins.top: 0 margins.bottom: 0 @@ -487,7 +488,7 @@ Item { ColumnLayout { - ChartView { + InteractiveChartView { id: gpuChart Layout.fillWidth: true diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index 380dd21ecf..52c1321072 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -18,12 +18,11 @@ FloatingPane { property var featureExtractionNode: null ColumnLayout { - // Header RowLayout { // FeatureExtraction node name Label { - text: featureExtractionNode.label + text: featureExtractionNode ? featureExtractionNode.label : "" Layout.fillWidth: true } // Settings menu @@ -47,6 +46,7 @@ FloatingPane { flat: true Layout.fillWidth: true model: root.featuresViewer.displayModes + currentIndex: root.featuresViewer.displayMode onActivated: root.featuresViewer.displayMode = currentIndex } } @@ -73,15 +73,49 @@ FloatingPane { id: featureType property var viewer: root.featuresViewer.itemAt(index) + spacing: 4 - // Visibility toogle + // Features visibility toogle MaterialToolButton { - text: featureType.viewer.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off - onClicked: featureType.viewer.visible = !featureType.viewer.visible + id: featuresVisibilityButton + checkable: true + checked: true + text: MaterialIcons.center_focus_strong + onClicked: { + console.warn("featuresVisibilityButton.checked: " + featuresVisibilityButton.checked) + featureType.viewer.displayfeatures = featuresVisibilityButton.checked; + } font.pointSize: 10 opacity: featureType.viewer.visible ? 1.0 : 0.6 } + + // Tracks visibility toogle + MaterialToolButton { + id: tracksVisibilityButton + checkable: true + checked: true + text: MaterialIcons.timeline + onClicked: { + console.warn("tracksVisibilityButton.checked: " + tracksVisibilityButton.checked) + featureType.viewer.displayTracks = tracksVisibilityButton.checked; + } + font.pointSize: 10 + } + + // Landmarks visibility toogle + MaterialToolButton { + id: landmarksVisibilityButton + checkable: true + checked: true + text: MaterialIcons.fiber_manual_record + onClicked: { + console.warn("landmarksVisibilityButton.checked: " + landmarksVisibilityButton.checked) + featureType.viewer.displayLandmarks = landmarksVisibilityButton.checked; + } + font.pointSize: 10 + } + // ColorChart picker ColorChart { implicitWidth: 12 @@ -89,15 +123,22 @@ FloatingPane { colors: root.featuresViewer.colors currentIndex: featureType.viewer.colorIndex // offset featuresViewer color set when changing the color of one feature type - onColorPicked: root.featuresViewer.colorOffset = colorIndex - index + onColorPicked: featureType.viewer.colorOffset = colorIndex - index } // Feature type name Label { - text: featureType.viewer.describerType + (featureType.viewer.loading ? "" : ": " + featureType.viewer.features.length) + text: { + if(featureType.viewer.loadingFeatures) + return featureType.viewer.describerType; + return featureType.viewer.describerType + ": " + + featureType.viewer.features.length + " / " + + (featureType.viewer.haveValidTracks ? featureType.viewer.nbTracks : " - ") + " / " + + (featureType.viewer.haveValidLandmarks ? featureType.viewer.nbLandmarks : " - "); + } } // Feature loading status Loader { - active: featureType.viewer.loading + active: featureType.viewer.loadingFeatures sourceComponent: BusyIndicator { padding: 0 implicitWidth: 12 @@ -105,6 +146,7 @@ FloatingPane { running: true } } + } } } diff --git a/meshroom/ui/qml/Viewer/FeaturesViewer.qml b/meshroom/ui/qml/Viewer/FeaturesViewer.qml index 6e21de14b2..83b796cc52 100644 --- a/meshroom/ui/qml/Viewer/FeaturesViewer.qml +++ b/meshroom/ui/qml/Viewer/FeaturesViewer.qml @@ -12,29 +12,35 @@ Repeater { /// ViewID to display the features of property int viewId + /// SfMData to display the data of SfM + property var sfmData /// Folder containing the features files - property string folder + property string featureFolder + /// Folder containing the matches files + property var tracks /// 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 + property int displayMode: 2 /// 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 + property var colors: [Colors.blue, Colors.green, Colors.yellow, Colors.orange, Colors.cyan, Colors.pink, Colors.lime] //, Colors.red model: root.describerTypes // instantiate one FeaturesViewer by describer type delegate: AliceVision.FeaturesViewer { - readonly property int colorIndex: (index+root.colorOffset)%root.colors.length + readonly property int colorIndex: (index + colorOffset) % root.colors.length + property int colorOffset: 0 describerType: modelData - folder: root.folder + featureFolder: root.featureFolder + mtracks: root.tracks viewId: root.viewId color: root.colors[colorIndex] + landmarkColor: Colors.red displayMode: root.displayMode + msfmData: root.sfmData } } diff --git a/meshroom/ui/qml/Viewer/ImageMetadataView.qml b/meshroom/ui/qml/Viewer/ImageMetadataView.qml index 412bf3d98a..35fbb3b03b 100644 --- a/meshroom/ui/qml/Viewer/ImageMetadataView.qml +++ b/meshroom/ui/qml/Viewer/ImageMetadataView.qml @@ -19,6 +19,7 @@ FloatingPane { clip: true padding: 4 + anchors.rightMargin: 0 /** * Convert GPS metadata to degree coordinates. diff --git a/meshroom/ui/qml/Viewer/MSfMData.qml b/meshroom/ui/qml/Viewer/MSfMData.qml new file mode 100644 index 0000000000..eb880a31fc --- /dev/null +++ b/meshroom/ui/qml/Viewer/MSfMData.qml @@ -0,0 +1,7 @@ +import QtQuick 2.11 +import AliceVision 1.0 as AliceVision + +// Data from the SfM +AliceVision.MSfMData { + id: root +} diff --git a/meshroom/ui/qml/Viewer/MTracks.qml b/meshroom/ui/qml/Viewer/MTracks.qml new file mode 100644 index 0000000000..9b5afd081d --- /dev/null +++ b/meshroom/ui/qml/Viewer/MTracks.qml @@ -0,0 +1,6 @@ +import QtQuick 2.11 +import AliceVision 1.0 as AliceVision + +AliceVision.MTracks { + id: root +} diff --git a/meshroom/ui/qml/Viewer/SfmGlobalStats.qml b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml new file mode 100644 index 0000000000..81f8aea843 --- /dev/null +++ b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml @@ -0,0 +1,327 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import QtPositioning 5.8 +import QtLocation 5.9 +import QtCharts 2.13 +import Charts 1.0 + +import Controls 1.0 +import Utils 1.0 + +import AliceVision 1.0 as AliceVision + + +FloatingPane { + id: root + + property var msfmData + property var mTracks + property color textColor: Colors.sysPalette.text + + visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed()) ? root.visible : false + clip: true + padding: 4 + + // To avoid interaction with components in background + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: {} + onReleased: {} + onWheel: {} + } + + + InteractiveChartView { + id: residualsPerViewChart + width: parent.width * 0.5 + height: parent.height * 0.5 + + title: "Residuals Per View" + legend.visible: false + antialiasing: true + + ValueAxis { + id: residualsPerViewValueAxisX + labelFormat: "%i" + titleText: "Ordered Views" + min: 0 + max: sfmDataStat.residualsPerViewMaxAxisX + } + ValueAxis { + id: residualsPerViewValueAxisY + titleText: "Reprojection Error (pix)" + min: 0 + max: sfmDataStat.residualsPerViewMaxAxisY + tickAnchor: 0 + tickInterval: 0.50 + tickCount: sfmDataStat.residualsPerViewMaxAxisY * 2 + } + LineSeries { + id: residualsMinPerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Min" + } + LineSeries { + id: residualsMaxPerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Max" + } + LineSeries { + id: residualsMeanPerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Mean" + } + LineSeries { + id: residualsMedianPerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Median" + } + LineSeries { + id: residualsFirstQuartilePerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Q1" + } + LineSeries { + id: residualsThirdQuartilePerViewLineSerie + axisX: residualsPerViewValueAxisX + axisY: residualsPerViewValueAxisY + name: "Q3" + } + } + + Item { + id: residualsPerViewBtnContainer + + Layout.fillWidth: true + anchors.bottom: residualsPerViewChart.bottom + anchors.bottomMargin: 35 + anchors.left: residualsPerViewChart.left + anchors.leftMargin: residualsPerViewChart.width * 0.25 + + RowLayout { + + ChartViewCheckBox { + id: allObservations + text: "ALL" + color: textColor + checkState: residualsPerViewLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < residualsPerViewChart.count; ++i) + { + residualsPerViewChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: residualsPerViewLegend + chartView: residualsPerViewChart + } + + } + } + + InteractiveChartView { + id: observationsLengthsPerViewChart + width: parent.width * 0.5 + height: parent.height * 0.5 + anchors.top: parent.top + anchors.topMargin: (parent.height) * 0.5 + + title: "Observations Lengths Per View" + legend.visible: false + antialiasing: true + + ValueAxis { + id: observationsLengthsPerViewValueAxisX + labelFormat: "%i" + titleText: "Ordered Views" + min: 0 + max: sfmDataStat.observationsLengthsPerViewMaxAxisX + } + ValueAxis { + id: observationsLengthsPerViewValueAxisY + titleText: "Observations Lengths" + min: 0 + max: sfmDataStat.observationsLengthsPerViewMaxAxisY + tickAnchor: 0 + tickInterval: 0.50 + tickCount: sfmDataStat.observationsLengthsPerViewMaxAxisY * 2 + } + + LineSeries { + id: observationsLengthsMinPerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Min" + } + LineSeries { + id: observationsLengthsMaxPerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Max" + } + LineSeries { + id: observationsLengthsMeanPerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Mean" + } + LineSeries { + id: observationsLengthsMedianPerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Median" + } + LineSeries { + id: observationsLengthsFirstQuartilePerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Q1" + } + LineSeries { + id: observationsLengthsThirdQuartilePerViewLineSerie + axisX: observationsLengthsPerViewValueAxisX + axisY: observationsLengthsPerViewValueAxisY + name: "Q3" + } + } + + Item { + id: observationsLengthsPerViewBtnContainer + + Layout.fillWidth: true + anchors.bottom: observationsLengthsPerViewChart.bottom + anchors.bottomMargin: 35 + anchors.left: observationsLengthsPerViewChart.left + anchors.leftMargin: observationsLengthsPerViewChart.width * 0.25 + + RowLayout { + + ChartViewCheckBox { + id: allModes + text: "ALL" + color: textColor + checkState: observationsLengthsPerViewLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < observationsLengthsPerViewChart.count; ++i) + { + observationsLengthsPerViewChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: observationsLengthsPerViewLegend + chartView: observationsLengthsPerViewChart + } + + } + } + + InteractiveChartView { + id: landmarksPerViewChart + width: parent.width * 0.5 + height: parent.height * 0.5 + anchors.left: parent.left + anchors.leftMargin: (parent.width) * 0.5 + anchors.top: parent.top + + title: "Landmarks Per View" + legend.visible: false + antialiasing: true + + ValueAxis { + id: landmarksPerViewValueAxisX + titleText: "Ordered Views" + min: 0.0 + max: sfmDataStat.landmarksPerViewMaxAxisX + } + ValueAxis { + id: landmarksPerViewValueAxisY + labelFormat: "%i" + titleText: "Number of Landmarks" + min: 0 + max: sfmDataStat.landmarksPerViewMaxAxisY + } + LineSeries { + id: landmarksPerViewLineSerie + axisX: landmarksPerViewValueAxisX + axisY: landmarksPerViewValueAxisY + name: "Landmarks" + } + LineSeries { + id: tracksPerViewLineSerie + axisX: landmarksPerViewValueAxisX + axisY: landmarksPerViewValueAxisY + name: "Tracks" + } + } + + Item { + id: landmarksFeatTracksPerViewBtnContainer + + Layout.fillWidth: true + anchors.bottom: landmarksPerViewChart.bottom + anchors.bottomMargin: 35 + anchors.left: landmarksPerViewChart.left + anchors.leftMargin: landmarksPerViewChart.width * 0.25 + + RowLayout { + + ChartViewCheckBox { + id: allFeatures + text: "ALL" + color: textColor + checkState: landmarksFeatTracksPerViewLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < landmarksPerViewChart.count; ++i) + { + landmarksPerViewChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: landmarksFeatTracksPerViewLegend + chartView: landmarksPerViewChart + } + + } + } + + // Stats from the sfmData + AliceVision.MSfMDataStats { + id: sfmDataStat + msfmData: root.msfmData + mTracks: root.mTracks + + onAxisChanged: { + fillLandmarksPerViewSerie(landmarksPerViewLineSerie); + fillTracksPerViewSerie(tracksPerViewLineSerie); + fillResidualsMinPerViewSerie(residualsMinPerViewLineSerie); + fillResidualsMaxPerViewSerie(residualsMaxPerViewLineSerie); + fillResidualsMeanPerViewSerie(residualsMeanPerViewLineSerie); + fillResidualsMedianPerViewSerie(residualsMedianPerViewLineSerie); + fillResidualsFirstQuartilePerViewSerie(residualsFirstQuartilePerViewLineSerie); + fillResidualsThirdQuartilePerViewSerie(residualsThirdQuartilePerViewLineSerie); + fillObservationsLengthsMinPerViewSerie(observationsLengthsMinPerViewLineSerie); + fillObservationsLengthsMaxPerViewSerie(observationsLengthsMaxPerViewLineSerie); + fillObservationsLengthsMeanPerViewSerie(observationsLengthsMeanPerViewLineSerie); + fillObservationsLengthsMedianPerViewSerie(observationsLengthsMedianPerViewLineSerie); + fillObservationsLengthsFirstQuartilePerViewSerie(observationsLengthsFirstQuartilePerViewLineSerie); + fillObservationsLengthsThirdQuartilePerViewSerie(observationsLengthsThirdQuartilePerViewLineSerie); + } + } +} diff --git a/meshroom/ui/qml/Viewer/SfmStatsView.qml b/meshroom/ui/qml/Viewer/SfmStatsView.qml new file mode 100644 index 0000000000..974078e3b0 --- /dev/null +++ b/meshroom/ui/qml/Viewer/SfmStatsView.qml @@ -0,0 +1,263 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import QtPositioning 5.8 +import QtLocation 5.9 +import QtCharts 2.13 +import Charts 1.0 + +import Controls 1.0 +import Utils 1.0 + +import AliceVision 1.0 as AliceVision + + + +FloatingPane { + id: root + + property var msfmData: null + property int viewId + property color textColor: Colors.sysPalette.text + + visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed()) ? root.visible : false + clip: true + padding: 4 + + // To avoid interaction with components in background + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: {} + onReleased: {} + onWheel: {} + } + + InteractiveChartView { + id: residualChart + width: parent.width * 0.5 + height: parent.height * 0.5 + + title: "Reprojection Errors" + legend.visible: false + antialiasing: true + + ValueAxis { + id: residualValueAxisX + titleText: "Reprojection Error" + min: 0.0 + max: viewStat.residualMaxAxisX + } + ValueAxis { + id: residualValueAxisY + labelFormat: "%i" + titleText: "Number of Points" + min: 0 + max: viewStat.residualMaxAxisY + } + LineSeries { + id: residualFullLineSerie + axisX: residualValueAxisX + axisY: residualValueAxisY + name: "Average on All Cameras" + } + LineSeries { + id: residualViewLineSerie + axisX: residualValueAxisX + axisY: residualValueAxisY + name: "Current" + } + } + + Item { + id: residualBtnContainer + + Layout.fillWidth: true + anchors.bottom: residualChart.bottom + anchors.bottomMargin: 35 + anchors.left: residualChart.left + anchors.leftMargin: residualChart.width * 0.15 + + RowLayout { + + ChartViewCheckBox { + id: allResiduals + text: "ALL" + color: textColor + checkState: residualLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < residualChart.count; ++i) + { + residualChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: residualLegend + chartView: residualChart + } + } + } + + InteractiveChartView { + id: observationsLengthsChart + width: parent.width * 0.5 + height: parent.height * 0.5 + anchors.top: parent.top + anchors.topMargin: (parent.height) * 0.5 + + legend.visible: false + title: "Observations Lengths" + + ValueAxis { + id: observationsLengthsvalueAxisX + labelFormat: "%i" + titleText: "Observations Length" + min: 2 + max: viewStat.observationsLengthsMaxAxisX + tickAnchor: 2 + tickInterval: 1 + tickCount: 5 + } + ValueAxis { + id: observationsLengthsvalueAxisY + labelFormat: "%i" + titleText: "Number of Points" + min: 0 + max: viewStat.observationsLengthsMaxAxisY + } + LineSeries { + id: observationsLengthsFullLineSerie + axisX: observationsLengthsvalueAxisX + axisY: observationsLengthsvalueAxisY + name: "All Cameras" + } + LineSeries { + id: observationsLengthsViewLineSerie + axisX: observationsLengthsvalueAxisX + axisY: observationsLengthsvalueAxisY + name: "Current" + } + + } + + Item { + id: observationsLengthsBtnContainer + + Layout.fillWidth: true + anchors.bottom: observationsLengthsChart.bottom + anchors.bottomMargin: 35 + anchors.left: observationsLengthsChart.left + anchors.leftMargin: observationsLengthsChart.width * 0.25 + + RowLayout { + + ChartViewCheckBox { + id: allObservations + text: "ALL" + color: textColor + checkState: observationsLengthsLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < observationsLengthsChart.count; ++i) + { + observationsLengthsChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: observationsLengthsLegend + chartView: observationsLengthsChart + } + + } + } + + InteractiveChartView { + id: observationsScaleChart + width: parent.width * 0.5 + height: parent.height * 0.5 + anchors.left: parent.left + anchors.leftMargin: (parent.width) * 0.5 + anchors.top: parent.top + + legend.visible: false + title: "Observations Scale" + + ValueAxis { + id: observationsScaleValueAxisX + titleText: "Scale" + min: 0 + max: viewStat.observationsScaleMaxAxisX + } + ValueAxis { + id: observationsScaleValueAxisY + titleText: "Number of Points" + min: 0 + max: viewStat.observationsScaleMaxAxisY + } + LineSeries { + id: observationsScaleFullLineSerie + axisX: observationsScaleValueAxisX + axisY: observationsScaleValueAxisY + name: " Average on All Cameras" + } + LineSeries { + id: observationsScaleViewLineSerie + axisX: observationsScaleValueAxisX + axisY: observationsScaleValueAxisY + name: "Current" + } + } + + Item { + id: observationsScaleBtnContainer + + Layout.fillWidth: true + anchors.bottom: observationsScaleChart.bottom + anchors.bottomMargin: 35 + anchors.left: observationsScaleChart.left + anchors.leftMargin: observationsScaleChart.width * 0.15 + + RowLayout { + + ChartViewCheckBox { + id: allObservationsScales + text: "ALL" + color: textColor + checkState: observationsScaleLegend.buttonGroup.checkState + onClicked: { + var _checked = checked; + for(var i = 0; i < observationsScaleChart.count; ++i) + { + observationsScaleChart.series(i).visible = _checked; + } + } + } + + ChartViewLegend { + id: observationsScaleLegend + chartView: observationsScaleChart + } + } + } + + // Stats from a view the sfmData + AliceVision.MViewStats { + id: viewStat + msfmData: (root.visible && root.msfmData && root.msfmData.status === AliceVision.MSfMData.Ready) ? root.msfmData : null + viewId: root.viewId + onViewStatsChanged: { + fillResidualFullSerie(residualFullLineSerie); + fillResidualViewSerie(residualViewLineSerie); + fillObservationsLengthsFullSerie(observationsLengthsFullLineSerie); + fillObservationsLengthsViewSerie(observationsLengthsViewLineSerie); + fillObservationsScaleFullSerie(observationsScaleFullLineSerie); + fillObservationsScaleViewSerie(observationsScaleViewLineSerie); + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 1140a07fe4..919c732bc6 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -17,6 +17,35 @@ FocusScope { readonly property bool floatViewerAvailable: floatViewerComp.status === Component.Ready property alias useFloatImageViewer: displayHDR.checked + property string loadingModules: { + var res = "" + if(imgContainer.image.status === Image.Loading) + res += " Image"; + if(featuresViewerLoader.status === Loader.Ready) + { + for (var i = 0; i < featuresViewerLoader.item.count; ++i) { + if(featuresViewerLoader.item.itemAt(i).loadingFeatures) + { + res += " Features"; + break; + } + } + } + if(msfmDataLoader.status === Loader.Ready) + { + if(msfmDataLoader.item.status === MSfMData.Loading) + { + res += " SfMData"; + } + } + if(mtracksLoader.status === Loader.Ready) + { + if(mtracksLoader.item.status === MTracks.Loading) + res += " Tracks"; + } + return res; + } + function clear() { source = '' @@ -225,14 +254,20 @@ FocusScope { x: (imgContainer.image && rotation === 90) ? imgContainer.image.paintedWidth : 0 y: (imgContainer.image && rotation === -90) ? imgContainer.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); }), - }) + onActiveChanged: { + if(active) { + // 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) : ""; }), + '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 + setSource("", {}) + } } } } @@ -297,6 +332,113 @@ FocusScope { metadata: visible ? root.metadata : {} } + Loader { + id: msfmDataLoader + // active: _reconstruction.sfm && _reconstruction.sfm.isComputed() + + property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked + property var activeNode: _reconstruction.sfm + 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; + if(!active && isUsed) + active = true; + } + onActiveNodeChanged: { + if(!isUsed) + active = false; + else if(!isComputed) + active = false; + else + active = true; + } + + Component.onCompleted: { + // instantiate and initialize a SfmStatsView component dynamically using Loader.setSource + // so it can fail safely if the c++ plugin is not available + setSource("MSfMData.qml", { + 'sfmDataPath': Qt.binding(function() { return Filepath.stringToUrl(isComputed ? activeNode.attribute("output").value : ""); }), + }) + } + } + Loader { + id: mtracksLoader + // active: _reconstruction.featureMatching + + property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked + property var activeNode: _reconstruction.featureMatching + 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; + if(!active && isUsed) + active = true; + } + onActiveNodeChanged: { + console.warn("mtracksLoader.onActiveNodeChanged, activeNode: " + activeNode) + if(!isUsed) + active = false; + else if(!isComputed) + active = false; + else + active = true; + } + + Component.onCompleted: { + // 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 : ""); }), + }) + } + } + Loader { + id: sfmStatsView + anchors.fill: parent + active: msfmDataLoader.status === Loader.Ready && displaySfmStatsView.checked + + Component.onCompleted: { + // instantiate and initialize a SfmStatsView component dynamically using Loader.setSource + // so it can fail safely if the c++ plugin is not available + setSource("SfmStatsView.qml", { + 'msfmData': Qt.binding(function() { return msfmDataLoader.item; }), + 'viewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), + }) + } + } + Loader { + id: sfmGlobalStats + anchors.fill: parent + active: msfmDataLoader.status === Loader.Ready && displaySfmDataGlobalStats.checked + + Component.onCompleted: { + // instantiate and initialize a SfmStatsView component dynamically using Loader.setSource + // so it can fail safely if the c++ plugin is not available + setSource("SfmGlobalStats.qml", { + 'msfmData': Qt.binding(function() { return msfmDataLoader.item; }), + 'mTracks': Qt.binding(function() { return mtracksLoader.item; }), + + }) + } + } + Loader { id: featuresOverlay anchors { @@ -324,7 +466,7 @@ FocusScope { // zoom label Label { - text: ((imgContainer.image && (imgContainer.image.status == Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" + text: ((imgContainer.image && (imgContainer.image.status === Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" state: "xsmall" MouseArea { anchors.fill: parent @@ -373,7 +515,6 @@ FocusScope { checkable: true checked: false } - Label { id: resolutionLabel Layout.fillWidth: true @@ -414,6 +555,53 @@ FocusScope { } } + MaterialToolButton { + id: displaySfmStatsView + + font.family: MaterialIcons.fontFamily + text: MaterialIcons.assessment + + ToolTip.text: "StructureFromMotion Statistics" + ToolTip.visible: hovered + + font.pointSize: 14 + padding: 2 + smooth: false + flat: true + checkable: enabled + enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed() && _reconstruction.selectedViewId >= 0 + onCheckedChanged: { + if(checked == true) + { + displaySfmDataGlobalStats.checked = false + metadataCB.checked = false + } + } + } + + MaterialToolButton { + id: displaySfmDataGlobalStats + + font.family: MaterialIcons.fontFamily + text: MaterialIcons.language + + ToolTip.text: "StructureFromMotion Global Statistics" + ToolTip.visible: hovered + + font.pointSize: 14 + padding: 2 + smooth: false + flat: true + checkable: enabled + enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed() + onCheckedChanged: { + if(checked == true) + { + displaySfmStatsView.checked = false + metadataCB.checked = false + } + } + } MaterialToolButton { id: metadataCB @@ -429,7 +617,15 @@ FocusScope { flat: true checkable: enabled enabled: _reconstruction.selectedViewId >= 0 + onCheckedChanged: { + if(checked == true) + { + displaySfmDataGlobalStats.checked = false + displaySfmStatsView.checked = false + } + } } + } } } diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index fe88f54666..3a5e80fb18 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -82,6 +82,8 @@ Item { Layout.fillHeight: true Layout.fillWidth: true Layout.minimumWidth: 50 + loading: viewer2D.loadingModules.length > 0 + loadingText: loading ? "Loading " + viewer2D.loadingModules : "" headerBar: RowLayout { MaterialToolButton { diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index cb78edc756..80fa3b9fbe 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -380,6 +380,10 @@ def __init__(self, defaultPipeline='', parent=None): 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 @@ -455,6 +459,7 @@ def onGraphChanged(self): self._liveSfmManager.reset() self.selectedViewId = "-1" self.featureExtraction = None + self.featureMatching = None self.sfm = None self.prepareDenseScene = None self.depthMap = None @@ -505,6 +510,10 @@ 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 @@ -830,6 +839,8 @@ def setActiveNodeOfType(self, node): self.sfm = node elif node.nodeType == "FeatureExtraction": self.featureExtraction = node + elif node.nodeType == "FeatureMatching": + self.featureMatching = node elif node.nodeType == "CameraInit": self.cameraInit = node elif node.nodeType == "PrepareDenseScene": @@ -992,6 +1003,9 @@ def getPoseRT(self, viewpoint): 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)