Meshroom/meshroom/ui/qml/Viewer/Viewer2D.qml
Candice Bentéjac cfb44d8b54 [ui] Add currentViewPath property
Add a property that contains the path of the image that is currently
displayed in the viewer. If no image is displayed, the property is set
with an empty string.

The path information is set directly from the Viewer2D when an image is
loaded to be displayed.
2024-06-27 17:28:25 +02:00

1571 lines
75 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.11
import MaterialIcons 2.2
import Controls 1.0
import Utils 1.0
FocusScope {
id: root
clip: true
property var displayedNode: null
property var displayedAttr: (displayedNode && outputAttribute.name != "gallery") ? displayedNode.attributes.get(outputAttribute.name) : null
property var displayedAttrValue: displayedAttr ? displayedAttr.value : ""
property bool useExternal: false
property url sourceExternal
property url source
property var viewIn3D
property Component floatViewerComp: Qt.createComponent("FloatImage.qml")
property Component panoramaViewerComp: Qt.createComponent("PanoramaViewer.qml")
property var useFloatImageViewer: displayHDR.checked
property alias useLensDistortionViewer: displayLensDistortionViewer.checked
property alias usePanoramaViewer: displayPanoramaViewer.checked
property var activeNodeFisheye: _reconstruction ? _reconstruction.activeNodes.get("PanoramaInit").node : null
property bool cropFisheye : activeNodeFisheye ? activeNodeFisheye.attribute("useFisheye").value : false
property bool enable8bitViewer: enable8bitViewerAction.checked
property bool enableSequencePlayer: enableSequencePlayerAction.checked
readonly property alias sync3DSelected: sequencePlayer.sync3DSelected
property var sequence: []
property alias currentFrame: sequencePlayer.frameId
property alias frameRange: sequencePlayer.frameRange
QtObject {
id: m
property variant viewpointMetadata: {
// Metadata from viewpoint attribute
// Read from the reconstruction object
if (_reconstruction) {
let vp = getViewpoint(_reconstruction.selectedViewId)
if (vp) {
return JSON.parse(vp.childAttribute("metadata").value)
}
}
return {}
}
property variant imgMetadata: {
// Metadata from FloatImage viewer
// Directly read from the image file on disk
if (floatImageViewerLoader.active) {
return floatImageViewerLoader.item.metadata
}
// Use viewpoint metadata for the special case of the 8-bit viewer
if (qtImageViewerLoader.active) {
return viewpointMetadata
}
return {}
}
}
Loader {
id: aliceVisionPluginLoader
active: true
source: "TestAliceVisionPlugin.qml"
}
readonly property bool aliceVisionPluginAvailable: aliceVisionPluginLoader.status === Component.Ready
Component.onCompleted: {
if (!aliceVisionPluginAvailable) {
console.warn("Missing plugin qtAliceVision.")
displayHDR.checked = false
}
}
property string loadingModules: {
if (!imgContainer.image)
return ""
var res = ""
if (imgContainer.image.imageStatus === Image.Loading) {
res += " Image"
}
if (mfeaturesLoader.status === Loader.Ready) {
if (mfeaturesLoader.item && mfeaturesLoader.item.status === MFeatures.Loading)
res += " Features"
}
if (mtracksLoader.status === Loader.Ready) {
if (mtracksLoader.item && mtracksLoader.item.status === MTracks.Loading)
res += " Tracks"
}
if (msfmDataLoader.status === Loader.Ready) {
if (msfmDataLoader.item && msfmDataLoader.item.status === MSfMData.Loading)
res += " SfMData"
}
return res
}
function clear() {
source = ''
}
// slots
Keys.onPressed: {
if (event.key === Qt.Key_F) {
root.fit()
event.accepted = true
}
}
// mouse area
MouseArea {
anchors.fill: parent
property double factor: 1.2
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: {
imgContainer.forceActiveFocus()
if (mouse.button & Qt.MiddleButton || (mouse.button & Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier))
drag.target = imgContainer // start drag
}
onReleased: {
drag.target = undefined // stop drag
if (mouse.button & Qt.RightButton) {
var menu = contextMenu.createObject(root)
menu.x = mouse.x
menu.y = mouse.y
menu.open()
}
}
onWheel: {
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1 / factor
if (Math.min(imgContainer.width, imgContainer.image.height) * imgContainer.scale * zoomFactor < 10)
return
var point = mapToItem(imgContainer, wheel.x, wheel.y)
imgContainer.x += (1-zoomFactor) * point.x * imgContainer.scale
imgContainer.y += (1-zoomFactor) * point.y * imgContainer.scale
imgContainer.scale *= zoomFactor
}
}
onEnable8bitViewerChanged: {
if (!enable8bitViewer) {
displayHDR.checked = true
}
}
// functions
function fit() {
// make sure the image is ready for use
if (!imgContainer.image)
return
// for Exif orientation tags 5 to 8, a 90 degrees rotation is applied
// therefore image dimensions must be inverted
let dimensionsInverted = ["5", "6", "7", "8"].includes(imgContainer.orientationTag)
let orientedWidth = dimensionsInverted ? imgContainer.image.height : imgContainer.image.width
let orientedHeight = dimensionsInverted ? imgContainer.image.width : imgContainer.image.height
// fit oriented image
imgContainer.scale = Math.min(imgLayout.width / orientedWidth, root.height / orientedHeight)
imgContainer.x = Math.max((imgLayout.width - orientedWidth * imgContainer.scale) * 0.5, 0)
imgContainer.y = Math.max((imgLayout.height - orientedHeight * imgContainer.scale) * 0.5, 0)
// correct position when image dimensions are inverted
// so that container center corresponds to image center
imgContainer.x += (orientedWidth - imgContainer.image.width) * 0.5 * imgContainer.scale
imgContainer.y += (orientedHeight - imgContainer.image.height) * 0.5 * imgContainer.scale
}
function tryLoadNode(node) {
useExternal = false
// safety check
if (!node) {
return false
}
// node must be computed or at least running
if (node.isComputable && !node.isPartiallyFinished()) {
return false
}
// node must have at least one output attribute with the image semantic
if (!node.hasImageOutput && !node.hasSequenceOutput) {
return false
}
displayedNode = node
return true
}
function loadExternal(path) {
useExternal = true
sourceExternal = path
displayedNode = null
}
function getViewpoint(viewId) {
// Get viewpoint from cameraInit with matching id
// This requires to loop over all viewpoints
for (var i = 0; i < _reconstruction.viewpoints.count; i++) {
var vp = _reconstruction.viewpoints.at(i)
if (vp.childAttribute("viewId").value == viewId) {
return vp
}
}
return undefined
}
function getImageFile() {
if (useExternal) {
// Entry point for getting the image file from an external URL
return sourceExternal
}
if (_reconstruction && (!displayedNode || outputAttribute.name == "gallery")) {
// Entry point for getting the image file from the gallery
let vp = getViewpoint(_reconstruction.pickedViewId)
let path = vp ? vp.childAttribute("path").value : ""
_reconstruction.currentViewPath = path
return Filepath.stringToUrl(path)
}
if (_reconstruction && displayedNode && displayedNode.hasSequenceOutput && displayedAttr && (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) {
// Entry point for getting the image file from a sequence defined by an output attribute
var path = sequence[currentFrame-frameRange.min]
_reconstruction.currentViewPath = path
return Filepath.stringToUrl(path)
}
if (_reconstruction) {
// Entry point for getting the image file from an output attribute and associated to the current viewpoint
let vp = getViewpoint(_reconstruction.pickedViewId)
let path = displayedAttr ? displayedAttr.value : ""
let resolved = vp ? Filepath.resolve(path, vp) : path
_reconstruction.currentViewPath = resolved
return Filepath.stringToUrl(resolved)
}
return undefined
}
function buildOrderedSequence(path_template) {
// Resolve the path template on the sequence of viewpoints
// ordered by path
let objs = []
if (displayedNode && displayedNode.hasSequenceOutput && displayedAttr && (displayedAttr.desc.semantic === "imageList" || displayedAttr.desc.semantic === "sequence")) {
let sequence = Filepath.resolveSequence(path_template)
let ids = sequence[0]
let resolved = sequence[1]
// reset current frame to 0 if it is imageList but not sequence
if (displayedAttr.desc.semantic === "imageList") {
// concat in one array all sequences in resolved
resolved = [].concat.apply([], resolved)
frameRange.min = 0
frameRange.max = resolved.length-1
currentFrame = 0
}
if (displayedAttr.desc.semantic === "sequence") {
// if there is several sequences, take the first one, else take the only one
if (typeof resolved[0] === "object")
resolved = resolved[0]
ids = ids[0]
frameRange.min = ids[0]
frameRange.max = ids[ids.length-1]
currentFrame = frameRange.min
}
return resolved
} else {
for (let i = 0; i < _reconstruction.viewpoints.count; i++) {
objs.push(_reconstruction.viewpoints.at(i))
}
objs.sort((a, b) => { return a.childAttribute("path").value < b.childAttribute("path").value ? -1 : 1; })
let seq = [];
for (let i = 0; i < objs.length; i++) {
seq.push(Filepath.resolve(path_template, objs[i]))
}
frameRange.min = 0
frameRange.max = seq.length-1
currentFrame = 0
return seq
}
}
function getSequence() {
// Entry point for getting the current image sequence
if (useExternal) {
return []
}
if (_reconstruction && (!displayedNode || outputAttribute.name == "gallery")) {
return buildOrderedSequence("<PATH>")
}
if (_reconstruction) {
return buildOrderedSequence(displayedAttrValue)
}
return []
}
onDisplayedNodeChanged: {
if (!displayedNode) {
root.source = ""
}
// update output attribute names
var names = []
if (displayedNode) {
// store attr name for output attributes that represent images
for (var i = 0; i < displayedNode.attributes.count; i++) {
var attr = displayedNode.attributes.at(i)
if (attr.isOutput && (attr.desc.semantic === "image" || attr.desc.semantic === "sequence" || attr.desc.semantic === "imageList") && attr.enabled) {
names.push(attr.name)
}
}
}
if (!displayedNode || displayedNode.isComputable) names.push("gallery")
outputAttribute.names = names
}
onDisplayedAttrValueChanged: {
if (displayedNode && !displayedNode.hasSequenceOutput) {
root.source = getImageFile()
root.sequence = []
} else {
root.source = ""
root.sequence = getSequence()
enableSequencePlayerAction.checked = true
}
}
Connections {
target: _reconstruction
function onSelectedViewIdChanged() {
root.source = getImageFile()
if (useExternal)
useExternal = false
}
}
Connections {
target: displayedNode
function onOutputAttrEnabledChanged() {
tryLoadNode(displayedNode)
}
}
// 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 {
anchors.fill: parent
HdrImageToolbar {
id: hdrImageToolbar
anchors.margins: 0
visible: displayImageToolBarAction.checked && displayImageToolBarAction.enabled
Layout.fillWidth: true
onVisibleChanged: {
resetDefaultValues()
}
colorPickerVisible: {
return !displayPanoramaViewer.checked
}
colorRGBA: {
if (!floatImageViewerLoader.item ||
floatImageViewerLoader.item.imageStatus !== Image.Ready) {
return null
}
if (floatImageViewerLoader.item.containsMouse === false) {
return null
}
var pix = floatImageViewerLoader.item.pixelValueAt(Math.floor(floatImageViewerLoader.item.mouseX), Math.floor(floatImageViewerLoader.item.mouseY))
return pix
}
}
LensDistortionToolbar {
id: lensDistortionImageToolbar
anchors.margins: 0
visible: displayLensDistortionToolBarAction.checked && displayLensDistortionToolBarAction.enabled
Layout.fillWidth: true
}
PanoramaToolbar {
id: panoramaViewerToolbar
anchors.margins: 0
visible: displayPanoramaToolBarAction.checked && displayPanoramaToolBarAction.enabled
Layout.fillWidth: true
}
// Image
Item {
id: imgLayout
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Image {
id: alphaBackground
anchors.fill: parent
visible: displayAlphaBackground.checked
fillMode: Image.Tile
horizontalAlignment: Image.AlignLeft
verticalAlignment: Image.AlignTop
source: "../../img/checkerboard_light.png"
scale: 4
smooth: false
}
Item {
id: imgContainer
transformOrigin: Item.TopLeft
property var orientationTag: m.imgMetadata ? m.imgMetadata["Orientation"] : 0
// qtAliceVision Image Viewer
ExifOrientedViewer {
id: floatImageViewerLoader
active: root.aliceVisionPluginAvailable && (root.useFloatImageViewer || root.useLensDistortionViewer) && !panoramaViewerLoader.active
visible: (floatImageViewerLoader.status === Loader.Ready) && active
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
property bool fittedOnce: false
property int previousWidth: 0
property int previousHeight: 0
property real targetSize: Math.max(width, height) * imgContainer.scale
onHeightChanged: {
/* Image size is not updated through a single signal with the floatImage viewer, unlike
* the simple QML image viewer: instead of updating straight away the width and height to x and
* y, the emitted signals look like:
* - width = -1, height = -1
* - width = x, height = -1
* - width = x, height = y
* We want to do the auto-fit on the first display of an image from the group, and then keep its
* scale when displaying another image from the group, so we need to know if an image in the
* group has already been auto-fitted. If we change the group of images (when another project is
* opened, for example, and the images have a different size), then another auto-fit needs to be
* performed */
if ((!fittedOnce && imgContainer.image && imgContainer.image.height > 0) ||
(fittedOnce && ((width > 1 && previousWidth != width) || (height > 1 && previousHeight != height)))) {
fit()
fittedOnce = true
previousWidth = width
previousHeight = height
}
}
onActiveChanged: {
if (active) {
// Instantiate and initialize a FloatImage component dynamically using Loader.setSource
// Note: It does not work to use previously created component, so we re-create it with setSource.
floatImageViewerLoader.setSource("FloatImage.qml", {
'source': Qt.binding(function() { return getImageFile() }),
'gamma': Qt.binding(function() { return hdrImageToolbar.gammaValue }),
'gain': Qt.binding(function() { return hdrImageToolbar.gainValue }),
'channelModeString': Qt.binding(function() { return hdrImageToolbar.channelModeValue }),
'isPrincipalPointsDisplayed': Qt.binding(function() { return lensDistortionImageToolbar.displayPrincipalPoint }),
'surface.displayGrid': Qt.binding(function() { return lensDistortionImageToolbar.visible && lensDistortionImageToolbar.displayGrid }),
'surface.gridOpacity': Qt.binding(function() { return lensDistortionImageToolbar.opacityValue }),
'surface.gridColor': Qt.binding(function() { return lensDistortionImageToolbar.color }),
'surface.subdivisions': Qt.binding(function() { return root.useFloatImageViewer ? 1 : lensDistortionImageToolbar.subdivisionsValue }),
'viewerTypeString': Qt.binding(function() { return displayLensDistortionViewer.checked ? "distortion" : "hdr" }),
'sfmRequired': Qt.binding(function() { return displayLensDistortionViewer.checked ? true : false }),
'surface.msfmData': Qt.binding(function() { return (msfmDataLoader.status === Loader.Ready && msfmDataLoader.item != null && msfmDataLoader.item.status === 2) ? msfmDataLoader.item : null }),
'canBeHovered': false,
'idView': Qt.binding(function() { return ((root.displayedNode && !root.displayedNode.hasSequenceOutput && _reconstruction) ? _reconstruction.selectedViewId : -1) }),
'cropFisheye': false,
'sequence': Qt.binding(function() { return ((root.enableSequencePlayer && (_reconstruction || (root.displayedNode && root.displayedNode.hasSequenceOutput))) ? getSequence() : []) }),
'targetSize': Qt.binding(function() { return floatImageViewerLoader.targetSize }),
'useSequence': Qt.binding(function() {
let attr = root.displayedNode ? root.displayedNode.attributes.get(outputAttribute.name) : undefined
return (root.enableSequencePlayer && !useExternal && (_reconstruction || (root.displayedNode && root.displayedNode.hasSequenceOutput)) && (attr.desc.semantic === "imageList" || attr.desc.semantic === "sequence"))
}),
'fetchingSequence': Qt.binding(function() { return sequencePlayer.loading }),
'memoryLimit': Qt.binding(function() { return sequencePlayer.settings_SequencePlayer.maxCacheMemory }),
})
} else {
// Forcing the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
floatImageViewerLoader.setSource("", {})
fittedOnce = false
}
}
}
// qtAliceVision Panorama Viewer
Loader {
id: panoramaViewerLoader
active: root.aliceVisionPluginAvailable && root.usePanoramaViewer && _reconstruction.activeNodes.get('sfm').node
visible: (panoramaViewerLoader.status === Loader.Ready) && active
anchors.centerIn: parent
onActiveChanged: {
if (active) {
setSource("PanoramaViewer.qml", {
'subdivisionsPano': Qt.binding(function() { return panoramaViewerToolbar.subdivisionsValue }),
'cropFisheyePano': Qt.binding(function() { return root.cropFisheye }),
'downscale': Qt.binding(function() { return panoramaViewerToolbar.downscaleValue }),
'isEditable': Qt.binding(function() { return panoramaViewerToolbar.enableEdit }),
'isHighlightable': Qt.binding(function() { return panoramaViewerToolbar.enableHover }),
'displayGridPano': Qt.binding(function() { return panoramaViewerToolbar.displayGrid }),
'mouseMultiplier': Qt.binding(function() { return panoramaViewerToolbar.mouseSpeed }),
'msfmData': Qt.binding(function() { return (msfmDataLoader && msfmDataLoader.item && msfmDataLoader.status === Loader.Ready
&& msfmDataLoader.item.status === 2) ? msfmDataLoader.item : null }),
})
} else {
// Forcing the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
setSource("", {})
displayPanoramaViewer.checked = false
}
}
}
// Simple QML Image Viewer (using Qt or qtAliceVisionImageIO to load images)
ExifOrientedViewer {
id: qtImageViewerLoader
active: !floatImageViewerLoader.active && !panoramaViewerLoader.active
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
sourceComponent: Image {
id: qtImageViewer
asynchronous: true
smooth: false
fillMode: Image.PreserveAspectFit
onWidthChanged: if (status==Image.Ready) fit()
source: getImageFile()
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 maintain a displayed image for smoother transitions
Image {
id: qtImageViewerCache
anchors.fill: parent
asynchronous: true
smooth: parent.smooth
fillMode: parent.fillMode
visible: qtImageViewer.status === Image.Loading
}
}
}
property var image: {
if (floatImageViewerLoader.active)
floatImageViewerLoader.item
else if (panoramaViewerLoader.active)
panoramaViewerLoader.item
else
qtImageViewerLoader.item
}
width: image ? (image.width > 0 ? image.width : 1) : 1
height: image ? (image.height > 0 ? image.height : 1) : 1
scale: 1.0
// FeatureViewer: display view extracted feature points
// note: requires QtAliceVision plugin - use a Loader to evaluate plugin availability at runtime
ExifOrientedViewer {
id: featuresViewerLoader
active: displayFeatures.checked && !useExternal
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("featureProvider").node : null
width: imgContainer.width
height: imgContainer.height
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
onActiveChanged: {
if (active) {
// 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 }),
'syncFeaturesSelected': Qt.binding(function() { return sequencePlayer.syncFeaturesSelected }),
})
} else {
// Forcing the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
setSource("", {})
}
}
}
// FisheyeCircleViewer: display fisheye circle
// Note: use a Loader to evaluate if a PanoramaInit node exist and displayFisheyeCircle checked at runtime
ExifOrientedViewer {
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("PanoramaInit").node : null
active: displayFisheyeCircleLoader.checked && activeNode
sourceComponent: CircleGizmo {
width: imgContainer.width
height: imgContainer.height
property bool useAuto: activeNode.attribute("estimateFisheyeCircle").value
readOnly: useAuto
visible: (!useAuto) || activeNode.isComputed
property real userFisheyeRadius: activeNode.attribute("fisheyeRadius").value
property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(activeNode)
circleX: useAuto ? fisheyeAutoParams.x : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value
circleY: useAuto ? fisheyeAutoParams.y : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value
circleRadius: useAuto ? fisheyeAutoParams.z : ((imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (userFisheyeRadius * 0.01))
circleBorder.width: Math.max(1, (3.0 / imgContainer.scale))
onMoved: {
if (!useAuto) {
_reconstruction.setAttribute(
activeNode.attribute("fisheyeCenterOffset"),
JSON.stringify([xoffset, yoffset])
)
}
}
onIncrementRadius: {
if (!useAuto) {
_reconstruction.setAttribute(activeNode.attribute("fisheyeRadius"), activeNode.attribute("fisheyeRadius").value + radiusOffset)
}
}
}
}
// LightingCalibration: display circle
ExifOrientedViewer {
property var activeNode: _reconstruction.activeNodes.get("SphereDetection").node
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
active: displayLightingCircleLoader.checked && activeNode
sourceComponent: CircleGizmo {
property var jsonFolder: activeNode.attribute("output").value
property var json: null
property var currentViewId: _reconstruction.selectedViewId
property var nodeCircleX: activeNode.attribute("sphereCenter.x").value
property var nodeCircleY: activeNode.attribute("sphereCenter.y").value
property var nodeCircleRadius: activeNode.attribute("sphereRadius").value
width: imgContainer.width
height: imgContainer.height
readOnly: activeNode.attribute("autoDetect").value
circleX: nodeCircleX
circleY: nodeCircleY
circleRadius: nodeCircleRadius
circleBorder.width: Math.max(1, (3.0 / imgContainer.scale))
onJsonFolderChanged: {
json = null
if(activeNode.attribute("autoDetect").value) {
// auto detection enabled
var jsonPath = activeNode.attribute("output").value + "/detection.json"
Request.get(Filepath.stringToUrl(jsonPath), function(xhr) {
if (xhr.readyState === XMLHttpRequest.DONE) {
try {
json = JSON.parse(xhr.responseText)
} catch(exc) {
console.warn("Failed to parse SphereDetection JSON file: " + jsonPath)
}
}
updateGizmo()
})
}
}
onCurrentViewIdChanged: { updateGizmo() }
onNodeCircleXChanged : { updateGizmo() }
onNodeCircleYChanged : { updateGizmo() }
onNodeCircleRadiusChanged : { updateGizmo() }
function updateGizmo() {
if(activeNode.attribute("autoDetect").value) {
// update gizmo from auto detection json file
if(json) {
// json file found
var data = json[currentViewId]
if(data && data[0]) {
// current view id found
circleX = data[0].x
circleY= data[0].y
circleRadius = data[0].r
return
}
}
// no auto detection data
circleX = -1
circleY= -1
circleRadius = 0
}
else {
// update gizmo from node manual parameters
circleX = nodeCircleX
circleY = nodeCircleY
circleRadius = nodeCircleRadius
}
}
onMoved: {
_reconstruction.setAttribute(activeNode.attribute("sphereCenter"), JSON.stringify([xoffset, yoffset]))
}
onIncrementRadius: {
_reconstruction.setAttribute(activeNode.attribute("sphereRadius"), activeNode.attribute("sphereRadius").value + radiusOffset)
}
}
}
// ColorCheckerViewer: display color checker detection results
// note: use a Loader to evaluate if a ColorCheckerDetection node exist and displayColorChecker checked at runtime
ExifOrientedViewer {
id: colorCheckerViewerLoader
anchors.centerIn: parent
orientationTag: imgContainer.orientationTag
xOrigin: imgContainer.width / 2
yOrigin: imgContainer.height / 2
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("ColorCheckerDetection").node : null
active: (displayColorCheckerViewerLoader.checked && activeNode)
sourceComponent: ColorCheckerViewer {
width: imgContainer.width
height: imgContainer.height
visible: activeNode.isComputed && json !== undefined && imgContainer.image.imageStatus === Image.Ready
source: Filepath.stringToUrl(activeNode.attribute("outputData").value)
viewpoint: _reconstruction.selectedViewpoint
zoom: imgContainer.scale
updatePane: function() {
colorCheckerPane.colors = getColors();
}
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
FloatingPane {
id: imagePathToolbar
Layout.fillWidth: true
Layout.fillHeight: false
Layout.preferredHeight: childrenRect.height
visible: displayImagePathAction.checked
RowLayout {
width: parent.width
height: childrenRect.height
// 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())
}
// write which node is being displayed
Label {
id: displayedNodeName
text: root.displayedNode ? root.displayedNode.label : ""
font.pointSize: 8
horizontalAlignment: TextInput.AlignLeft
Layout.fillWidth: false
Layout.preferredWidth: contentWidth
height: contentHeight
}
// button to clear currently displayed file
MaterialToolButton {
id: clearViewerButton
text: MaterialIcons.close
ToolTip.text: root.useExternal ? "Close external file" : "Clear node"
enabled: root.displayedNode || root.useExternal
visible: root.displayedNode || root.useExternal
onClicked: {
if (root.displayedNode) root.displayedNode = null
if (root.useExternal) root.useExternal = false
}
}
}
}
FloatingPane {
Layout.fillWidth: true
Layout.fillHeight: false
Layout.preferredHeight: childrenRect.height
visible: floatImageViewerLoader.item !== null && floatImageViewerLoader.item.imageStatus === Image.Error
Layout.alignment: Qt.AlignHCenter
RowLayout {
anchors.fill: parent
Label {
font.pointSize: 8
text: {
if (floatImageViewerLoader.item !== null) {
switch (floatImageViewerLoader.item.status) {
case 2: // AliceVision.FloatImageViewer.EStatus.OUTDATED_LOADING
return "Outdated Loading"
case 3: // AliceVision.FloatImageViewer.EStatus.MISSING_FILE
return "Missing File"
case 4: // AliceVision.FloatImageViewer.EStatus.ERROR
return "Error"
default:
return ""
}
}
return ""
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
}
}
}
Item {
id: imgPlaceholder
Layout.fillWidth: true
Layout.fillHeight: true
// Image Metadata overlay Pane
ImageMetadataView {
width: 350
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
}
visible: metadataCB.checked
// only load metadata model if visible
metadata: {
if (visible) {
if (root.useExternal || outputAttribute.name != "gallery") {
return m.imgMetadata
} else {
return m.viewpointMetadata
}
}
return {}
}
}
ColorCheckerPane {
id: colorCheckerPane
width: 250
height: 170
anchors {
top: parent.top
right: parent.right
}
visible: displayColorCheckerViewerLoader.checked && colorCheckerPane.colors !== null
}
Loader {
id: mfeaturesLoader
property bool isUsed: displayFeatures.checked
property var activeNode: {
if (!root.aliceVisionPluginAvailable) {
return null
}
return _reconstruction ? _reconstruction.activeNodes.get("featureProvider").node : null
}
property bool isComputed: activeNode && activeNode.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", {
'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 {
// Forcing the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
setSource("", {})
}
}
}
Loader {
id: msfmDataLoader
property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked
|| displayPanoramaViewer.checked || displayLensDistortionViewer.checked
property var activeNode: {
if (!root.aliceVisionPluginAvailable) {
return null
}
var nodeType = "sfm"
if (displayLensDistortionViewer.checked) {
nodeType = "sfmData"
}
var sfmNode = _reconstruction ? _reconstruction.activeNodes.get(nodeType).node : null
if (sfmNode === null) {
return null
}
if (displayPanoramaViewer.checked) {
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 && activeNode.hasAttribute("output")) {
sfmValue = activeNode.attribute("output").value
}
return Filepath.stringToUrl(sfmValue)
}
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("MSfMData.qml", {
'sfmDataPath': Qt.binding(function() { return filepath }),
})
} else {
// Force the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
setSource("", {})
}
}
}
Loader {
id: mtracksLoader
property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked || displayPanoramaViewer.checked
property var activeNode: {
if (!root.aliceVisionPluginAvailable) {
return null
}
return _reconstruction ? _reconstruction.activeNodes.get("matchProvider").node : null
}
property bool isComputed: activeNode && activeNode.isComputed
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", {
'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 {
// Forcing the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14
setSource("", {})
}
}
}
Loader {
id: sfmStatsView
anchors.fill: parent
active: msfmDataLoader.status === Loader.Ready && displaySfmStatsView.checked
onActiveChanged: {
// Load and unload the component explicitly
// (necessary since Qt 5.14, Component.onCompleted cannot be used anymore to load the data once and for all)
if (active) {
setSource("SfmStatsView.qml", {
"msfmData": Qt.binding(function() { return msfmDataLoader.item }),
"viewId": Qt.binding(function() { return _reconstruction.selectedViewId }),
})
} else {
setSource("", {})
}
}
}
Loader {
id: sfmGlobalStats
anchors.fill: parent
active: msfmDataLoader.status === Loader.Ready && displaySfmDataGlobalStats.checked
onActiveChanged: {
// Load and unload the component explicitly
// (necessary since Qt 5.14, Component.onCompleted cannot be used anymore to load the data once and for all)
if (active) {
setSource("SfmGlobalStats.qml", {
'msfmData': Qt.binding(function() { return msfmDataLoader.item }),
'mTracks': Qt.binding(function() { return mtracksLoader.item }),
})
} else {
setSource("", {})
}
}
}
Loader {
id: featuresOverlay
anchors {
bottom: parent.bottom
left: parent.left
margins: 2
}
active: root.aliceVisionPluginAvailable && displayFeatures.checked && featuresViewerLoader.status === Loader.Ready
sourceComponent: FeaturesInfoOverlay {
pluginStatus: featuresViewerLoader.status
featuresViewer: featuresViewerLoader.item
mfeatures: mfeaturesLoader.item
mtracks: mtracksLoader.item
msfmdata: msfmDataLoader.item
}
}
Loader {
id: ldrHdrCalibrationGraph
anchors.fill: parent
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get('LdrToHdrCalibration').node : null
property var isEnabled: displayLdrHdrCalibrationGraph.checked && activeNode && activeNode.isComputed
active: isEnabled
property var path: activeNode && activeNode.hasAttribute("response") ? activeNode.attribute("response").value : ""
property var vp: _reconstruction ? getViewpoint(_reconstruction.selectedViewId) : null
sourceComponent: CameraResponseGraph {
responsePath: Filepath.resolve(path, vp)
}
}
}
FloatingPane {
id: bottomToolbar
padding: 4
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
RowLayout {
anchors.fill: parent
// zoom label
MLabel {
text: ((imgContainer.image && (imgContainer.image.imageStatus === Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button & Qt.LeftButton) {
fit()
} else if (mouse.button & Qt.RightButton) {
var menu = contextMenu.createObject(root)
var point = mapToItem(root, mouse.x, mouse.y)
menu.x = point.x
menu.y = point.y
menu.open()
}
}
}
ToolTip.text: "Zoom"
}
MaterialToolButton {
id: displayAlphaBackground
ToolTip.text: "Alpha Background"
text: MaterialIcons.texture
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true
}
MaterialToolButton
{
id: displayHDR
ToolTip.text: "High-Dynamic-Range Image Viewer"
text: MaterialIcons.hdr_on
// larger font but smaller padding,
// so it is visually similar.
font.pointSize: 20
padding: 0
Layout.minimumWidth: 0
checkable: true
checked: root.aliceVisionPluginAvailable
enabled: root.aliceVisionPluginAvailable
visible: root.enable8bitViewer
onCheckedChanged : {
if (displayLensDistortionViewer.checked && checked) {
displayLensDistortionViewer.checked = false
}
root.useFloatImageViewer = !root.useFloatImageViewer
}
}
MaterialToolButton {
id: displayLensDistortionViewer
property int numberChanges: 0
property bool previousChecked: false
property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get('sfmData').node : null
property bool isComputed: {
if (!activeNode)
return false
if (activeNode.isComputed)
return true
if (!activeNode.hasAttribute("input"))
return false
var inputAttr = activeNode.attribute("input")
var inputAttrLink = inputAttr.rootLinkParam
if (!inputAttrLink)
return false
return inputAttrLink.node.isComputed
}
ToolTip.text: "Lens Distortion Viewer" + (isComputed ? (": " + activeNode.label) : "")
text: MaterialIcons.panorama_horizontal
font.pointSize: 16
padding: 0
Layout.minimumWidth: 0
checkable: true
checked: false
enabled: activeNode && isComputed
onCheckedChanged : {
if ((displayHDR.checked || displayPanoramaViewer.checked) && checked) {
displayHDR.checked = false
displayPanoramaViewer.checked = false
} else if (!checked) {
displayHDR.checked = true
}
}
onActiveNodeChanged: {
numberChanges += 1
}
onEnabledChanged: {
if (!enabled) {
previousChecked = checked
checked = false
numberChanges = 0
}
if (enabled && (numberChanges == 1) && previousChecked) {
checked = true
}
}
}
MaterialToolButton {
id: displayPanoramaViewer
property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get('SfMTransform').node : null
property bool isComputed: {
if (!activeNode)
return false
if (activeNode.attribute("method").value !== "manual")
return false
var inputAttr = activeNode.attribute("input")
if (!inputAttr)
return false
var inputAttrLink = inputAttr.rootLinkParam
if (!inputAttrLink)
return false
return inputAttrLink.node.isComputed
}
ToolTip.text: activeNode ? "Panorama Viewer " + activeNode.label : "Panorama Viewer"
text: MaterialIcons.panorama_photosphere
font.pointSize: 16
padding: 0
Layout.minimumWidth: 0
checkable: true
checked: false
enabled: activeNode && isComputed
onCheckedChanged : {
if (displayLensDistortionViewer.checked && checked) {
displayLensDistortionViewer.checked = false
}
if (displayFisheyeCircleLoader.checked && checked) {
displayFisheyeCircleLoader.checked = false
}
}
onEnabledChanged : {
if (!enabled) {
checked = false
}
}
}
MaterialToolButton {
id: displayFeatures
ToolTip.text: "Display Features"
text: MaterialIcons.scatter_plot
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true && !useExternal
checked: false
enabled: root.aliceVisionPluginAvailable && !displayPanoramaViewer.checked && !useExternal
onEnabledChanged : {
if (useExternal) return
if (enabled == false) checked = false
}
}
MaterialToolButton {
id: displayFisheyeCircleLoader
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get('PanoramaInit').node : null
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: activeNode && activeNode.attribute("useFisheye").value && !displayPanoramaViewer.checked
visible: activeNode
}
MaterialToolButton {
id: displayLightingCircleLoader
property var activeNode: _reconstruction.activeNodes.get('SphereDetection').node
ToolTip.text: "Display Lighting Circle: " + (activeNode ? activeNode.label : "No Node")
text: MaterialIcons.location_searching
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true
checked: false
enabled: activeNode
visible: activeNode
}
MaterialToolButton {
id: displayColorCheckerViewerLoader
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get('ColorCheckerDetection').node : null
ToolTip.text: "Display Color Checker: " + (activeNode ? activeNode.label : "No Node")
text: MaterialIcons.view_comfy //view_module grid_on gradient view_comfy border_all
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true
enabled: activeNode && activeNode.isComputed && _reconstruction.selectedViewId !== -1
checked: false
visible: activeNode
onEnabledChanged: {
if (enabled == false)
checked = false
}
onCheckedChanged: {
if (checked == true) {
displaySfmDataGlobalStats.checked = false
displaySfmStatsView.checked = false
metadataCB.checked = false
}
}
}
MaterialToolButton {
id: displayLdrHdrCalibrationGraph
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("LdrToHdrCalibration").node : null
property bool isComputed: activeNode && activeNode.isComputed
ToolTip.text: "Display Camera Response Function: " + (activeNode ? activeNode.label : "No Node")
text: MaterialIcons.timeline
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true
checked: false
enabled: activeNode && activeNode.isComputed
visible: activeNode
onIsComputedChanged: {
if (!isComputed)
checked = false
}
}
Label {
id: resolutionLabel
Layout.fillWidth: true
text: (imgContainer.image && imgContainer.image.sourceSize.width > 0) ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : ""
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
ComboBox {
id: outputAttribute
clip: true
Layout.minimumWidth: 0
flat: true
property var names: ["gallery"]
property string name: names[currentIndex]
model: names.map(n => (n === "gallery") ? "Image Gallery" : displayedNode.attributes.get(n).label)
enabled: count > 1
FontMetrics {
id: fontMetrics
}
Layout.preferredWidth: model.reduce((acc, label) => Math.max(acc, fontMetrics.boundingRect(label).width), 0) + 3.0 * Qt.application.font.pixelSize
onNameChanged: {
root.source = getImageFile()
root.sequence = getSequence()
}
}
MaterialToolButton {
id: displayImageOutputIn3D
enabled: root.aliceVisionPluginAvailable && _reconstruction && displayedNode && Filepath.basename(root.source).includes("depthMap")
ToolTip.text: "View Depth Map in 3D"
text: MaterialIcons.input
font.pointSize: 11
Layout.minimumWidth: 0
onClicked: {
root.viewIn3D(
root.source,
displayedNode.name + ":" + outputAttribute.name + " " + String(_reconstruction.selectedViewId)
)
}
}
MaterialToolButton {
id: displaySfmStatsView
property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get('sfm').node : null
property bool isComputed: activeNode && activeNode.isComputed
font.family: MaterialIcons.fontFamily
text: MaterialIcons.assessment
ToolTip.text: "StructureFromMotion Statistics" + (isComputed ? (": " + activeNode.label) : "")
ToolTip.visible: hovered
font.pointSize: 14
padding: 2
smooth: false
flat: true
checkable: enabled
enabled: activeNode && activeNode.isComputed && _reconstruction.selectedViewId >= 0
onCheckedChanged: {
if (checked == true) {
displaySfmDataGlobalStats.checked = false
metadataCB.checked = false
displayColorCheckerViewerLoader.checked = false
}
}
}
MaterialToolButton {
id: displaySfmDataGlobalStats
property var activeNode: root.aliceVisionPluginAvailable && _reconstruction ? _reconstruction.activeNodes.get('sfm').node : null
property bool isComputed: activeNode && activeNode.isComputed
font.family: MaterialIcons.fontFamily
text: MaterialIcons.language
ToolTip.text: "StructureFromMotion Global Statistics" + (isComputed ? (": " + activeNode.label) : "")
ToolTip.visible: hovered
font.pointSize: 14
padding: 2
smooth: false
flat: true
checkable: enabled
enabled: activeNode && activeNode.isComputed
onCheckedChanged: {
if (checked == true) {
displaySfmStatsView.checked = false
metadataCB.checked = false
displayColorCheckerViewerLoader.checked = false
}
}
}
MaterialToolButton {
id: metadataCB
font.family: MaterialIcons.fontFamily
text: MaterialIcons.info_outline
ToolTip.text: "Image Metadata"
ToolTip.visible: hovered
font.pointSize: 14
padding: 2
smooth: false
flat: true
checkable: enabled
onCheckedChanged: {
if (checked == true) {
displaySfmDataGlobalStats.checked = false
displaySfmStatsView.checked = false
displayColorCheckerViewerLoader.checked = false
}
}
}
}
}
SequencePlayer {
id: sequencePlayer
anchors.margins: 0
Layout.fillWidth: true
sortedViewIds: { return (root.enableSequencePlayer && (root.displayedNode && root.displayedNode.hasSequenceOutput)) ? root.sequence : (_reconstruction && _reconstruction.viewpoints.count > 0) ? buildOrderedSequence("<VIEW_ID>") : [] }
viewer: floatImageViewerLoader.status === Loader.Ready ? floatImageViewerLoader.item : null
visible: root.enableSequencePlayer
enabled: root.enableSequencePlayer
isOutputSequence: root.displayedNode && root.displayedNode.hasSequenceOutput
}
}
}
}
// Busy indicator
BusyIndicator {
anchors.centerIn: parent
// running property binding seems broken, only dynamic binding assignment works
Component.onCompleted: {
running = Qt.binding(function() {
return (root.usePanoramaViewer === true && imgContainer.image && imgContainer.image.allImagesLoaded === false)
|| (imgContainer.image && imgContainer.image.imageStatus === Image.Loading)
})
}
// disable the visibility when unused to avoid stealing the mouseEvent to the image color picker
visible: running
onVisibleChanged: {
if (panoramaViewerLoader.active)
fit()
}
}
// Actions for RGBA filters
Action {
id: rFilterAction
shortcut: "R"
onTriggered: {
if (hdrImageToolbar.channelModeValue !== 'r') {
hdrImageToolbar.channelModeValue = 'r'
} else {
hdrImageToolbar.channelModeValue = 'rgba'
}
}
}
Action {
id: gFilterAction
shortcut: "G"
onTriggered: {
if (hdrImageToolbar.channelModeValue !== 'g') {
hdrImageToolbar.channelModeValue = 'g'
} else {
hdrImageToolbar.channelModeValue = 'rgba'
}
}
}
Action {
id: bFilterAction
shortcut: "B"
onTriggered: {
if (hdrImageToolbar.channelModeValue !== 'b') {
hdrImageToolbar.channelModeValue = 'b'
} else {
hdrImageToolbar.channelModeValue = 'rgba'
}
}
}
Action {
id: aFilterAction
shortcut: "A"
onTriggered: {
if (hdrImageToolbar.channelModeValue !== 'a') {
hdrImageToolbar.channelModeValue = 'a'
} else {
hdrImageToolbar.channelModeValue = 'rgba'
}
}
}
// Actions for Metadata overlay
Action {
id: metadataAction
shortcut: "I"
onTriggered: {
metadataCB.checked = !metadataCB.checked
}
}
}