diff --git a/meshroom/ui/img/checkerboard_light.png b/meshroom/ui/img/checkerboard_light.png new file mode 100644 index 00000000..cef70154 Binary files /dev/null and b/meshroom/ui/img/checkerboard_light.png differ diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index 37e930c3..380dd21e 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -46,8 +46,8 @@ FloatingPane { id: displayModeCB flat: true Layout.fillWidth: true - model: featuresViewer.displayModes - onActivated: featuresViewer.displayMode = currentIndex + model: root.featuresViewer.displayModes + onActivated: root.featuresViewer.displayMode = currentIndex } } } @@ -67,12 +67,12 @@ FloatingPane { implicitHeight: contentHeight implicitWidth: contentItem.childrenRect.width - model: featuresViewer !== null ? featuresViewer.model : 0 + model: root.featuresViewer !== null ? root.featuresViewer.model : 0 delegate: RowLayout { id: featureType - property var viewer: featuresViewer.itemAt(index) + property var viewer: root.featuresViewer.itemAt(index) spacing: 4 // Visibility toogle @@ -86,10 +86,10 @@ FloatingPane { ColorChart { implicitWidth: 12 implicitHeight: implicitWidth - colors: featuresViewer.colors + colors: root.featuresViewer.colors currentIndex: featureType.viewer.colorIndex - // offset FeaturesViewer color set when changing the color of one feature type - onColorPicked: featuresViewer.colorOffset = colorIndex - index + // offset featuresViewer color set when changing the color of one feature type + onColorPicked: root.featuresViewer.colorOffset = colorIndex - index } // Feature type name Label { diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 9c315f53..80e3e9e9 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -13,9 +13,9 @@ FocusScope { property var metadata property var viewIn3D - property Component floatViewerComp: Qt.createComponent("FloatImage.qml", imgContainer) + property Component floatViewerComp: Qt.createComponent("FloatImage.qml") readonly property bool floatViewerAvailable: floatViewerComp.status === Component.Ready - property bool useFloatImageViewer: false + property alias useFloatImageViewer: displayHDR.checked function clear() { @@ -31,148 +31,6 @@ FocusScope { } } - // functions - function fit() { - if(imgContainer.image.status != Image.Ready) - return; - imgContainer.scale = Math.min(imgContainer.width/imgContainer.image.width, imgContainer.height/imgContainer.image.height) - imgContainer.x = Math.max((imgContainer.width-imgContainer.width*imgContainer.scale)*0.5, 0) - imgContainer.y = Math.max((imgContainer.height-imgContainer.height*imgContainer.scale)*0.5, 0) - } - - function getImageFile(type) { - 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"); - } - return ""; - } - - // context menu - property Component contextMenu: Menu { - MenuItem { - text: "Fit" - onTriggered: fit() - } - MenuItem { - text: "Zoom 100%" - onTriggered: { - imgContainer.scale = 1 - imgContainer.x = Math.max((imgContainer.width-imgContainer.width*imgContainer.scale)*0.5, 0) - imgContainer.y = Math.max((imgContainer.height-imgContainer.height*imgContainer.scale)*0.5, 0) - } - } - } - - // Image - Item { - id: imgContainer - transformOrigin: Item.TopLeft - width: parent.width - height: parent.height - - property var image: qtImageViewerLoader.active ? qtImageViewerLoader.item : floatImageViewerLoader.item - - // qtAliceVision Image Viewer - Loader { - id: floatImageViewerLoader - active: root.useFloatImageViewer - visible: (floatImageViewerLoader.status === Loader.Ready) - anchors.centerIn: parent - - Component.onCompleted: { - // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource - // Note: It does not work to use previously created component, - // so we re-create it with setSource. - // floatViewerComp.createObject(floatImageViewerLoader, { - setSource("FloatImage.qml", { - 'source': Qt.binding(function() { return getImageFile(imageType.type); }), - 'gamma': Qt.binding(function() { return imageToolbar.gammaValue; }), - 'offset': Qt.binding(function() { return imageToolbar.offsetValue; }), - 'channelModeString': Qt.binding(function() { return imageToolbar.channelModeValue; }), - }) - } - } - - // Simple QML Image Viewer (using Qt or qtOIIO to load images) - Loader { - id: qtImageViewerLoader - active: (!root.useFloatImageViewer) || (floatImageViewerLoader.status === Loader.Error) - anchors.centerIn: parent - sourceComponent: Image { - id: qtImageViewer - asynchronous: true - smooth: false - fillMode: Image.PreserveAspectFit - autoTransform: true - onWidthChanged: if(status==Image.Ready) fit() - source: getImageFile(imageType.type) - onStatusChanged: { - // update cache source when image is loaded - if(status === Image.Ready) - qtImageViewerCache.source = source - } - - // Image cache of the last loaded image - // Only visible when the main one is loading, to keep an image - // displayed at all time and smoothen transitions - Image { - id: qtImageViewerCache - - anchors.fill: parent - asynchronous: true - smooth: parent.smooth - fillMode: parent.fillMode - autoTransform: parent.autoTransform - - visible: qtImageViewer.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: (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); }), - }) - } - } - } - - // Busy indicator - BusyIndicator { - anchors.centerIn: parent - // running property binding seems broken, only dynamic binding assignment works - Component.onCompleted: { - running = Qt.binding(function() { return imgContainer.image && imgContainer.image.status === Image.Loading }) - } - // disable the visibility when unused to avoid stealing the mouseEvent to the image color picker - visible: running - } - // mouse area MouseArea { anchors.fill: parent @@ -203,12 +61,44 @@ FocusScope { } } + // functions + function fit() { + if(imgContainer.image.status != Image.Ready) + return; + imgContainer.scale = Math.min(imgLayout.width / imgContainer.image.width, root.height / imgContainer.image.height) + imgContainer.x = Math.max((imgLayout.width - imgContainer.image.width * imgContainer.scale)*0.5, 0) + imgContainer.y = Math.max((imgLayout.height - imgContainer.image.height * imgContainer.scale)*0.5, 0) + // console.warn("fit: imgLayout.width: " + imgContainer.scale + ", imgContainer.image.width: " + imgContainer.image.width) + // console.warn("fit: imgContainer.scale: " + imgContainer.scale + ", x: " + imgContainer.x + ", y: " + imgContainer.y) + } + + function getImageFile(type) { + 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"); + } + return ""; + } + + // context menu + property Component contextMenu: Menu { + MenuItem { + text: "Fit" + onTriggered: fit() + } + MenuItem { + text: "Zoom 100%" + onTriggered: { + imgContainer.scale = 1 + imgContainer.x = Math.max((imgLayout.width-imgContainer.width*imgContainer.scale)*0.5, 0) + imgContainer.y = Math.max((imgLayout.height-imgContainer.height*imgContainer.scale)*0.5, 0) + } + } + } + ColumnLayout { - id: topToolbar - anchors.top: parent.top - anchors.margins: 0 - width: parent.width - spacing: 0 + anchors.fill: parent ImageToolbar { id: imageToolbar @@ -232,147 +122,300 @@ FocusScope { } } - FloatingPane { - id: imagePathToolbar - anchors.margins: 0 - radius: 0 - padding: 4 - visible: displayImagePathAction.checked + // Image + Item { + id: imgLayout Layout.fillWidth: true - - RowLayout { + Layout.fillHeight: true + clip: true + Image { + id: alphaBackground anchors.fill: parent - - // selectable filepath to source image - TextField { - padding: 0 - background: Item {} - horizontalAlignment: TextInput.AlignLeft - Layout.fillWidth: true - font.pointSize: 8 - readOnly: true - selectByMouse: true - text: Filepath.urlToString(getImageFile(imageType.type)) - } - - // show which depthmap node is active - Label { - id: depthMapNodeName - visible: (_reconstruction.depthMap != undefined) && (imageType.type != "image") - text: (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "") - font.pointSize: 8 - - horizontalAlignment: TextInput.AlignLeft - Layout.fillWidth: false - Layout.preferredWidth: contentWidth - } - } - } - } - - // Image Metadata overlay Pane - ImageMetadataView { - width: 350 - anchors { - top: topToolbar.bottom - right: parent.right - bottom: bottomToolbar.top - } - - visible: metadataCB.checked - // only load metadata model if visible - 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 - anchors.margins: 0 - width: parent.width - topPadding: 2 - bottomPadding: topPadding - - RowLayout { - anchors.fill: parent - - // zoom label - Label { - text: ((imgContainer.image && (imgContainer.image.status == Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" - state: "xsmall" - } - MaterialToolButton { - id: displayFeatures - font.pointSize: 11 - ToolTip.text: "Display Features" - checkable: true - text: MaterialIcons.scatter_plot + visible: displayAlphaBackground.checked + fillMode: Image.Tile + horizontalAlignment: Image.AlignLeft + verticalAlignment: Image.AlignTop + source: "../../img/checkerboard_light.png" + scale: 4 + smooth: false } Item { - Layout.fillWidth: true - Label { - id: resolutionLabel - text: imgContainer.image ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" + id: imgContainer + transformOrigin: Item.TopLeft + + // qtAliceVision Image Viewer + Loader { + id: floatImageViewerLoader + active: root.useFloatImageViewer + visible: (floatImageViewerLoader.status === Loader.Ready) anchors.centerIn: parent - elide: Text.ElideMiddle + + Component.onCompleted: { + // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource + // Note: It does not work to use previously created component, + // so we re-create it with setSource. + // floatViewerComp.createObject(floatImageViewerLoader, { + setSource("FloatImage.qml", { + 'source': Qt.binding(function() { return getImageFile(imageType.type); }), + 'gamma': Qt.binding(function() { return imageToolbar.gammaValue; }), + 'offset': Qt.binding(function() { return imageToolbar.offsetValue; }), + 'channelModeString': Qt.binding(function() { return imageToolbar.channelModeValue; }), + }) + } + } + + // Simple QML Image Viewer (using Qt or qtOIIO to load images) + Loader { + id: qtImageViewerLoader + active: (!root.useFloatImageViewer) || (floatImageViewerLoader.status === Loader.Error) + anchors.centerIn: parent + sourceComponent: Image { + id: qtImageViewer + asynchronous: true + smooth: false + fillMode: Image.PreserveAspectFit + autoTransform: true + onWidthChanged: if(status==Image.Ready) fit() + source: getImageFile(imageType.type) + onStatusChanged: { + // update cache source when image is loaded + if(status === Image.Ready) + qtImageViewerCache.source = source + } + + // Image cache of the last loaded image + // Only visible when the main one is loading, to keep an image + // displayed at all time and smoothen transitions + Image { + id: qtImageViewerCache + + anchors.fill: parent + asynchronous: true + smooth: parent.smooth + fillMode: parent.fillMode + autoTransform: parent.autoTransform + + visible: qtImageViewer.status === Image.Loading + } + } + } + + property var image: qtImageViewerLoader.active ? qtImageViewerLoader.item : floatImageViewerLoader.item + width: image ? image.width : 1 + height: image ? image.height : 1 + scale: 1.0 + + // 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: (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); }), + }) + } } } - ComboBox { - id: imageType - // set min size to 5 characters + one margin for the combobox - Layout.minimumWidth: 6.0 * Qt.application.font.pixelSize - Layout.preferredWidth: Layout.minimumWidth - flat: true - - property var types: ["image", "depth", "sim"] - property string type: types[currentIndex] + ColumnLayout { + anchors.fill: parent + spacing: 0 + FloatingPane { + id: imagePathToolbar + Layout.fillWidth: true + // Layout.fillHeight: true + Layout.preferredHeight: childrenRect.height + visible: displayImagePathAction.checked - model: types - enabled: _reconstruction.depthMap != undefined - } + RowLayout { + width: parent.width + height: childrenRect.height - MaterialToolButton { - font.pointSize: 11 - enabled: _reconstruction.depthMap != undefined - ToolTip.text: "View Depth Map in 3D (" + (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "No DepthMap Node Selected") + ")" - text: MaterialIcons.input + // selectable filepath to source image + TextField { + padding: 0 + background: Item {} + horizontalAlignment: TextInput.AlignLeft + Layout.fillWidth: true + height: contentHeight + font.pointSize: 8 + readOnly: true + selectByMouse: true + text: Filepath.urlToString(getImageFile(imageType.type)) + } - onClicked: { - root.viewIn3D(root.getImageFile("depth")) + // show which depthmap node is active + Label { + id: depthMapNodeName + visible: (_reconstruction.depthMap != undefined) && (imageType.type != "image") + text: (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "") + font.pointSize: 8 + + horizontalAlignment: TextInput.AlignLeft + Layout.fillWidth: false + Layout.preferredWidth: contentWidth + height: contentHeight + } + } } - } + Item { + id: imgPlaceholder + Layout.fillWidth: true + Layout.fillHeight: true - ToolButton { - id: metadataCB - padding: 3 + // Image Metadata overlay Pane + ImageMetadataView { + width: 350 + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + } - font.family: MaterialIcons.fontFamily - text: MaterialIcons.info_outline + visible: metadataCB.checked + // only load metadata model if visible + metadata: visible ? root.metadata : {} + } - ToolTip.text: "Image Metadata" - ToolTip.visible: hovered + Loader { + id: featuresOverlay + anchors { + bottom: parent.bottom + left: parent.left + margins: 2 + } + active: displayFeatures.checked - font.pointSize: 12 - smooth: false - flat: true - checkable: enabled + sourceComponent: FeaturesInfoOverlay { + featureExtractionNode: _reconstruction.featureExtraction + pluginStatus: featuresViewerLoader.status + featuresViewer: featuresViewerLoader.item + } + } + } + FloatingPane { + id: bottomToolbar + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + + RowLayout { + anchors.fill: parent + + // zoom label + Label { + text: ((imgContainer.image && (imgContainer.image.status == Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" + state: "xsmall" + } + MaterialToolButton { + id: displayAlphaBackground + font.pointSize: 11 + ToolTip.text: "Alpha Background" + checkable: true + text: MaterialIcons.texture + } + MaterialToolButton { + id: displayHDR + font.pointSize: 20 + padding: 0 + ToolTip.text: "HDR Floating Point Viewer" + text: MaterialIcons.hdr_on + checkable: true + checked: false + enabled: root.floatViewerAvailable + } + MaterialToolButton { + id: displayFeatures + font.pointSize: 11 + ToolTip.text: "Display Features" + checkable: true + text: MaterialIcons.scatter_plot + } + + Item { + Layout.fillWidth: true + Label { + id: resolutionLabel + text: imgContainer.image ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" + anchors.centerIn: parent + elide: Text.ElideMiddle + } + } + + ComboBox { + id: imageType + // set min size to 5 characters + one margin for the combobox + Layout.minimumWidth: 6.0 * Qt.application.font.pixelSize + Layout.preferredWidth: Layout.minimumWidth + flat: true + + property var types: ["image", "depth", "sim"] + property string type: types[currentIndex] + + model: types + enabled: _reconstruction.depthMap != undefined + } + + MaterialToolButton { + font.pointSize: 11 + enabled: _reconstruction.depthMap != undefined + ToolTip.text: "View Depth Map in 3D (" + (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "No DepthMap Node Selected") + ")" + text: MaterialIcons.input + + onClicked: { + root.viewIn3D(root.getImageFile("depth")) + } + } + + ToolButton { + id: metadataCB + padding: 3 + + font.family: MaterialIcons.fontFamily + text: MaterialIcons.info_outline + + ToolTip.text: "Image Metadata" + ToolTip.visible: hovered + + font.pointSize: 12 + smooth: false + flat: true + checkable: enabled + enabled: _reconstruction.selectedViewId >= 0 + } + } + } } } } + + // Busy indicator + BusyIndicator { + anchors.centerIn: parent + // running property binding seems broken, only dynamic binding assignment works + Component.onCompleted: { + running = Qt.binding(function() { return imgContainer.image && imgContainer.image.status === Image.Loading }) + } + // disable the visibility when unused to avoid stealing the mouseEvent to the image color picker + visible: running + } } diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 8eac11e9..1eca9e48 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -95,21 +95,12 @@ Item { id: imageViewerMenu y: parent.height x: -width + parent.width - Action { - id: useFloatImageViewerAction - text: "Use HDR Image Viewer" - checkable: true - checked: false - enabled: viewer2D.floatViewerAvailable - - // tooltip: "Floating Point Image Viewer allows to visualize image with the full color dynamic. It is useful for HDR or RAW visualization. (Requires a plugin)" - } Action { id: displayImageToolBarAction text: "Display HDR Toolbar" checkable: true checked: true - enabled: useFloatImageViewerAction.checked + enabled: viewer2D.useFloatImageViewer } Action { id: displayImagePathAction @@ -124,7 +115,6 @@ Item { Viewer2D { id: viewer2D anchors.fill: parent - useFloatImageViewer: useFloatImageViewerAction.checked viewIn3D: root.load3DMedia