From 4fd1f9708d511e20b7d85c077c01374a3a6477da Mon Sep 17 00:00:00 2001 From: Fabien Servant Date: Thu, 19 Dec 2019 13:45:24 +0100 Subject: [PATCH 001/100] Add debug options for visualization of borders and seams --- meshroom/nodes/aliceVision/PanoramaCompositing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshroom/nodes/aliceVision/PanoramaCompositing.py b/meshroom/nodes/aliceVision/PanoramaCompositing.py index 34af53ad70..417d5fd70e 100644 --- a/meshroom/nodes/aliceVision/PanoramaCompositing.py +++ b/meshroom/nodes/aliceVision/PanoramaCompositing.py @@ -37,6 +37,16 @@ class PanoramaCompositing(desc.CommandLineNode): exclusive=True, uid=[0] ), + desc.ChoiceParam( + name='overlayType', + label='Overlay Type', + description='Which overlay to display on top of panorama for debug', + value='none', + values=['none', 'borders', 'seams'], + exclusive=True, + advanced=True, + uid=[0] + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 1b8aea7db38eade8e056e1494571eb029a841f42 Mon Sep 17 00:00:00 2001 From: fabienservant Date: Fri, 7 Feb 2020 07:58:26 +0100 Subject: [PATCH 002/100] add camera type choice for equidistant --- meshroom/nodes/aliceVision/CameraInit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index eff321e12a..7bdc8583df 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -23,7 +23,7 @@ desc.IntParam(name="intrinsicId", label="Id", description="Intrinsic UID", value=-1, uid=[0], range=None), desc.FloatParam(name="pxInitialFocalLength", label="Initial Focal Length", description="Initial Guess on the Focal Length", value=-1.0, uid=[0], range=None), desc.FloatParam(name="pxFocalLength", label="Focal Length", description="Known/Calibrated Focal Length", value=-1.0, uid=[0], range=None), - desc.ChoiceParam(name="type", label="Camera Type", description="Camera Type", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4'], exclusive=True, uid=[0]), + desc.ChoiceParam(name="type", label="Camera Type", description="Camera Type", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'equidistant_r3'], exclusive=True, uid=[0]), desc.IntParam(name="width", label="Width", description="Image Width", value=0, uid=[], range=(0, 10000, 1)), desc.IntParam(name="height", label="Height", description="Image Height", value=0, uid=[], range=(0, 10000, 1)), desc.StringParam(name="serialNumber", label="Serial Number", description="Device Serial Number (camera and lens combined)", value="", uid=[]), From 65db54e9d5c93d9fd177cbddbb5345251043bfbe Mon Sep 17 00:00:00 2001 From: fabienservant Date: Fri, 7 Feb 2020 07:59:44 +0100 Subject: [PATCH 003/100] add rotation matrix evaluation for panorama --- meshroom/nodes/aliceVision/PanoramaEstimation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index 6aaff58f36..967b4f90d7 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -98,7 +98,7 @@ class PanoramaEstimation(desc.CommandLineNode): description="Method for relative rotation :\n" " * from essential matrix\n" " * from homography matrix", - values=['essential_matrix', 'homography_matrix'], + values=['essential_matrix', 'homography_matrix', 'rotation_matrix'], value='homography_matrix', exclusive=True, uid=[0], From 6d4015c9d45abe4333e51befc9e35adb52a0c49e Mon Sep 17 00:00:00 2001 From: fabienservant Date: Fri, 7 Feb 2020 08:21:37 +0100 Subject: [PATCH 004/100] add a node to select fisheye circle --- meshroom/nodes/aliceVision/FisheyeCircle.py | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 meshroom/nodes/aliceVision/FisheyeCircle.py diff --git a/meshroom/nodes/aliceVision/FisheyeCircle.py b/meshroom/nodes/aliceVision/FisheyeCircle.py new file mode 100644 index 0000000000..cea2711192 --- /dev/null +++ b/meshroom/nodes/aliceVision/FisheyeCircle.py @@ -0,0 +1,64 @@ +__version__ = "1.0" + +import json +import os + +from meshroom.core import desc + + +class FisheyeCircle(desc.CommandLineNode): + commandLine = 'aliceVision_fisheyeCircle {allParams}' + size = desc.DynamicNodeSize('input') + + inputs = [ + desc.File( + name='input', + label='Input', + description="SfM Data File", + value='', + uid=[0], + ), + desc.FloatParam( + name='offsetx', + label='Center X offset', + description='Fisheye visibillity circle offset in X (pixels).', + value=0.0, + range=(-1000.0, 1000.0, 0.01), + uid=[0], + ), + desc.FloatParam( + name='offsety', + label='Center Y offset', + description='Fisheye visibillity circle offset in Y (pixels).', + value=0.0, + range=(-1000.0, 1000.0, 0.01), + uid=[0], + ), + desc.FloatParam( + name='radius', + label='Radius', + description='Fisheye visibillity circle radius (pixels).', + value=100.0, + range=(0.0, 100000.0, 0.01), + uid=[0], + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='Verbosity level (fatal, error, warning, info, debug, trace).', + value='info', + values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], + exclusive=True, + uid=[], + ), + ] + + outputs = [ + desc.File( + name='outSfMDataFilename', + label='Output SfMData File', + description='Path to the output sfmdata file', + value=desc.Node.internalFolder + 'sfmData.abc', + uid=[], + ) + ] From ff341021901aaded75614d384b3b7545d3d81bc5 Mon Sep 17 00:00:00 2001 From: fabienservant Date: Thu, 13 Feb 2020 11:11:27 +0100 Subject: [PATCH 005/100] Add sensor size to camera init --- meshroom/nodes/aliceVision/CameraInit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 7bdc8583df..23817b4005 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -26,6 +26,8 @@ desc.ChoiceParam(name="type", label="Camera Type", description="Camera Type", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'equidistant_r3'], exclusive=True, uid=[0]), desc.IntParam(name="width", label="Width", description="Image Width", value=0, uid=[], range=(0, 10000, 1)), desc.IntParam(name="height", label="Height", description="Image Height", value=0, uid=[], range=(0, 10000, 1)), + desc.FloatParam(name="sensorWidth", label="Sensor Width", description="Sensor Width (mm)", value=36, uid=[], range=(0, 1000, 1)), + desc.FloatParam(name="sensorHeight", label="Sensor Height", description="Sensor Height (mm)", value=24, uid=[], range=(0, 1000, 1)), desc.StringParam(name="serialNumber", label="Serial Number", description="Device Serial Number (camera and lens combined)", value="", uid=[]), desc.GroupAttribute(name="principalPoint", label="Principal Point", description="", groupDesc=[ desc.FloatParam(name="x", label="x", description="", value=0, uid=[], range=(0, 10000, 1)), From 215ef1eb4827c9b6978dd25ebf9720964ce787ab Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 25 Feb 2020 14:21:47 +0100 Subject: [PATCH 006/100] [nodes] PanoramaEstimation: use rotation matrix by default --- meshroom/nodes/aliceVision/PanoramaEstimation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index 967b4f90d7..460a6a329b 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -97,9 +97,10 @@ class PanoramaEstimation(desc.CommandLineNode): label='Relative Rotation Method', description="Method for relative rotation :\n" " * from essential matrix\n" - " * from homography matrix", + " * from homography matrix\n" + " * from rotation matrix", values=['essential_matrix', 'homography_matrix', 'rotation_matrix'], - value='homography_matrix', + value='rotation_matrix', exclusive=True, uid=[0], advanced=True, From 20c0e04c6726c9a6dafd97821acd03c071c3be08 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 26 Feb 2020 21:52:26 +0100 Subject: [PATCH 007/100] [pipeline] hdri: use high preset for featureExtraction --- meshroom/multiview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 99983a6661..d0afcccf66 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -134,7 +134,7 @@ def hdriPipeline(graph): featureExtraction = graph.addNewNode('FeatureExtraction', input=ldr2hdr.outSfMDataFilename) - featureExtraction.describerPreset.value = 'ultra' + featureExtraction.describerPreset.value = 'high' imageMatching = graph.addNewNode('ImageMatching', input=featureExtraction.input, featuresFolders=[featureExtraction.output]) From 8929456ae4de5519c678d1795c78d16028a72759 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 27 Feb 2020 16:54:10 +0100 Subject: [PATCH 008/100] [nodes] rename PanoramaExternalInfo to PanoramaInit * update HDRI pipeline * rename the fake tractor dependency param to make it more explicit that it is only used for dependency (and not as a real input) --- meshroom/multiview.py | 16 ++++++++-------- ...PanoramaExternalInfo.py => PanoramaInit.py} | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) rename meshroom/nodes/aliceVision/{PanoramaExternalInfo.py => PanoramaInit.py} (73%) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index d0afcccf66..9d12eaf8d8 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -135,21 +135,21 @@ def hdriPipeline(graph): featureExtraction = graph.addNewNode('FeatureExtraction', input=ldr2hdr.outSfMDataFilename) featureExtraction.describerPreset.value = 'high' - imageMatching = graph.addNewNode('ImageMatching', + panoramaInit = graph.addNewNode('PanoramaInit', input=featureExtraction.input, + dependency=[featureExtraction.output] # Workaround for tractor submission with a fake dependency + ) + + imageMatching = graph.addNewNode('ImageMatching', + input=panoramaInit.outSfMDataFilename, featuresFolders=[featureExtraction.output]) featureMatching = graph.addNewNode('FeatureMatching', input=imageMatching.input, featuresFolders=imageMatching.featuresFolders, imagePairsList=imageMatching.output) - panoramaExternalInfo = graph.addNewNode('PanoramaExternalInfo', - input=ldr2hdr.outSfMDataFilename, - matchesFolders=[featureMatching.output] # Workaround for tractor submission with a fake dependency - ) - panoramaEstimation = graph.addNewNode('PanoramaEstimation', - input=panoramaExternalInfo.outSfMDataFilename, + input=featureMatching.input, featuresFolders=featureMatching.featuresFolders, matchesFolders=[featureMatching.output]) @@ -162,9 +162,9 @@ def hdriPipeline(graph): return [ cameraInit, featureExtraction, + panoramaInit, imageMatching, featureMatching, - panoramaExternalInfo, panoramaEstimation, panoramaWarping, panoramaCompositing, diff --git a/meshroom/nodes/aliceVision/PanoramaExternalInfo.py b/meshroom/nodes/aliceVision/PanoramaInit.py similarity index 73% rename from meshroom/nodes/aliceVision/PanoramaExternalInfo.py rename to meshroom/nodes/aliceVision/PanoramaInit.py index 4fca9880ad..8629c069a0 100644 --- a/meshroom/nodes/aliceVision/PanoramaExternalInfo.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -6,8 +6,8 @@ from meshroom.core import desc -class PanoramaExternalInfo(desc.CommandLineNode): - commandLine = 'aliceVision_panoramaExternalInfo {allParams}' +class PanoramaInit(desc.CommandLineNode): + commandLine = 'aliceVision_panoramaInit {allParams}' size = desc.DynamicNodeSize('input') inputs = [ @@ -27,16 +27,16 @@ class PanoramaExternalInfo(desc.CommandLineNode): ), desc.ListAttribute( elementDesc=desc.File( - name='matchesFolder', - label='Matches Folder', + name='dependency', + label='', description="", value='', - uid=[0], + uid=[], ), - name='matchesFolders', - label='Matches Folders', - description="Folder(s) in which computed matches are stored. (WORKAROUND for valid Tractor graph submission)", - group='forDependencyOnly', + name='dependency', + label='Dependency', + description="Folder(s) in which computed features are stored. (WORKAROUND for valid Tractor graph submission)", + group='forDependencyOnly', # not a command line argument ), desc.ChoiceParam( name='verboseLevel', From 042111671a22a7b089761b69007b3858766899ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 20 Feb 2020 21:10:39 +0100 Subject: [PATCH 009/100] [ui] Viewer2D: Add Fisheye Circle display on PanoramaInit node --- meshroom/nodes/aliceVision/FisheyeCircle.py | 64 --------------------- meshroom/nodes/aliceVision/PanoramaInit.py | 34 ++++++++++- meshroom/ui/qml/Viewer/CircleGizmo.qml | 53 +++++++++++++++++ meshroom/ui/qml/Viewer/Viewer2D.qml | 29 ++++++++++ meshroom/ui/reconstruction.py | 14 +++++ 5 files changed, 129 insertions(+), 65 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/FisheyeCircle.py create mode 100644 meshroom/ui/qml/Viewer/CircleGizmo.qml diff --git a/meshroom/nodes/aliceVision/FisheyeCircle.py b/meshroom/nodes/aliceVision/FisheyeCircle.py deleted file mode 100644 index cea2711192..0000000000 --- a/meshroom/nodes/aliceVision/FisheyeCircle.py +++ /dev/null @@ -1,64 +0,0 @@ -__version__ = "1.0" - -import json -import os - -from meshroom.core import desc - - -class FisheyeCircle(desc.CommandLineNode): - commandLine = 'aliceVision_fisheyeCircle {allParams}' - size = desc.DynamicNodeSize('input') - - inputs = [ - desc.File( - name='input', - label='Input', - description="SfM Data File", - value='', - uid=[0], - ), - desc.FloatParam( - name='offsetx', - label='Center X offset', - description='Fisheye visibillity circle offset in X (pixels).', - value=0.0, - range=(-1000.0, 1000.0, 0.01), - uid=[0], - ), - desc.FloatParam( - name='offsety', - label='Center Y offset', - description='Fisheye visibillity circle offset in Y (pixels).', - value=0.0, - range=(-1000.0, 1000.0, 0.01), - uid=[0], - ), - desc.FloatParam( - name='radius', - label='Radius', - description='Fisheye visibillity circle radius (pixels).', - value=100.0, - range=(0.0, 100000.0, 0.01), - uid=[0], - ), - desc.ChoiceParam( - name='verboseLevel', - label='Verbose Level', - description='Verbosity level (fatal, error, warning, info, debug, trace).', - value='info', - values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], - exclusive=True, - uid=[], - ), - ] - - outputs = [ - desc.File( - name='outSfMDataFilename', - label='Output SfMData File', - description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'sfmData.abc', - uid=[], - ) - ] diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index 8629c069a0..0d24bc9ae0 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -38,6 +38,38 @@ class PanoramaInit(desc.CommandLineNode): description="Folder(s) in which computed features are stored. (WORKAROUND for valid Tractor graph submission)", group='forDependencyOnly', # not a command line argument ), + desc.BoolParam( + name='useFisheye', + label='Full Fisheye', + description='To declare a full fisheye panorama setup', + value=False, + uid=[0], + ), + desc.GroupAttribute( + name="fisheyeCenterOffset", + label="Fisheye Center", + description="Center of the Fisheye circle (XY offset to the center in pixels).", + groupDesc=[ + desc.FloatParam( + name="x", label="x", description="", + value=0.0, + uid=[0], + range=(-1000.0, 10000.0, 1.0)), + desc.FloatParam( + name="y", label="y", description="", + value=0.0, + uid=[0], + range=(-1000.0, 10000.0, 1.0)), + ], + ), + desc.FloatParam( + name='fisheyeRadius', + label='Radius', + description='Fisheye visibillity circle radius (% of image shortest side).', + value=96.0, + range=(0.0, 150.0, 0.01), + uid=[0], + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', @@ -54,7 +86,7 @@ class PanoramaInit(desc.CommandLineNode): name='outSfMDataFilename', label='Output SfMData File', description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'sfmData.abc', + value=desc.Node.internalFolder + 'sfmData.sfm', uid=[], ) ] diff --git a/meshroom/ui/qml/Viewer/CircleGizmo.qml b/meshroom/ui/qml/Viewer/CircleGizmo.qml new file mode 100644 index 0000000000..b9d6e586b2 --- /dev/null +++ b/meshroom/ui/qml/Viewer/CircleGizmo.qml @@ -0,0 +1,53 @@ +import QtQuick 2.11 + +Rectangle { + id: root + + signal moved() + + width: radius * 2 + height: width + color: "transparent" + border.width: 5 + border.color: "yellow" + + Behavior on x { + NumberAnimation { + duration: 100 + } + } + + Behavior on y { + NumberAnimation { + duration: 100 + } + } + + Behavior on radius { + NumberAnimation { + duration: 100 + } + } + + MouseArea { + id: mArea + anchors.fill: parent + cursorShape: Qt.OpenHandCursor + acceptedButtons: Qt.LeftButton + hoverEnabled: true + drag.target: parent + + drag.onActiveChanged: + { + if(!drag.active) + { + cursorShape = Qt.OpenHandCursor; + moved(); + } + else + { + cursorShape = Qt.ClosedHandCursor; + } + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 1140a07fe4..041cee16cc 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -235,6 +235,24 @@ FocusScope { }) } } + + // FisheyeCircleViewer: display fisheye circle + // note: use a Loader to evaluate if a PanoramaInit node exist and displayFisheyeCircle checked at runtime + Loader { + anchors.centerIn: parent + active: (_reconstruction.panoramaInit && displayFisheyeCircleLoader.checked) + sourceComponent: CircleGizmo { + x: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.x").value + y: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.y").value + radius: (imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (_reconstruction.panoramaInit.attribute("fisheyeRadius").value * 0.01) + border.width: Math.max(1, (3.0 / imgContainer.scale)) + + onMoved: { + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.x"), x) + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.y"), y) + } + } + } } ColumnLayout { @@ -373,6 +391,17 @@ FocusScope { checkable: true checked: false } + MaterialToolButton { + id: displayFisheyeCircleLoader + ToolTip.text: "Display Fisheye Circle" + text: MaterialIcons.panorama_fish_eye + font.pointSize: 11 + Layout.minimumWidth: 0 + checkable: true + checked: false + enabled: _reconstruction.panoramaInit + visible: enabled + } Label { id: resolutionLabel diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index cb78edc756..0dcf0bd1b4 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -403,6 +403,10 @@ def __init__(self, defaultPipeline='', parent=None): self._ldr2hdr = None self.cameraInitChanged.connect(self.updateLdr2hdrNode) + # - PanoramaInit + self._panoramaInit = None + self.cameraInitChanged.connect(self.updatePanoramaInitNode) + # react to internal graph changes to update those variables self.graphChanged.connect(self.onGraphChanged) @@ -461,6 +465,7 @@ def onGraphChanged(self): self.texturing = None self.ldr2hdr = None self.hdrCameraInit = None + self.panoramaInit = None self.updateCameraInits() if not self._graph: return @@ -527,6 +532,10 @@ def setupLDRToHDRCameraInit(self): tmpCameraInit = Node("CameraInit", viewpoints=views, intrinsics=intrinsics) self.hdrCameraInit = tmpCameraInit + def updatePanoramaInitNode(self): + """ Set the current FeatureExtraction node based on the current CameraInit node. """ + self.panoramaInit = self.lastNodeOfType('PanoramaInit', self.cameraInit) if self.cameraInit else None + def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS) @@ -836,6 +845,8 @@ def setActiveNodeOfType(self, node): self.prepareDenseScene = node elif node.nodeType in ("DepthMap", "DepthMapFilter"): self.depthMap = node + elif node.nodeType == "PanoramaInit": + self.panoramaInit = node def updateSfMResults(self): """ @@ -1009,6 +1020,9 @@ def getPoseRT(self, viewpoint): ldr2hdrChanged = Signal() ldr2hdr = makeProperty(QObject, "_ldr2hdr", notify=ldr2hdrChanged, resetOnDestroy=True) + panoramaInitChanged = Signal() + panoramaInit = makeProperty(QObject, "_panoramaInit", notify=panoramaInitChanged, resetOnDestroy=True) + nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) # Signals to propagate high-level messages From 517014faf63201614ac7ae613d9b642a4b6e9a68 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 6 Mar 2020 18:21:35 +0100 Subject: [PATCH 010/100] [core] command line: subattributes of GroupAttribute can be used directly on command line --- meshroom/core/node.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 049c08034f..f296c05a6d 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -549,6 +549,23 @@ def _computeUids(self): self._uids[uidIndex] = hashValue(uidAttributes) def _buildCmdVars(self): + def _buildAttributeCmdVars(cmdVars, name, attr): + if attr.attributeDesc.group != None: + # if there is a valid command line "group" + v = attr.getValueStr() + cmdVars[name] = '--{name} {value}'.format(name=name, value=v) + cmdVars[name + 'Value'] = str(v) + + if v: + cmdVars[attr.attributeDesc.group] = cmdVars.get(attr.attributeDesc.group, '') + \ + ' ' + cmdVars[name] + elif isinstance(attr, GroupAttribute): + assert isinstance(attr.value, DictModel) + # if the GroupAttribute is not set in a single command line argument, + # the sub-attributes may need to be exposed individually + for v in attr._value: + _buildAttributeCmdVars(cmdVars, v.name, v) + """ Generate command variables using input attributes and resolved output attributes names and values. """ for uidIndex, value in self._uids.items(): self._cmdVars['uid{}'.format(uidIndex)] = value @@ -557,14 +574,7 @@ def _buildCmdVars(self): for name, attr in self._attributes.objects.items(): if attr.isOutput: continue # skip outputs - v = attr.getValueStr() - - self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) - self._cmdVars[name + 'Value'] = str(v) - - if v: - self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ - ' ' + self._cmdVars[name] + _buildAttributeCmdVars(self._cmdVars, name, attr) # For updating output attributes invalidation values cmdVarsNoCache = self._cmdVars.copy() From dcf91c244e6acf6d761f6be7ac5698a0505469ee Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 6 Mar 2020 18:25:00 +0100 Subject: [PATCH 011/100] [nodes] PanoramaInit: fisheyeCenterOffset x/y are now 2 parameters on command line Avoid error with negative values on "y" interpreted as a new argument --- meshroom/nodes/aliceVision/PanoramaInit.py | 5 +++-- meshroom/ui/qml/Viewer/Viewer2D.qml | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index 0d24bc9ae0..d1867dcfb0 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -51,16 +51,17 @@ class PanoramaInit(desc.CommandLineNode): description="Center of the Fisheye circle (XY offset to the center in pixels).", groupDesc=[ desc.FloatParam( - name="x", label="x", description="", + name="fisheyeCenterOffset_x", label="x", description="X Offset in pixels", value=0.0, uid=[0], range=(-1000.0, 10000.0, 1.0)), desc.FloatParam( - name="y", label="y", description="", + name="fisheyeCenterOffset_y", label="y", description="Y Offset in pixels", value=0.0, uid=[0], range=(-1000.0, 10000.0, 1.0)), ], + group=None, # skip group from command line ), desc.FloatParam( name='fisheyeRadius', diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 041cee16cc..18d40df07b 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -242,14 +242,14 @@ FocusScope { anchors.centerIn: parent active: (_reconstruction.panoramaInit && displayFisheyeCircleLoader.checked) sourceComponent: CircleGizmo { - x: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.x").value - y: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.y").value + x: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value + y: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value radius: (imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (_reconstruction.panoramaInit.attribute("fisheyeRadius").value * 0.01) border.width: Math.max(1, (3.0 / imgContainer.scale)) onMoved: { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.x"), x) - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.y"), y) + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) } } } From 0e434908a59f4bffb0850c199e30d6cf9e7f1a1e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 16 Mar 2020 19:58:37 +0100 Subject: [PATCH 012/100] [ui] improve open recent files * fix path conversion on windows * remove invalid paths from the list on error * explicit error message for "No Such File" --- meshroom/core/graph.py | 2 + meshroom/ui/app.py | 58 +++++++++++++++++-- meshroom/ui/graph.py | 10 ++-- .../ui/qml/Viewer/FeaturesInfoOverlay.qml | 2 +- meshroom/ui/qml/main.qml | 16 +++-- meshroom/ui/reconstruction.py | 32 ++++++++-- 6 files changed, 101 insertions(+), 19 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 1b176e8a59..9f73008763 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -280,6 +280,8 @@ def load(self, filepath, setupProjectFile=True): # Create graph edges by resolving attributes expressions self._applyExpr() + return True + @property def updateEnabled(self): return self._updateEnabled diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index fba3425ada..5abe029c53 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -8,6 +8,8 @@ import meshroom from meshroom.core import nodesDesc +from meshroom.core import pyCompatibility + from meshroom.ui import components from meshroom.ui.components.clipboard import ClipboardHelper from meshroom.ui.components.filepath import FilepathHelper @@ -183,8 +185,19 @@ def _recentProjectFiles(self): return projects @Slot(str) + @Slot(QUrl) def addRecentProjectFile(self, projectFile): - projectFile = QUrl(projectFile).path() + if not isinstance(projectFile, (QUrl, pyCompatibility.basestring)): + raise TypeError("Unexpected data type: {}".format(projectFile.__class__)) + if isinstance(projectFile, QUrl): + projectFileNorm = projectFile.toLocalFile() + if not projectFileNorm: + projectFileNorm = projectFile.toString() + else: + projectFileNorm = QUrl(projectFile).toLocalFile() + if not projectFileNorm: + projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile() + projects = self._recentProjectFiles() # remove duplicates while preserving order @@ -192,10 +205,10 @@ def addRecentProjectFile(self, projectFile): uniqueProjects = OrderedDict.fromkeys(projects) projects = list(uniqueProjects) # remove previous usage of the value - if projectFile in uniqueProjects: - projects.remove(projectFile) + if projectFileNorm in uniqueProjects: + projects.remove(projectFileNorm) # add the new value in the first place - projects.insert(0, projectFile) + projects.insert(0, projectFileNorm) # keep only the 10 first elements projects = projects[0:20] @@ -211,6 +224,43 @@ def addRecentProjectFile(self, projectFile): self.recentProjectFilesChanged.emit() + @Slot(str) + @Slot(QUrl) + def removeRecentProjectFile(self, projectFile): + if not isinstance(projectFile, (QUrl, pyCompatibility.basestring)): + raise TypeError("Unexpected data type: {}".format(projectFile.__class__)) + if isinstance(projectFile, QUrl): + projectFileNorm = projectFile.toLocalFile() + if not projectFileNorm: + projectFileNorm = projectFile.toString() + else: + projectFileNorm = QUrl(projectFile).toLocalFile() + if not projectFileNorm: + projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile() + + projects = self._recentProjectFiles() + + # remove duplicates while preserving order + from collections import OrderedDict + uniqueProjects = OrderedDict.fromkeys(projects) + projects = list(uniqueProjects) + # remove previous usage of the value + if projectFileNorm not in uniqueProjects: + return + + projects.remove(projectFileNorm) + + settings = QSettings() + settings.beginGroup("RecentFiles") + size = settings.beginWriteArray("Projects") + for i, p in enumerate(projects): + settings.setArrayIndex(i) + settings.setValue("filepath", p) + settings.endArray() + settings.sync() + + self.recentProjectFilesChanged.emit() + @Slot(str, result=str) def markdownToHtml(self, md): """ diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 566c122c58..f6fb501d5f 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -309,16 +309,14 @@ def stopChildThreads(self): self.stopExecution() self._chunksMonitor.stop() - def load(self, filepath, setupProjectFile=True): + @Slot(str, result=bool) + def loadGraph(self, filepath, setupProjectFile=True): g = Graph('') - g.load(filepath, setupProjectFile) + status = g.load(filepath, setupProjectFile) if not os.path.exists(g.cacheDir): os.mkdir(g.cacheDir) self.setGraph(g) - - @Slot(QUrl) - def loadUrl(self, url): - self.load(url.toLocalFile()) + return status @Slot(QUrl) def saveAs(self, url): diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index 380dd21ecf..909b61bef3 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -75,7 +75,7 @@ FloatingPane { property var viewer: root.featuresViewer.itemAt(index) spacing: 4 - // Visibility toogle + // Visibility toggle MaterialToolButton { text: featureType.viewer.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off onClicked: featureType.viewer.visible = !featureType.viewer.visible diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 75b8cbeb4d..b8e490283e 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -207,8 +207,10 @@ ApplicationWindow { title: "Open File" nameFilters: ["Meshroom Graphs (*.mg)"] onAccepted: { - _reconstruction.loadUrl(file.toString()) - MeshroomApp.addRecentProjectFile(file.toString()) + if(_reconstruction.loadUrl(file)) + { + MeshroomApp.addRecentProjectFile(file.toString()) + } } } @@ -353,8 +355,14 @@ ApplicationWindow { MenuItem { onTriggered: ensureSaved(function() { openRecentMenu.dismiss(); - _reconstruction.load(modelData); - MeshroomApp.addRecentProjectFile(modelData); + if(_reconstruction.loadUrl(modelData)) + { + MeshroomApp.addRecentProjectFile(modelData); + } + else + { + MeshroomApp.removeRecentProjectFile(modelData); + } }) text: fileTextMetrics.elidedText diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 0dcf0bd1b4..f8fe909a43 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -430,10 +430,10 @@ def new(self, pipeline=None): # use the user-provided default photogrammetry project file self.load(p, setupProjectFile=False) - @Slot(str) + @Slot(str, result=bool) def load(self, filepath, setupProjectFile=True): try: - super(Reconstruction, self).load(filepath, setupProjectFile) + status = super(Reconstruction, self).loadGraph(filepath, setupProjectFile) # warn about pre-release projects being automatically upgraded if Version(self._graph.fileReleaseVersion).major == "0": self.warning.emit(Message( @@ -442,17 +442,41 @@ def load(self, filepath, setupProjectFile=True): "Data might have been lost in the process.", "Open it with the corresponding version of Meshroom to recover your data." )) + return status + except FileNotFoundError as e: + self.error.emit( + Message( + "No Such File", + "Error While Loading '{}': No Such File.".format(os.path.basename(filepath)), + "" + ) + ) + logging.error("Error while loading '{}': No Such File.".format(os.path.basename(filepath))) + return False except Exception as e: import traceback trace = traceback.format_exc() self.error.emit( Message( - "Error while loading {}".format(os.path.basename(filepath)), - "An unexpected error has occurred", + "Error While Loading Project File", + "An unexpected error has occurred while loading file: '{}'".format(os.path.basename(filepath)), trace ) ) logging.error(trace) + return False + + @Slot(QUrl, result=bool) + def loadUrl(self, url): + if isinstance(url, (QUrl)): + # depending how the QUrl has been initialized, + # toLocalFile() may return the local path or an empty string + localFile = url.toLocalFile() + if not localFile: + localFile = url.toString() + else: + localFile = url + return self.load(localFile) def onGraphChanged(self): """ React to the change of the internal graph. """ From f9477eadfc9424248e3febd79f4a29c1df4e4100 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 15:13:19 +0100 Subject: [PATCH 013/100] [ui] add Import Images menu --- meshroom/ui/qml/main.qml | 23 ++++++++++++++++++++++- meshroom/ui/reconstruction.py | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index b8e490283e..aa0f5fd40e 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -4,7 +4,10 @@ import QtQuick.Controls 1.4 as Controls1 // For SplitView import QtQuick.Layouts 1.1 import QtQuick.Window 2.3 import QtQml.Models 2.2 + import Qt.labs.platform 1.0 as Platform +import QtQuick.Dialogs 1.3 + import Qt.labs.settings 1.0 import GraphEditor 1.0 import MaterialIcons 2.2 @@ -202,7 +205,7 @@ ApplicationWindow { } } - Platform.FileDialog { + FileDialog { id: openFileDialog title: "Open File" nameFilters: ["Meshroom Graphs (*.mg)"] @@ -214,6 +217,18 @@ ApplicationWindow { } } + FileDialog { + id: importFilesDialog + title: "Import Images" + selectExisting: true + selectMultiple: true + nameFilters: [] + onAccepted: { + console.warn("importFilesDialog fileUrls: " + importFilesDialog.fileUrls) + _reconstruction.importImagesUrls(importFilesDialog.fileUrls) + } + } + AboutDialog { id: aboutDialog } @@ -375,6 +390,12 @@ ApplicationWindow { } } } + Action { + id: importActionItem + text: "Import Images" + shortcut: "Ctrl+I" + onTriggered: importFilesDialog.open() + } Action { id: saveAction text: "Save" diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index f8fe909a43..6a0439cb9c 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -726,10 +726,24 @@ def importImagesFromFolder(self, path, recursive=False): recursive: List files in folders recursively. """ + logging.warning("importImagesFromFolder: " + str(path)) filesByType = multiview.findFilesByTypeInFolder(path, recursive) if filesByType.images: self.buildIntrinsics(self.cameraInit, filesByType.images) + @Slot("QVariant") + def importImagesUrls(self, imagePaths, recursive=False): + paths = [] + for imagePath in imagePaths: + if isinstance(imagePath, (QUrl)): + p = imagePath.toLocalFile() + if not p: + p = imagePath.toString() + else: + p = imagePath + paths.append(p) + self.importImagesFromFolder(paths) + def importImagesAsync(self, images, cameraInit): """ Add the given list of images to the Reconstruction. """ # Start the process of updating views and intrinsics From 6115a810440849dca0e51f4e62988d2c72b89883 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 17:46:15 +0100 Subject: [PATCH 014/100] [ui] GraphEditor fix: Ctrl double click setup node in solo mode --- meshroom/ui/qml/main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index aa0f5fd40e..852d3140b0 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -715,7 +715,7 @@ ApplicationWindow { for(var i=0; i < node.attributes.count; ++i) { var attr = node.attributes.at(i) - if(attr.isOutput && workspaceView.viewAttribute(attr)) + if(attr.isOutput && workspaceView.viewAttribute(attr, mouse)) break; } } From 8e141d7e585dde05ffa91972c6b094d7ee3d266e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 19:52:07 +0100 Subject: [PATCH 015/100] [core] stats: do not break on stats errors Watching the subprocess for statistics may create errors on windows on specific cases, it should not break the UI. --- meshroom/core/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index ec07dbef31..e9a325381f 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -303,7 +303,7 @@ def run(self): if self.proc.is_running(): self.updateStats() return - except (KeyboardInterrupt, SystemError, GeneratorExit): + except (KeyboardInterrupt, SystemError, GeneratorExit, psutil.NoSuchProcess): pass def stopRequest(self): From 1b5ba3788e75a31f08611212dd58cd5c6f787d31 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 19:55:11 +0100 Subject: [PATCH 016/100] [ui] force unload of features & hdr viewers This is necessary since version 5.14 of Qt, otherwise the model is not updated and remains empty. --- meshroom/ui/qml/Viewer/Viewer2D.qml | 49 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 18d40df07b..a31ce106b7 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -151,17 +151,21 @@ FocusScope { 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 hdrImageToolbar.gammaValue; }), - 'offset': Qt.binding(function() { return hdrImageToolbar.offsetValue; }), - 'channelModeString': Qt.binding(function() { return hdrImageToolbar.channelModeValue; }), - }) + onActiveChanged: { + if(active) { + // 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 hdrImageToolbar.gammaValue; }), + 'offset': Qt.binding(function() { return hdrImageToolbar.offsetValue; }), + 'channelModeString': Qt.binding(function() { return hdrImageToolbar.channelModeValue; }), + }) + } else { + // Force the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14 + setSource("", {}) + } } } @@ -207,7 +211,7 @@ FocusScope { scale: 1.0 // FeatureViewer: display view extracted feature points - // note: requires QtAliceVision plugin - use a Loader to evaluate plugin avaibility at runtime + // note: requires QtAliceVision plugin - use a Loader to evaluate plugin availability at runtime Loader { id: featuresViewerLoader @@ -225,14 +229,19 @@ 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", { + '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); }), + }) + } else { + // Force the unload (instead of using Component.onCompleted to load it once and for all) is necessary since Qt 5.14 + setSource("", {}) + } } } From 9843b2967071b631221d5092c74d7653cb0bec65 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 19:58:17 +0100 Subject: [PATCH 017/100] [ui] Viewer2D: ensure that featureViewerLoader is ready FeaturesInfoOverlay relies on the FeatureViewer, so we ensure that it is loaded first. --- meshroom/ui/qml/Viewer/Viewer2D.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index a31ce106b7..69c6688370 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -331,7 +331,7 @@ FocusScope { left: parent.left margins: 2 } - active: displayFeatures.checked + active: displayFeatures.checked && featuresViewerLoader.status === Loader.Ready sourceComponent: FeaturesInfoOverlay { featureExtractionNode: _reconstruction.featureExtraction From 4f443baa1e56a88f9868800e0604a782602233af Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 17 Mar 2020 20:03:01 +0100 Subject: [PATCH 018/100] [ui] Viewer2D: fix navigation with FloatImage The internal MouseArea in the FloatImage is used to retrieve the mouse over events but should not catch the other mouse events. --- meshroom/ui/qml/Viewer/FloatImage.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/Viewer/FloatImage.qml b/meshroom/ui/qml/Viewer/FloatImage.qml index 36bc77cb9f..24809c4da4 100644 --- a/meshroom/ui/qml/Viewer/FloatImage.qml +++ b/meshroom/ui/qml/Viewer/FloatImage.qml @@ -50,5 +50,7 @@ AliceVision.FloatImageViewer { id: mouseArea anchors.fill: parent hoverEnabled: true + // Do not intercept mouse events, only get the mouse over information + acceptedButtons: Qt.NoButton } } From e933e1adf65396595c690f801b8692f84e8f13b0 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 18 Mar 2020 20:21:02 +0100 Subject: [PATCH 019/100] [ui] Float attributes: ensure alignment on the left Ensure that the important part of the number is displayed. --- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 408a4823a4..5aee77bd6e 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -235,6 +235,10 @@ RowLayout { property string displayValue: String(slider.active && slider.item.pressed ? slider.item.formattedValue : attribute.value) text: displayValue selectByMouse: true + // Note: Use autoScroll as a workaround for alignment + // When the value change keep the text align to the left to be able to read the most important part + // of the number. When we are editing (item is in focus), the content should follow the editing. + autoScroll: activeFocus validator: attribute.type == "FloatParam" ? doubleValidator : intValidator onEditingFinished: setTextFieldAttribute(text) onAccepted: { From e88651ba0113053c1f39cec54f2deddda5d6a0c8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 18 Mar 2020 20:23:37 +0100 Subject: [PATCH 020/100] [ui] Viewer2D: allow to change the size of the Fisheye circle with Ctrl+Wheel * also add forceActiveFocus() to release other focus areas --- meshroom/ui/qml/Viewer/CircleGizmo.qml | 28 +++++++++++++++++--------- meshroom/ui/qml/Viewer/Viewer2D.qml | 3 +++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/meshroom/ui/qml/Viewer/CircleGizmo.qml b/meshroom/ui/qml/Viewer/CircleGizmo.qml index b9d6e586b2..bbfcb6f401 100644 --- a/meshroom/ui/qml/Viewer/CircleGizmo.qml +++ b/meshroom/ui/qml/Viewer/CircleGizmo.qml @@ -4,6 +4,7 @@ Rectangle { id: root signal moved() + signal incrementRadius(real radiusOffset) width: radius * 2 height: width @@ -32,21 +33,30 @@ Rectangle { MouseArea { id: mArea anchors.fill: parent - cursorShape: Qt.OpenHandCursor + cursorShape: controlModifierEnabled ? Qt.SizeBDiagCursor : (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + property bool controlModifierEnabled: false + onPositionChanged: { + mArea.controlModifierEnabled = (mouse.modifiers & Qt.ControlModifier) + } acceptedButtons: Qt.LeftButton hoverEnabled: true drag.target: parent - drag.onActiveChanged: - { - if(!drag.active) - { - cursorShape = Qt.OpenHandCursor; + drag.onActiveChanged: { + if(!drag.active) { moved(); } - else - { - cursorShape = Qt.ClosedHandCursor; + } + onPressed: { + forceActiveFocus(); + } + onWheel: { + mArea.controlModifierEnabled = (wheel.modifiers & Qt.ControlModifier) + if (wheel.modifiers & Qt.ControlModifier) { + incrementRadius(wheel.angleDelta.y / 120.0); + wheel.accepted = true; + } else { + wheel.accepted = false; } } } diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 69c6688370..8423603060 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -260,6 +260,9 @@ FocusScope { _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) } + onIncrementRadius: { + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeRadius"), _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radiusOffset) + } } } } From 7b8405c5f6d9e8b00b5c496b2dd0f4e2d10c1b4d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 20 Mar 2020 17:57:16 +0100 Subject: [PATCH 021/100] [ui] new pipeline hdriFisheye Setup the 2 fisheye options on LDRToHDR and PanoramaInit nodes. --- meshroom/multiview.py | 10 ++++++++++ meshroom/ui/qml/main.qml | 4 ++++ meshroom/ui/reconstruction.py | 3 +++ 3 files changed, 17 insertions(+) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 9d12eaf8d8..4d6ae6f752 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -117,6 +117,16 @@ def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), out return graph +def hdriFisheye(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output='', graph=None): + if not graph: + graph = Graph('HDRI-Fisheye') + with GraphModification(graph): + hdri(inputImages, inputViewpoints, inputIntrinsics, output, graph) + for ldrToHdr in graph.nodesByType("LDRToHDR"): + ldrToHdr.attribute("fisheyeLens").value = True + for panoramaInit in graph.nodesByType("PanoramaInit"): + panoramaInit.attribute("useFisheye").value = True + return graph def hdriPipeline(graph): """ diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 852d3140b0..32d92f0659 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -343,6 +343,10 @@ ApplicationWindow { text: "HDRI" onTriggered: ensureSaved(function() { _reconstruction.new("hdri") }) } + Action { + text: "HDRI Fisheye" + onTriggered: ensureSaved(function() { _reconstruction.new("hdriFisheye") }) + } } Action { id: openActionItem diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 6a0439cb9c..bd3fb218c5 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -426,6 +426,9 @@ def new(self, pipeline=None): elif p.lower() == "hdri": # default hdri pipeline self.setGraph(multiview.hdri()) + elif p.lower() == "hdrifisheye": + # default hdri pipeline + self.setGraph(multiview.hdriFisheye()) else: # use the user-provided default photogrammetry project file self.load(p, setupProjectFile=False) From b98c1e57edb9c0f2b5ca3253fd4c64baabc693ac Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 20 Mar 2020 17:59:32 +0100 Subject: [PATCH 022/100] [ui] Viewer2D: fisheye circle is enabled only when the option is activated --- meshroom/ui/qml/Viewer/Viewer2D.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 8423603060..23c3077289 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -253,7 +253,8 @@ FocusScope { sourceComponent: CircleGizmo { x: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value y: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value - radius: (imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (_reconstruction.panoramaInit.attribute("fisheyeRadius").value * 0.01) + property real fisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radius: (imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (fisheyeRadius * 0.01) border.width: Math.max(1, (3.0 / imgContainer.scale)) onMoved: { @@ -411,8 +412,8 @@ FocusScope { Layout.minimumWidth: 0 checkable: true checked: false - enabled: _reconstruction.panoramaInit - visible: enabled + enabled: _reconstruction.panoramaInit && _reconstruction.panoramaInit.attribute("useFisheye").value + visible: _reconstruction.panoramaInit } Label { From 4c736e8512a47682d16a06515b8d42456ecf3a2f Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 23 Mar 2020 19:21:53 +0100 Subject: [PATCH 023/100] [core] Node.isComputed is now a property --- meshroom/core/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index f296c05a6d..51a96f64d9 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -616,8 +616,7 @@ def hasStatus(self, status): return False return True - @Slot(result=bool) - def isComputed(self): + def _isComputed(self): return self.hasStatus(Status.SUCCESS) @Slot() @@ -783,6 +782,7 @@ def __repr__(self): size = Property(int, getSize, notify=sizeChanged) globalStatusChanged = Signal() globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) + isComputed = Property(bool, lambda self: self._isComputed(), notify=globalStatusChanged) class Node(BaseNode): From 4207462c4e2cb8a86e368dc1d89d33f689f03905 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 23 Mar 2020 19:23:12 +0100 Subject: [PATCH 024/100] [ui] Reconstruction: set default ldr2hdr node on double click --- meshroom/ui/reconstruction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index bd3fb218c5..0520af48e9 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -886,6 +886,8 @@ def setActiveNodeOfType(self, node): self.prepareDenseScene = node elif node.nodeType in ("DepthMap", "DepthMapFilter"): self.depthMap = node + elif node.nodeType == "LDRToHDR": + self.ldr2hdr = node elif node.nodeType == "PanoramaInit": self.panoramaInit = node From b7b1f49bc5aa978790b3d3e3dd35c6951368eb13 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 23 Mar 2020 19:24:44 +0100 Subject: [PATCH 025/100] [ui] ImageGallery HDR option: update when node is computed --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index d4dcd759ce..e50f76660f 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -353,10 +353,10 @@ Panel { padding: 0 anchors.margins: 0 implicitHeight: 14 - ToolTip.text: "Visualize HDR images" + ToolTip.text: "Visualize HDR images: " + (_reconstruction.ldr2hdr ? _reconstruction.ldr2hdr.label : "No Node") text: MaterialIcons.hdr_on visible: _reconstruction.ldr2hdr - enabled: visible && _reconstruction.ldr2hdr.isComputed() + enabled: _reconstruction.ldr2hdr && _reconstruction.ldr2hdr.isComputed onEnabledChanged: { // Reset the toggle to avoid getting stuck // with the HDR node checked but disabled. From bb60c9b78dd1f94d865c11d73bc3cbd96089cdc8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 23 Mar 2020 19:25:57 +0100 Subject: [PATCH 026/100] [ui] Add visualisation for automatically detected fisheye circle --- meshroom/nodes/aliceVision/PanoramaInit.py | 7 +++++ meshroom/ui/qml/Viewer/CircleGizmo.qml | 23 ++++++++++++++-- meshroom/ui/qml/Viewer/Viewer2D.qml | 31 +++++++++++++++------- meshroom/ui/reconstruction.py | 23 ++++++++++++++++ 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index d1867dcfb0..5d3a4c076c 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -45,6 +45,13 @@ class PanoramaInit(desc.CommandLineNode): value=False, uid=[0], ), + desc.BoolParam( + name='estimateFisheyeCircle', + label='Estimate Fisheye Circle', + description='Automatically estimate the Fisheye Circle center and radius instead of using user values.', + value=True, + uid=[0], + ), desc.GroupAttribute( name="fisheyeCenterOffset", label="Fisheye Center", diff --git a/meshroom/ui/qml/Viewer/CircleGizmo.qml b/meshroom/ui/qml/Viewer/CircleGizmo.qml index bbfcb6f401..1a48c22e4d 100644 --- a/meshroom/ui/qml/Viewer/CircleGizmo.qml +++ b/meshroom/ui/qml/Viewer/CircleGizmo.qml @@ -3,6 +3,8 @@ import QtQuick 2.11 Rectangle { id: root + property bool readOnly: false + signal moved() signal incrementRadius(real radiusOffset) @@ -10,7 +12,20 @@ Rectangle { height: width color: "transparent" border.width: 5 - border.color: "yellow" + border.color: readOnly ? "green" : "yellow" + + Rectangle { + color: parent.color + anchors.centerIn: parent + width: 20 + height: 2 + } + Rectangle { + color: parent.color + anchors.centerIn: parent + width: 2 + height: 20 + } Behavior on x { NumberAnimation { @@ -32,11 +47,15 @@ Rectangle { MouseArea { id: mArea + enabled: !root.readOnly anchors.fill: parent - cursorShape: controlModifierEnabled ? Qt.SizeBDiagCursor : (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + cursorShape: root.readOnly ? Qt.ArrowCursor : (controlModifierEnabled ? Qt.SizeBDiagCursor : (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor)) + propagateComposedEvents: true + property bool controlModifierEnabled: false onPositionChanged: { mArea.controlModifierEnabled = (mouse.modifiers & Qt.ControlModifier) + mouse.accepted = false; } acceptedButtons: Qt.LeftButton hoverEnabled: true diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 23c3077289..3a3943953b 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -249,20 +249,31 @@ FocusScope { // note: use a Loader to evaluate if a PanoramaInit node exist and displayFisheyeCircle checked at runtime Loader { anchors.centerIn: parent - active: (_reconstruction.panoramaInit && displayFisheyeCircleLoader.checked) + active: (displayFisheyeCircleLoader.checked && _reconstruction.panoramaInit) sourceComponent: CircleGizmo { - x: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value - y: _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value - property real fisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value - radius: (imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (fisheyeRadius * 0.01) - border.width: Math.max(1, (3.0 / imgContainer.scale)) + property bool useAuto: _reconstruction.panoramaInit.attribute("estimateFisheyeCircle").value + readOnly: useAuto + visible: (!useAuto) || _reconstruction.panoramaInit.isComputed + property real userFisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value + property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(_reconstruction.panoramaInit) + + x: useAuto ? (fisheyeAutoParams.x - imgContainer.image.width * 0.5) : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value + y: useAuto ? (fisheyeAutoParams.y - imgContainer.image.height * 0.5) : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value + radius: useAuto ? fisheyeAutoParams.z : ((imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (userFisheyeRadius * 0.01)) + border.width: Math.max(1, (3.0 / imgContainer.scale)) onMoved: { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) + if(!useAuto) + { + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) + } } onIncrementRadius: { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeRadius"), _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radiusOffset) + if(!useAuto) + { + _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeRadius"), _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radiusOffset) + } } } } @@ -406,7 +417,7 @@ FocusScope { } MaterialToolButton { id: displayFisheyeCircleLoader - ToolTip.text: "Display Fisheye Circle" + ToolTip.text: "Display Fisheye Circle: " + (_reconstruction.panoramaInit ? _reconstruction.panoramaInit.label : "No Node") text: MaterialIcons.panorama_fish_eye font.pointSize: 11 Layout.minimumWidth: 0 diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 0520af48e9..20d5e90c88 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -559,6 +559,29 @@ def setupLDRToHDRCameraInit(self): tmpCameraInit = Node("CameraInit", viewpoints=views, intrinsics=intrinsics) self.hdrCameraInit = tmpCameraInit + @Slot(QObject, result=QVector3D) + def getAutoFisheyeCircle(self, panoramaInit): + if not panoramaInit or not panoramaInit.isComputed: + return QVector3D(0.0, 0.0, 0.0) + if not panoramaInit.attribute("estimateFisheyeCircle").value: + return QVector3D(0.0, 0.0, 0.0) + + sfmFile = panoramaInit.attribute('outSfMDataFilename').value + if not os.path.exists(sfmFile): + return QVector3D(0.0, 0.0, 0.0) + import io # use io.open for Python2/3 compatibility (allow to specify encoding + errors handling) + # skip decoding errors to avoid potential exceptions due to non utf-8 characters in images metadata + with io.open(sfmFile, 'r', encoding='utf-8', errors='ignore') as f: + data = json.load(f) + + intrinsics = data.get('intrinsics', []) + if len(intrinsics) == 0: + return QVector3D(0.0, 0.0, 0.0) + intrinsic = intrinsics[0] + + res = QVector3D(float(intrinsic.get("fisheyeCenterX", 0.0)), float(intrinsic.get("fisheyeCenterY", 0.0)), float(intrinsic.get("fisheyeRadius", 0.0))) + return res + def updatePanoramaInitNode(self): """ Set the current FeatureExtraction node based on the current CameraInit node. """ self.panoramaInit = self.lastNodeOfType('PanoramaInit', self.cameraInit) if self.cameraInit else None From f19797c58f1ad52bddb006d2b2a39085b37a1c7e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 24 Mar 2020 16:03:01 +0100 Subject: [PATCH 027/100] [ui] Viewer2D: fix Circle Viewer when image has an orientation --- meshroom/ui/qml/Viewer/CircleGizmo.qml | 26 ++++++++++++++++++++------ meshroom/ui/qml/Viewer/Viewer2D.qml | 15 +++++++++++++-- meshroom/ui/reconstruction.py | 4 +++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/meshroom/ui/qml/Viewer/CircleGizmo.qml b/meshroom/ui/qml/Viewer/CircleGizmo.qml index 1a48c22e4d..c8d4f879ab 100644 --- a/meshroom/ui/qml/Viewer/CircleGizmo.qml +++ b/meshroom/ui/qml/Viewer/CircleGizmo.qml @@ -14,17 +14,31 @@ Rectangle { border.width: 5 border.color: readOnly ? "green" : "yellow" + /* + // visualize top-left corner for debugging purpose Rectangle { - color: parent.color + color: "red" + width: 500 + height: 50 + } + Rectangle { + color: "red" + width: 50 + height: 500 + } + */ + // Cross to visualize the circle center + Rectangle { + color: parent.border.color anchors.centerIn: parent - width: 20 - height: 2 + width: parent.width * 0.2 + height: parent.border.width * 0.5 } Rectangle { - color: parent.color + color: parent.border.color anchors.centerIn: parent - width: 2 - height: 20 + width: parent.border.width * 0.5 + height: parent.height * 0.2 } Behavior on x { diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 3a3943953b..b3c2c24ab7 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -250,6 +250,17 @@ FocusScope { Loader { anchors.centerIn: parent active: (displayFisheyeCircleLoader.checked && _reconstruction.panoramaInit) + + // 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; + } + } + sourceComponent: CircleGizmo { property bool useAuto: _reconstruction.panoramaInit.attribute("estimateFisheyeCircle").value readOnly: useAuto @@ -257,8 +268,8 @@ FocusScope { property real userFisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(_reconstruction.panoramaInit) - x: useAuto ? (fisheyeAutoParams.x - imgContainer.image.width * 0.5) : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value - y: useAuto ? (fisheyeAutoParams.y - imgContainer.image.height * 0.5) : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value + x: useAuto ? fisheyeAutoParams.x : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value + y: useAuto ? fisheyeAutoParams.y : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value radius: useAuto ? fisheyeAutoParams.z : ((imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (userFisheyeRadius * 0.01)) border.width: Math.max(1, (3.0 / imgContainer.scale)) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 20d5e90c88..a94e359c51 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -579,7 +579,9 @@ def getAutoFisheyeCircle(self, panoramaInit): return QVector3D(0.0, 0.0, 0.0) intrinsic = intrinsics[0] - res = QVector3D(float(intrinsic.get("fisheyeCenterX", 0.0)), float(intrinsic.get("fisheyeCenterY", 0.0)), float(intrinsic.get("fisheyeRadius", 0.0))) + res = QVector3D(float(intrinsic.get("fisheyeCenterX", 0.0)) - float(intrinsic.get("width", 0.0)) * 0.5, + float(intrinsic.get("fisheyeCenterY", 0.0)) - float(intrinsic.get("height", 0.0)) * 0.5, + float(intrinsic.get("fisheyeRadius", 0.0))) return res def updatePanoramaInitNode(self): From 9ef6eb48fcf5487847dec43d68cd55a6a37bae72 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 24 Mar 2020 21:12:37 +0100 Subject: [PATCH 028/100] [ui] Viewer Fisheye Circle: disable MouseArea when unused As the events cannot be propagated all the time as needed, at least unload the MouseArea when unused so the events are propagated. --- meshroom/ui/qml/Viewer/CircleGizmo.qml | 58 ++++++++++++++------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/meshroom/ui/qml/Viewer/CircleGizmo.qml b/meshroom/ui/qml/Viewer/CircleGizmo.qml index c8d4f879ab..0b7d9e9814 100644 --- a/meshroom/ui/qml/Viewer/CircleGizmo.qml +++ b/meshroom/ui/qml/Viewer/CircleGizmo.qml @@ -59,37 +59,41 @@ Rectangle { } } - MouseArea { - id: mArea - enabled: !root.readOnly + Loader { anchors.fill: parent - cursorShape: root.readOnly ? Qt.ArrowCursor : (controlModifierEnabled ? Qt.SizeBDiagCursor : (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor)) - propagateComposedEvents: true + active: !root.readOnly - property bool controlModifierEnabled: false - onPositionChanged: { - mArea.controlModifierEnabled = (mouse.modifiers & Qt.ControlModifier) - mouse.accepted = false; - } - acceptedButtons: Qt.LeftButton - hoverEnabled: true - drag.target: parent + sourceComponent: MouseArea { + id: mArea + anchors.fill: parent + cursorShape: root.readOnly ? Qt.ArrowCursor : (controlModifierEnabled ? Qt.SizeBDiagCursor : (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor)) + propagateComposedEvents: true - drag.onActiveChanged: { - if(!drag.active) { - moved(); + property bool controlModifierEnabled: false + onPositionChanged: { + mArea.controlModifierEnabled = (mouse.modifiers & Qt.ControlModifier) + mouse.accepted = false; } - } - onPressed: { - forceActiveFocus(); - } - onWheel: { - mArea.controlModifierEnabled = (wheel.modifiers & Qt.ControlModifier) - if (wheel.modifiers & Qt.ControlModifier) { - incrementRadius(wheel.angleDelta.y / 120.0); - wheel.accepted = true; - } else { - wheel.accepted = false; + acceptedButtons: Qt.LeftButton + hoverEnabled: true + drag.target: root + + drag.onActiveChanged: { + if(!drag.active) { + moved(); + } + } + onPressed: { + forceActiveFocus(); + } + onWheel: { + mArea.controlModifierEnabled = (wheel.modifiers & Qt.ControlModifier) + if (wheel.modifiers & Qt.ControlModifier) { + incrementRadius(wheel.angleDelta.y / 120.0); + wheel.accepted = true; + } else { + wheel.accepted = false; + } } } } From 1a968ca4beaa70de1abe6ba3133b8b520e5ddf5b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 25 Mar 2020 17:22:15 +0100 Subject: [PATCH 029/100] [ui] FisheyeCircle: var name update in the json file --- meshroom/ui/reconstruction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index a94e359c51..0fcc176d2c 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -579,9 +579,9 @@ def getAutoFisheyeCircle(self, panoramaInit): return QVector3D(0.0, 0.0, 0.0) intrinsic = intrinsics[0] - res = QVector3D(float(intrinsic.get("fisheyeCenterX", 0.0)) - float(intrinsic.get("width", 0.0)) * 0.5, - float(intrinsic.get("fisheyeCenterY", 0.0)) - float(intrinsic.get("height", 0.0)) * 0.5, - float(intrinsic.get("fisheyeRadius", 0.0))) + res = QVector3D(float(intrinsic.get("fisheyeCircleCenterX", 0.0)) - float(intrinsic.get("width", 0.0)) * 0.5, + float(intrinsic.get("fisheyeCircleCenterY", 0.0)) - float(intrinsic.get("height", 0.0)) * 0.5, + float(intrinsic.get("fisheyeCircleRadius", 0.0))) return res def updatePanoramaInitNode(self): From 5e9f7a32a956c8fd0595055e5ca657ddff5726d9 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 10:50:09 +0100 Subject: [PATCH 030/100] [nodes] PanoramaEstimation: remove unused param "orientation" --- meshroom/nodes/aliceVision/PanoramaEstimation.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index 460a6a329b..a2117daf5d 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -53,15 +53,6 @@ class PanoramaEstimation(desc.CommandLineNode): uid=[0], joinChar=',', ), - desc.IntParam( - name='orientation', - label='Orientation', - description='Orientation', - value=0, - range=(0, 6, 1), - uid=[0], - advanced=True, - ), desc.FloatParam( name='offsetLongitude', label='Longitude offset (deg.)', From 2ebcf19382e6e82e4a60753549d51c317835ea05 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 10:50:55 +0100 Subject: [PATCH 031/100] [nodes] remove old HDRIStitching node --- meshroom/nodes/aliceVision/HDRIstitching.py | 89 --------------------- 1 file changed, 89 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/HDRIstitching.py diff --git a/meshroom/nodes/aliceVision/HDRIstitching.py b/meshroom/nodes/aliceVision/HDRIstitching.py deleted file mode 100644 index af81410eec..0000000000 --- a/meshroom/nodes/aliceVision/HDRIstitching.py +++ /dev/null @@ -1,89 +0,0 @@ -__version__ = "1.0" - -from meshroom.core import desc - - -class HDRIstitching(desc.CommandLineNode): - commandLine = 'aliceVision_utils_fisheyeProjection {allParams}' - - inputs = [ - desc.ListAttribute( - elementDesc=desc.File( - name='inputFile', - label='Input File/Folder', - description="", - value='', - uid=[0], - ), - name='input', - label='Input Folder', - description="List of fisheye images or folder containing them." - ), - desc.FloatParam( - name='blurWidth', - label='Blur Width', - description="Blur width of alpha channel for all fisheye (between 0 and 1). \n" - "Determine the transitions sharpness.", - value=0.2, - range=(0, 1, 0.1), - uid=[0], - ), - desc.ListAttribute( - elementDesc=desc.FloatParam( - name='imageXRotation', - label='Image X Rotation', - description="", - value=0, - range=(-20, 20, 1), - uid=[0], - ), - name='xRotation', - label='X Rotations', - description="Rotations in degree on axis X (horizontal axis) for each image.", - ), - desc.ListAttribute( - elementDesc=desc.FloatParam( - name='imageYRotation', - label='Image Y Rotation', - description="", - value=0, - range=(-30, 30, 5), - uid=[0], - ), - name='yRotation', - label='Y Rotations', - description="Rotations in degree on axis Y (vertical axis) for each image.", - ), - desc.ListAttribute( - elementDesc=desc.FloatParam( - name='imageZRotation', - label='Image Z Rotation', - description="", - value=0, - range=(-10, 10, 1), - uid=[0], - ), - name='zRotation', - label='Z Rotations', - description="Rotations in degree on axis Z (depth axis) for each image.", - ), - desc.ChoiceParam( - name='verboseLevel', - label='Verbose Level', - description="Verbosity level (fatal, error, warning, info, debug, trace).", - value='info', - values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], - exclusive=True, - uid=[], - ), - ] - - outputs = [ - desc.File( - name='output', - label='Output Panorama', - description="Output folder for panorama", - value=desc.Node.internalFolder, - uid=[], - ), - ] \ No newline at end of file From 0e606eef4e03f00efa7a0abcbffa73a9d6f88c8c Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 10:54:00 +0100 Subject: [PATCH 032/100] [ui] Add the notion of Node Documentation --- meshroom/core/desc.py | 1 + meshroom/core/node.py | 4 ++ .../ui/qml/GraphEditor/NodeDocumentation.qml | 38 +++++++++++++++++++ meshroom/ui/qml/GraphEditor/NodeEditor.qml | 12 ++++++ 4 files changed, 55 insertions(+) create mode 100644 meshroom/ui/qml/GraphEditor/NodeDocumentation.qml diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 7618fa671f..7196a45953 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -397,6 +397,7 @@ class Node(object): outputs = [] size = StaticNodeSize(1) parallelization = None + documentation = '' def __init__(self): pass diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 51a96f64d9..dc8caa6a3b 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -474,6 +474,9 @@ def getLabel(self): t, idx = self._name.split("_") return "{}{}".format(t, idx if int(idx) > 1 else "") + def getDocumentation(self): + return self.nodeDesc.documentation + @property def packageFullName(self): return '-'.join([self.packageName, self.packageVersion]) @@ -766,6 +769,7 @@ def __repr__(self): name = Property(str, getName, constant=True) label = Property(str, getLabel, constant=True) nodeType = Property(str, nodeType.fget, constant=True) + documentation = Property(str, getDocumentation, constant=True) positionChanged = Signal() position = Property(Variant, position.fget, position.fset, notify=positionChanged) x = Property(float, lambda self: self._position.x, notify=positionChanged) diff --git a/meshroom/ui/qml/GraphEditor/NodeDocumentation.qml b/meshroom/ui/qml/GraphEditor/NodeDocumentation.qml new file mode 100644 index 0000000000..337f3e3711 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/NodeDocumentation.qml @@ -0,0 +1,38 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import Controls 1.0 + +import "common.js" as Common + +/** + * Displays Node documentation + */ +FocusScope { + id: root + + property variant node + + SystemPalette { id: activePalette } + + ScrollView { + width: parent.width + height: parent.height + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + clip: true + + TextEdit { + width: parent.parent.width + height: parent.height + + padding: 8 + textFormat: TextEdit.MarkdownText + selectByMouse: true + selectionColor: activePalette.highlight + color: activePalette.text + text: node.documentation + wrapMode: TextEdit.Wrap + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index b0d1041d24..4cc96ae6d9 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -148,6 +148,12 @@ Panel { chunkCurrentIndex: m.chunkCurrentIndex onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } } + + NodeDocumentation { + id: nodeDocumentation + Layout.fillWidth: true + node: root.node + } } } } @@ -185,6 +191,12 @@ Panel { leftPadding: 8 rightPadding: leftPadding } + TabButton { + text: "Documentation" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } } } } From 64a4c9426ad204eaf261758ab0c1cf4fed886886 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 11:20:44 +0100 Subject: [PATCH 033/100] [nodes] add some nodes documentation --- meshroom/nodes/aliceVision/CameraInit.py | 46 +++++++++++++--- .../nodes/aliceVision/ConvertSfMFormat.py | 7 ++- meshroom/nodes/aliceVision/DepthMap.py | 10 ++++ meshroom/nodes/aliceVision/DepthMapFilter.py | 5 ++ .../nodes/aliceVision/ExportAnimatedCamera.py | 5 ++ meshroom/nodes/aliceVision/ExportMaya.py | 7 +++ .../nodes/aliceVision/FeatureExtraction.py | 20 +++++++ meshroom/nodes/aliceVision/FeatureMatching.py | 22 ++++++++ meshroom/nodes/aliceVision/GlobalSfM.py | 5 ++ meshroom/nodes/aliceVision/ImageMatching.py | 22 ++++++++ .../aliceVision/ImageMatchingMultiSfM.py | 8 +++ .../nodes/aliceVision/KeyframeSelection.py | 7 +++ meshroom/nodes/aliceVision/LDRToHDR.py | 28 +++++++--- meshroom/nodes/aliceVision/MeshDecimate.py | 4 ++ meshroom/nodes/aliceVision/MeshDenoising.py | 5 ++ meshroom/nodes/aliceVision/MeshFiltering.py | 5 ++ meshroom/nodes/aliceVision/MeshResampling.py | 4 ++ meshroom/nodes/aliceVision/Meshing.py | 11 ++++ .../nodes/aliceVision/PanoramaCompositing.py | 17 +++++- .../nodes/aliceVision/PanoramaEstimation.py | 4 ++ meshroom/nodes/aliceVision/PanoramaInit.py | 12 +++++ meshroom/nodes/aliceVision/PanoramaWarping.py | 7 ++- .../nodes/aliceVision/PrepareDenseScene.py | 4 ++ meshroom/nodes/aliceVision/Publish.py | 5 ++ meshroom/nodes/aliceVision/SfMAlignment.py | 12 +++++ meshroom/nodes/aliceVision/SfMTransfer.py | 4 ++ meshroom/nodes/aliceVision/SfMTransform.py | 12 +++++ meshroom/nodes/aliceVision/SketchfabUpload.py | 5 ++ .../nodes/aliceVision/StructureFromMotion.py | 53 +++++++++++++++++++ meshroom/nodes/aliceVision/Texturing.py | 14 +++++ 30 files changed, 352 insertions(+), 18 deletions(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 23817b4005..bc2814a613 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -16,20 +16,39 @@ desc.IntParam(name="intrinsicId", label="Intrinsic", description="Internal Camera Parameters", value=-1, uid=[0], range=None), desc.IntParam(name="rigId", label="Rig", description="Rig Parameters", value=-1, uid=[0], range=None), desc.IntParam(name="subPoseId", label="Rig Sub-Pose", description="Rig Sub-Pose Parameters", value=-1, uid=[0], range=None), - desc.StringParam(name="metadata", label="Image Metadata", description="", value="", uid=[], advanced=True), + desc.StringParam(name="metadata", label="Image Metadata", + description="The configuration of the Viewpoints is based on the images metadata.\n" + "The important ones are:\n" + " * Focal Length: the focal length in mm.\n" + " * Make and Model: this information allows to convert the focal in mm into a focal length in pixel using an embedded sensor database.\n" + " * Serial Number: allows to uniquely identify a device so multiple devices with the same Make, Model can be differentiated and their internal parameters are optimized separately.", + value="", uid=[], advanced=True), ] Intrinsic = [ desc.IntParam(name="intrinsicId", label="Id", description="Intrinsic UID", value=-1, uid=[0], range=None), - desc.FloatParam(name="pxInitialFocalLength", label="Initial Focal Length", description="Initial Guess on the Focal Length", value=-1.0, uid=[0], range=None), - desc.FloatParam(name="pxFocalLength", label="Focal Length", description="Known/Calibrated Focal Length", value=-1.0, uid=[0], range=None), - desc.ChoiceParam(name="type", label="Camera Type", description="Camera Type", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'equidistant_r3'], exclusive=True, uid=[0]), + desc.FloatParam(name="pxInitialFocalLength", label="Initial Focal Length", + description="Initial Guess on the Focal Length (in pixels). \n" + "When we have an initial value from EXIF, this value is not accurate but cannot be wrong. \n" + "So this value is used to limit the range of possible values in the optimization. \n" + "If you put -1, this value will not be used and the focal length will not be bounded.", + value=-1.0, uid=[0], range=None), + desc.FloatParam(name="pxFocalLength", label="Focal Length", description="Known/Calibrated Focal Length (in pixels)", value=-1.0, uid=[0], range=None), + desc.ChoiceParam(name="type", label="Camera Type", + description="Mathematical Model used to represent a camera:\n" + " * pinhole: Simplest projective camera model without optical distortion (focal and optical center).\n" + " * radial1: Pinhole camera with one radial distortion parameter\n" + " * radial3: Pinhole camera with 3 radial distortion parameters\n" + " * brown: Pinhole camera with 3 radial and 2 tangential distortion parameters\n" + " * fisheye4: Pinhole camera with 4 distortion parameters suited for fisheye optics (like 120° FoV)\n" + " * equidistant_r3: Non-projective camera model suited for full-fisheye optics (like 180° FoV)\n", + value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'equidistant_r3'], exclusive=True, uid=[0]), desc.IntParam(name="width", label="Width", description="Image Width", value=0, uid=[], range=(0, 10000, 1)), desc.IntParam(name="height", label="Height", description="Image Height", value=0, uid=[], range=(0, 10000, 1)), desc.FloatParam(name="sensorWidth", label="Sensor Width", description="Sensor Width (mm)", value=36, uid=[], range=(0, 1000, 1)), desc.FloatParam(name="sensorHeight", label="Sensor Height", description="Sensor Height (mm)", value=24, uid=[], range=(0, 1000, 1)), - desc.StringParam(name="serialNumber", label="Serial Number", description="Device Serial Number (camera and lens combined)", value="", uid=[]), - desc.GroupAttribute(name="principalPoint", label="Principal Point", description="", groupDesc=[ + desc.StringParam(name="serialNumber", label="Serial Number", description="Device Serial Number (Camera UID and Lens UID combined)", value="", uid=[]), + desc.GroupAttribute(name="principalPoint", label="Principal Point", description="Position of the Optical Center in the Image (i.e. the sensor surface).", groupDesc=[ desc.FloatParam(name="x", label="x", description="", value=0, uid=[], range=(0, 10000, 1)), desc.FloatParam(name="y", label="y", description="", value=0, uid=[], range=(0, 10000, 1)), ]), @@ -96,6 +115,21 @@ class CameraInit(desc.CommandLineNode): size = desc.DynamicNodeSize('viewpoints') + documentation = ''' +This node describes your dataset. It lists the Viewpoints candidates, the guess about the type of optic, the initial focal length +and which images are sharing the same internal camera parameters, as well as potential cameras rigs. + +When you import new images into Meshroom, this node is automatically configured from the analysis of the image metadata. +The software can support images without any metadata but it is recommended to have them for robustness. + +### Metadata +Metadata allows images to be grouped together and provides an initialization of the focal length (in pixel unit). +The metadata needed are: + * **Focal Length**: the focal length in mm. + * **Make** & **Model**: this information allows to convert the focal in mm into a focal length in pixel using an embedded sensor database. + * **Serial Number**: allows to uniquely identify a device so multiple devices with the same Make, Model can be differentiated and their internal parameters are optimized separately (in the photogrammetry case). +''' + inputs = [ desc.ListAttribute( name="viewpoints", diff --git a/meshroom/nodes/aliceVision/ConvertSfMFormat.py b/meshroom/nodes/aliceVision/ConvertSfMFormat.py index 5260a59b79..2ffc80225b 100644 --- a/meshroom/nodes/aliceVision/ConvertSfMFormat.py +++ b/meshroom/nodes/aliceVision/ConvertSfMFormat.py @@ -6,7 +6,12 @@ class ConvertSfMFormat(desc.CommandLineNode): commandLine = 'aliceVision_convertSfMFormat {allParams}' size = desc.DynamicNodeSize('input') - + + documentation = ''' +Convert an SfM scene from one file format to another. +It can also be used to remove specific parts of from an SfM scene (like filter all 3D landmarks or filter 2D observations). +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index 24fa430009..3c4596226a 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -10,6 +10,16 @@ class DepthMap(desc.CommandLineNode): parallelization = desc.Parallelization(blockSize=3) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +For each camera that have been estimated by the Structure-From-Motion, it estimates the depth value per pixel. + +Adjust the downscale factor to compute depth maps at a higher/lower resolution. +Use a downscale factor of one (full-resolution) only if the quality of the input images is really high (camera on a tripod with high-quality optics). + +## Online +[https://alicevision.org/#photogrammetry/depth_maps_estimation](https://alicevision.org/#photogrammetry/depth_maps_estimation) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index a4f2ed5d14..7dd0eb0448 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -10,6 +10,11 @@ class DepthMapFilter(desc.CommandLineNode): parallelization = desc.Parallelization(blockSize=10) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +Filter depth map values that are not coherent in multiple depth maps. +This allows to filter unstable points before starting the fusion of all depth maps in the Meshing node. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/ExportAnimatedCamera.py b/meshroom/nodes/aliceVision/ExportAnimatedCamera.py index b8d97557aa..eefeb48900 100644 --- a/meshroom/nodes/aliceVision/ExportAnimatedCamera.py +++ b/meshroom/nodes/aliceVision/ExportAnimatedCamera.py @@ -6,6 +6,11 @@ class ExportAnimatedCamera(desc.CommandLineNode): commandLine = 'aliceVision_exportAnimatedCamera {allParams}' + documentation = ''' +Convert cameras from an SfM scene into an animated cameras in Alembic file format. +Based on the input image filenames, it will recognize the input video sequence to create an animated camera. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/ExportMaya.py b/meshroom/nodes/aliceVision/ExportMaya.py index 41568f5df1..9328852685 100644 --- a/meshroom/nodes/aliceVision/ExportMaya.py +++ b/meshroom/nodes/aliceVision/ExportMaya.py @@ -6,6 +6,13 @@ class ExportMaya(desc.CommandLineNode): commandLine = 'aliceVision_exportMeshroomMaya {allParams}' + documentation = ''' +Export a scene for Autodesk Maya, with an Alembic file describing the SfM: cameras and 3D points. +It will export half-size undistorted images to use as image planes for cameras and also export thumbnails. +Use the MeshroomMaya plugin, to load the ABC file. It will recognize the file structure and will setup the scene. +MeshroomMaya contains a user interface to browse all cameras. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index e5c1cfd4cf..a18537787a 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -9,6 +9,26 @@ class FeatureExtraction(desc.CommandLineNode): parallelization = desc.Parallelization(blockSize=40) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +This node extracts distinctive groups of pixels that are, to some extent, invariant to changing camera viewpoints during image acquisition. +Hence, a feature in the scene should have similar feature descriptions in all images. + +This node implements multiple methods: + * **SIFT** +The most standard method. This is the default and recommended value for all use cases. + * **AKAZE** +AKAZE can be interesting solution to extract features in challenging condition. It could be able to match wider angle than SIFT but has drawbacks. +It may extract to many features, the repartition is not always good. +It is known to be good on challenging surfaces such as skin. + * **CCTAG** +CCTag is a marker type with 3 or 4 crowns. You can put markers in the scene during the shooting session to automatically re-orient and re-scale the scene to a known size. +It is robust to motion-blur, depth-of-field, occlusion. Be careful to have enough white margin around your CCTags. + + +## Online +[https://alicevision.org/#photogrammetry/natural_feature_extraction](https://alicevision.org/#photogrammetry/natural_feature_extraction) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/FeatureMatching.py b/meshroom/nodes/aliceVision/FeatureMatching.py index 0b4c00d7f9..f86ff7b7c6 100644 --- a/meshroom/nodes/aliceVision/FeatureMatching.py +++ b/meshroom/nodes/aliceVision/FeatureMatching.py @@ -9,6 +9,28 @@ class FeatureMatching(desc.CommandLineNode): parallelization = desc.Parallelization(blockSize=20) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +This node performs the matching of all features between the candidate image pairs. + +It is performed in 2 steps: + + 1/ **Photometric Matches** + +It performs the photometric matches between the set of features descriptors from the 2 input images. +For each feature descriptor on the first image, it looks for the 2 closest descriptors in the second image and uses a relative threshold between them. +This assumption kill features on repetitive structure but has proved to be a robust criterion. + + 2/ **Geometric Filtering** + +It performs a geometric filtering of the photometric match candidates. +It uses the features positions in the images to make a geometric filtering by using epipolar geometry in an outlier detection framework +called RANSAC (RANdom SAmple Consensus). It randomly selects a small set of feature correspondences and compute the fundamental (or essential) matrix, +then it checks the number of features that validates this model and iterate through the RANSAC framework. + +## Online +[https://alicevision.org/#photogrammetry/feature_matching](https://alicevision.org/#photogrammetry/feature_matching) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/GlobalSfM.py b/meshroom/nodes/aliceVision/GlobalSfM.py index a60b7a7895..42996b938b 100644 --- a/meshroom/nodes/aliceVision/GlobalSfM.py +++ b/meshroom/nodes/aliceVision/GlobalSfM.py @@ -10,6 +10,11 @@ class GlobalSfM(desc.CommandLineNode): commandLine = 'aliceVision_globalSfM {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +Performs the Structure-From-Motion with a global approach. +It is known to be faster but less robust to challenging datasets than the Incremental approach. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/ImageMatching.py b/meshroom/nodes/aliceVision/ImageMatching.py index 3e1ffb409c..5c7485265c 100644 --- a/meshroom/nodes/aliceVision/ImageMatching.py +++ b/meshroom/nodes/aliceVision/ImageMatching.py @@ -8,6 +8,28 @@ class ImageMatching(desc.CommandLineNode): commandLine = 'aliceVision_imageMatching {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +The goal of this node is to select the image pairs to match. The ambition is to find the images that are looking to the same areas of the scene. +Thanks to this node, the FeatureMatching node will only compute the matches between the selected image pairs. + +It provides multiple methods: + * **VocabularyTree** +It uses image retrieval techniques to find images that share some content without the cost of resolving all feature matches in details. +Each image is represented in a compact image descriptor which allows to compute the distance between all images descriptors very efficiently. +If your scene contains less than "Voc Tree: Minimal Number of Images", all image pairs will be selected. + * **Sequential** +If your input is a video sequence, you can use this option to link images between them over time. + * **SequentialAndVocabularyTree** +Combines sequential approach with Voc Tree to enable connections between keyframes at different times. + * **Exhaustive** +Export all image pairs. + * **Frustum** +If images have known poses, computes the intersection between cameras frustums to create the list of image pairs. + +## Online +[https://alicevision.org/#photogrammetry/image_matching](https://alicevision.org/#photogrammetry/image_matching) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py b/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py index b5601c840f..2d506227a5 100644 --- a/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py +++ b/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py @@ -9,6 +9,14 @@ class ImageMatchingMultiSfM(desc.CommandLineNode): # use both SfM inputs to define Node's size size = desc.MultiDynamicNodeSize(['input', 'inputB']) + documentation = ''' +The goal of this node is to select the image pairs to match in the context of an SfM augmentation. +The ambition is to find the images that are looking to the same areas of the scene. +Thanks to this node, the FeatureMatching node will only compute the matches between the selected image pairs. + +## Online +[https://alicevision.org/#photogrammetry/image_matching](https://alicevision.org/#photogrammetry/image_matching) +''' inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/KeyframeSelection.py b/meshroom/nodes/aliceVision/KeyframeSelection.py index 8d85dff80a..987af42367 100644 --- a/meshroom/nodes/aliceVision/KeyframeSelection.py +++ b/meshroom/nodes/aliceVision/KeyframeSelection.py @@ -7,6 +7,13 @@ class KeyframeSelection(desc.CommandLineNode): commandLine = 'aliceVision_utils_keyframeSelection {allParams}' + documentation = ''' +Allows to extract keyframes from a video and insert metadata. +It can extract frames from a synchronized multi-cameras rig. + +You can extract frames at regular interval by configuring only the min/maxFrameStep. +''' + inputs = [ desc.ListAttribute( elementDesc=desc.File( diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 56e2f247fa..770bfcc70e 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -29,6 +29,17 @@ class LDRToHDR(desc.CommandLineNode): cpu = desc.Level.INTENSIVE ram = desc.Level.NORMAL + documentation=''' +This node fuse LDR (Low Dynamic Range) images with multi-bracketing into HDR (High Dynamic Range) images. + +It is done in 2 steps: + +1/ Estimation of the Camera Response Function (CRF) + +2/ HDR fusion relying on the CRF + +''' + inputs = [ desc.File( name='input', @@ -40,7 +51,7 @@ class LDRToHDR(desc.CommandLineNode): desc.IntParam( name='userNbBrackets', label='Number of Brackets', - description='Number of exposure brackets per HDR image (0 for automatic).', + description='Number of exposure brackets per HDR image (0 for automatic detection).', value=0, range=(0, 15, 1), uid=[0], @@ -111,17 +122,18 @@ class LDRToHDR(desc.CommandLineNode): description="Bypass HDR creation and use the medium bracket as the source for the next steps", value=False, uid=[0], + advanced=True, ), desc.ChoiceParam( name='calibrationMethod', label='Calibration Method', description="Method used for camera calibration \n" - " * linear \n" - " * robertson \n" - " * debevec \n" - " * grossberg \n" - " * laguerre", - values=['linear', 'robertson', 'debevec', 'grossberg', 'laguerre'], + " * Linear: Disable the calibration and assumes a linear Camera Response Function. If images are encoded in a known colorspace (like sRGB for JPEG), the images will be automatically converted to linear. \n" + " * Debevec: This is the standard method for HDR calibration. \n" + " * Grossberg: Based on learned database of cameras, it allows to reduce the CRF to few parameters while keeping all the precision. \n" + " * Laguerre: Simple but robust method estimating the minimal number of parameters. \n" + " * Robertson: First method for HDR calibration in the literature. \n", + values=['linear', 'debevec', 'grossberg', 'laguerre', 'robertson'], value='debevec', exclusive=True, uid=[0], @@ -142,7 +154,7 @@ class LDRToHDR(desc.CommandLineNode): desc.ChoiceParam( name='fusionWeight', label='Fusion Weight', - description="Weight function used to fuse all LDR images together \n" + description="Weight function used to fuse all LDR images together:\n" " * gaussian \n" " * triangle \n" " * plateau", diff --git a/meshroom/nodes/aliceVision/MeshDecimate.py b/meshroom/nodes/aliceVision/MeshDecimate.py index 280e9319a4..8b928350b0 100644 --- a/meshroom/nodes/aliceVision/MeshDecimate.py +++ b/meshroom/nodes/aliceVision/MeshDecimate.py @@ -9,6 +9,10 @@ class MeshDecimate(desc.CommandLineNode): cpu = desc.Level.NORMAL ram = desc.Level.NORMAL + documentation = ''' +This node allows to reduce the density of the Mesh. +''' + inputs = [ desc.File( name="input", diff --git a/meshroom/nodes/aliceVision/MeshDenoising.py b/meshroom/nodes/aliceVision/MeshDenoising.py index 438e611ca7..a750982698 100644 --- a/meshroom/nodes/aliceVision/MeshDenoising.py +++ b/meshroom/nodes/aliceVision/MeshDenoising.py @@ -6,6 +6,11 @@ class MeshDenoising(desc.CommandLineNode): commandLine = 'aliceVision_meshDenoising {allParams}' + documentation = ''' +This experimental node allows to reduce noise from a Mesh. +for now, the parameters are difficult to control and vary a lot from one dataset to another. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/MeshFiltering.py b/meshroom/nodes/aliceVision/MeshFiltering.py index 4a1ae7ea41..deddc28ff2 100644 --- a/meshroom/nodes/aliceVision/MeshFiltering.py +++ b/meshroom/nodes/aliceVision/MeshFiltering.py @@ -6,6 +6,11 @@ class MeshFiltering(desc.CommandLineNode): commandLine = 'aliceVision_meshFiltering {allParams}' + documentation = ''' +This node applies a Laplacian filtering to remove local defects from the raw Meshing cut. + +''' + inputs = [ desc.File( name='inputMesh', diff --git a/meshroom/nodes/aliceVision/MeshResampling.py b/meshroom/nodes/aliceVision/MeshResampling.py index e6966366a3..64c4cab717 100644 --- a/meshroom/nodes/aliceVision/MeshResampling.py +++ b/meshroom/nodes/aliceVision/MeshResampling.py @@ -9,6 +9,10 @@ class MeshResampling(desc.CommandLineNode): cpu = desc.Level.NORMAL ram = desc.Level.NORMAL + documentation = ''' +This node allows to recompute the mesh surface with a new topology and uniform density. +''' + inputs = [ desc.File( name="input", diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 449ef008aa..bef32309a4 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -9,6 +9,17 @@ class Meshing(desc.CommandLineNode): cpu = desc.Level.INTENSIVE ram = desc.Level.INTENSIVE + documentation = ''' +This node creates a dense geometric surface representation of the scene. + +First, it fuses all the depth maps into a global dense point cloud with an adaptive resolution. +It then performs a 3D Delaunay tetrahedralization and a voting procedure is done to compute weights on cells and weights on facets connecting the cells. +A Graph Cut Max-Flow is applied to optimally cut the volume. This cut represents the extracted mesh surface. + +## Online +[https://alicevision.org/#photogrammetry/meshing](https://alicevision.org/#photogrammetry/meshing) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/PanoramaCompositing.py b/meshroom/nodes/aliceVision/PanoramaCompositing.py index 417d5fd70e..eea2ae0f13 100644 --- a/meshroom/nodes/aliceVision/PanoramaCompositing.py +++ b/meshroom/nodes/aliceVision/PanoramaCompositing.py @@ -10,6 +10,13 @@ class PanoramaCompositing(desc.CommandLineNode): commandLine = 'aliceVision_panoramaCompositing {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +Once the images have been transformed geometrically (in PanoramaWarping), +they have to be fused together in a single panorama image which looks like a single photography. +The Multi-band Blending method provides the best quality. It averages the pixel values using multiple bands in the frequency domain. +Multiple cameras are contributing to the low frequencies and only the best one contributes to the high frequencies. +''' + inputs = [ desc.File( name='input', @@ -31,7 +38,10 @@ class PanoramaCompositing(desc.CommandLineNode): desc.ChoiceParam( name='compositerType', label='Compositer Type', - description='Which compositer should be used to blend images', + description='Which compositer should be used to blend images:\n' + ' * multiband: high quality transition by fusing images by frequency bands\n' + ' * replace: debug option with straight transitions\n' + ' * alpha: debug option with linear transitions\n', value='multiband', values=['replace', 'alpha', 'multiband'], exclusive=True, @@ -40,7 +50,10 @@ class PanoramaCompositing(desc.CommandLineNode): desc.ChoiceParam( name='overlayType', label='Overlay Type', - description='Which overlay to display on top of panorama for debug', + description='Overlay on top of panorama to analyze transitions:\n' + ' * none: no overlay\n' + ' * borders: display image borders\n' + ' * seams: display transitions between images\n', value='none', values=['none', 'borders', 'seams'], exclusive=True, diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index a2117daf5d..d58b0142a4 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -10,6 +10,10 @@ class PanoramaEstimation(desc.CommandLineNode): commandLine = 'aliceVision_panoramaEstimation {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +Estimate relative camera rotations between input images. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index 5d3a4c076c..066deec0a5 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -10,6 +10,18 @@ class PanoramaInit(desc.CommandLineNode): commandLine = 'aliceVision_panoramaInit {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +This node allows to setup the Panorama: + +1/ Enables the initialization the cameras from known position in an XML file (provided by +["Roundshot VR Drive"](https://www.roundshot.com/xml_1/internet/fr/application/d394/d395/f396.cfm) ). + +2/ Enables to setup Full Fisheye Optics (to use an Equirectangular camera model). + +3/ To automatically detects the Fisheye Circle (radius + center) in input images or manually adjust it. + +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/PanoramaWarping.py b/meshroom/nodes/aliceVision/PanoramaWarping.py index a127fe3524..c44e7b7a17 100644 --- a/meshroom/nodes/aliceVision/PanoramaWarping.py +++ b/meshroom/nodes/aliceVision/PanoramaWarping.py @@ -10,6 +10,10 @@ class PanoramaWarping(desc.CommandLineNode): commandLine = 'aliceVision_panoramaWarping {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +Compute the image warping for each input image in the panorama coordinate system. +''' + inputs = [ desc.File( name='input', @@ -21,7 +25,8 @@ class PanoramaWarping(desc.CommandLineNode): desc.IntParam( name='panoramaWidth', label='Panorama Width', - description='Panorama width (pixels). 0 For automatic size', + description='Panorama Width (in pixels).\n' + 'Set 0 to let the software choose the size automatically, so that on average the input resolution is kept (to limit over/under sampling).', value=10000, range=(0, 50000, 1000), uid=[0] diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index 5467d576ab..afd5b4b27d 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -9,6 +9,10 @@ class PrepareDenseScene(desc.CommandLineNode): parallelization = desc.Parallelization(blockSize=40) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +This node export undistorted images so the depth map and texturing can be computed on Pinhole images without distortion. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/Publish.py b/meshroom/nodes/aliceVision/Publish.py index ebe2b9b832..ae49bdd03f 100644 --- a/meshroom/nodes/aliceVision/Publish.py +++ b/meshroom/nodes/aliceVision/Publish.py @@ -10,6 +10,11 @@ class Publish(desc.Node): size = desc.DynamicNodeSize('inputFiles') + + documentation = ''' +This node allows to copy files into a specific folder. +''' + inputs = [ desc.ListAttribute( elementDesc=desc.File( diff --git a/meshroom/nodes/aliceVision/SfMAlignment.py b/meshroom/nodes/aliceVision/SfMAlignment.py index 798ce1c59c..3e277bffa9 100644 --- a/meshroom/nodes/aliceVision/SfMAlignment.py +++ b/meshroom/nodes/aliceVision/SfMAlignment.py @@ -7,6 +7,18 @@ class SfMAlignment(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmAlignment {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +This node allows to change the coordinate system of one SfM scene to align it on another one. + +The alignment can be based on: + * from_cameras_viewid: Align cameras in both SfM on the specified viewId + * from_cameras_poseid: Align cameras in both SfM on the specified poseId + * from_cameras_filepath: Align cameras with a filepath matching, using 'fileMatchingPattern' + * from_cameras_metadata: Align cameras with matching metadata, using 'metadataMatchingList' + * from_markers: Align from markers with the same Id + +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/SfMTransfer.py b/meshroom/nodes/aliceVision/SfMTransfer.py index a30695ca26..9be8772b7b 100644 --- a/meshroom/nodes/aliceVision/SfMTransfer.py +++ b/meshroom/nodes/aliceVision/SfMTransfer.py @@ -7,6 +7,10 @@ class SfMTransfer(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmTransfer {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +This node allows to transfer poses and/or intrinsics form one SfM scene onto another one. +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index 6a9a975a84..e65bb56f6c 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -7,6 +7,18 @@ class SfMTransform(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmTransform {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +This node allows to change the coordinate system of one SfM scene. + +The transformation can be based on: + * transformation: Apply a given transformation + * auto_from_cameras: Fit all cameras into a box [-1,1] + * auto_from_landmarks: Fit all landmarks into a box [-1,1] + * from_single_camera: Use a specific camera as the origin of the coordinate system + * from_markers: Align specific markers to custom coordinates + +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/SketchfabUpload.py b/meshroom/nodes/aliceVision/SketchfabUpload.py index 2a552b879c..e599b1362a 100644 --- a/meshroom/nodes/aliceVision/SketchfabUpload.py +++ b/meshroom/nodes/aliceVision/SketchfabUpload.py @@ -51,6 +51,11 @@ def progressUpdate(size=None, progress=None, logManager=None): class SketchfabUpload(desc.Node): size = desc.DynamicNodeSize('inputFiles') + + documentation = ''' +Upload a textured mesh on Sketchfab. +''' + inputs = [ desc.ListAttribute( elementDesc=desc.File( diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index c0f960abb5..6a00b3cf35 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -10,6 +10,59 @@ class StructureFromMotion(desc.CommandLineNode): commandLine = 'aliceVision_incrementalSfM {allParams}' size = desc.DynamicNodeSize('input') + documentation = ''' +This node will analyze feature matches to understand the geometric relationship behind all the 2D observations, +and infer the rigid scene structure (3D points) with the pose (position and orientation) and internal calibration of all cameras. +The pipeline is a growing reconstruction process (called incremental SfM): it first computes an initial two-view reconstruction that is iteratively extended by adding new views. + +1/ Fuse 2-View Matches into Tracks + +It fuses all feature matches between image pairs into tracks. Each track represents a candidate point in space, visible from multiple cameras. +However, at this step of the pipeline, it still contains many outliers. + +2/ Initial Image Pair + +It chooses the best initial image pair. This choice is critical for the quality of the final reconstruction. +It should indeed provide robust matches and contain reliable geometric information. +So, this image pair should maximize the number of matches and the repartition of the corresponding features in each image. +But at the same time, the angle between the cameras should also be large enough to provide reliable geometric information. + +3/ Initial 2-View Geometry + +It computes the fundamental matrix between the 2 images selected and consider that the first one is the origin of the coordinate system. + +4/ Triangulate + +Now with the pose of the 2 first cameras, it triangulates the corresponding 2D features into 3D points. + +5/ Next Best View Selection + +After that, it selects all the images that have enough associations with the features that are already reconstructed in 3D. + +6/ Estimate New Cameras + +Based on these 2D-3D associations it performs the resectioning of each of these new cameras. +The resectioning is a Perspective-n-Point algorithm (PnP) in a RANSAC framework to find the pose of the camera that validates most of the features associations. +On each camera, a non-linear minimization is performed to refine the pose. + +7/ Triangulate + +From these new cameras poses, some tracks become visible by 2 or more resected cameras and it triangulates them. + +8/ Optimize + +It performs a Bundle Adjustment to refine everything: extrinsics and intrinsics parameters of all cameras as well as the position of all 3D points. +It filters the results of the Bundle Adjustment by removing all observations that have high reprojection error or insufficient angles between observations. + +9/ Loop from 5 to 9 + +As we have triangulated new points, we get more image candidates for next best views selection and we can iterate from 5 to 9. +It iterates like that, adding cameras and triangulating new 2D features into 3D points and removing 3D points that became invalidated, until we cannot localize new views. + +## Online +[https://alicevision.org/#photogrammetry/sfm](https://alicevision.org/#photogrammetry/sfm) +''' + inputs = [ desc.File( name='input', diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 117201a82f..1e36756f06 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -7,6 +7,20 @@ class Texturing(desc.CommandLineNode): commandLine = 'aliceVision_texturing {allParams}' cpu = desc.Level.INTENSIVE ram = desc.Level.INTENSIVE + + documentation = ''' +This node computes the texturing on the mesh. + +If the mesh has no associated UV, it automatically computes UV maps. + +For each triangle, it uses the visibility information associated to each vertex to retrieve the texture candidates. +It select the best cameras based on the resolution covering the triangle. Finally it averages the pixel values using multiple bands in the frequency domain. +Many cameras are contributing to the low frequencies and only the best ones contributes to the high frequencies. + +## Online +[https://alicevision.org/#photogrammetry/texturing](https://alicevision.org/#photogrammetry/texturing) +''' + inputs = [ desc.File( name='input', From 505f0c68eed4c15ba076eb720f2d596cea23eaef Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 14:30:50 +0100 Subject: [PATCH 034/100] [ui] ImageGallery: update scene when the selected ldr2hdr node changed --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index e50f76660f..975fd7f8c0 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -357,6 +357,13 @@ Panel { text: MaterialIcons.hdr_on visible: _reconstruction.ldr2hdr enabled: _reconstruction.ldr2hdr && _reconstruction.ldr2hdr.isComputed + property string nodeID: _reconstruction.ldr2hdr.label + _reconstruction.ldr2hdr.isComputed + onNodeIDChanged: { + if(checked) + { + _reconstruction.setupLDRToHDRCameraInit(); + } + } onEnabledChanged: { // Reset the toggle to avoid getting stuck // with the HDR node checked but disabled. From 43d7fc1c1dcbf331a8f23f9518f6843fc02ada19 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 19:25:51 +0100 Subject: [PATCH 035/100] [nodes] CameraInit: no special characters in descriptions --- meshroom/nodes/aliceVision/CameraInit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index bc2814a613..86e920ffef 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -40,8 +40,8 @@ " * radial1: Pinhole camera with one radial distortion parameter\n" " * radial3: Pinhole camera with 3 radial distortion parameters\n" " * brown: Pinhole camera with 3 radial and 2 tangential distortion parameters\n" - " * fisheye4: Pinhole camera with 4 distortion parameters suited for fisheye optics (like 120° FoV)\n" - " * equidistant_r3: Non-projective camera model suited for full-fisheye optics (like 180° FoV)\n", + " * fisheye4: Pinhole camera with 4 distortion parameters suited for fisheye optics (like 120deg FoV)\n" + " * equidistant_r3: Non-projective camera model suited for full-fisheye optics (like 180deg FoV)\n", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'equidistant_r3'], exclusive=True, uid=[0]), desc.IntParam(name="width", label="Width", description="Image Width", value=0, uid=[], range=(0, 10000, 1)), desc.IntParam(name="height", label="Height", description="Image Height", value=0, uid=[], range=(0, 10000, 1)), From 51dbc88371c6e024c2515cebbfb1d6250fe68619 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 19:26:39 +0100 Subject: [PATCH 036/100] [ui] fix xml drag&drop: PanoramaExternalInfo is now PanoramaInit --- meshroom/ui/reconstruction.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 0fcc176d2c..0491f8dea2 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -695,22 +695,22 @@ def handleFilesDrop(self, drop, cameraInit): "", )) else: - panoramaExternalInfoNodes = self.graph.nodesByType('PanoramaExternalInfo') + panoramaInitNodes = self.graph.nodesByType('PanoramaInit') for panoramaInfoFile in filesByType.panoramaInfo: - for panoramaInfoNode in panoramaExternalInfoNodes: - panoramaInfoNode.attribute('config').value = panoramaInfoFile - if panoramaExternalInfoNodes: + for panoramaInitNode in panoramaInitNodes: + panoramaInitNode.attribute('config').value = panoramaInfoFile + if panoramaInitNodes: self.info.emit( Message( "Panorama XML", - "XML file declared on PanoramaExternalInfo node", - "XML file '{}' set on node '{}'".format(','.join(filesByType.panoramaInfo), ','.join([n.getLabel() for n in panoramaExternalInfoNodes])), + "XML file declared on PanoramaInit node", + "XML file '{}' set on node '{}'".format(','.join(filesByType.panoramaInfo), ','.join([n.getLabel() for n in panoramaInitNodes])), )) else: self.error.emit( Message( - "No PanoramaExternalInfo Node", - "No PanoramaExternalInfo Node to set the Panorama file:\n'{}'.".format(','.join(filesByType.panoramaInfo)), + "No PanoramaInit Node", + "No PanoramaInit Node to set the Panorama file:\n'{}'.".format(','.join(filesByType.panoramaInfo)), "", )) From bb683b6e32a16e40f081a81a467d5b66f5e23168 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 20:42:49 +0100 Subject: [PATCH 037/100] [nodes] ImageProcessing: add option keepImageFilename --- meshroom/nodes/aliceVision/ImageProcessing.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/ImageProcessing.py b/meshroom/nodes/aliceVision/ImageProcessing.py index 4f917b2e09..8a7a1afbd1 100644 --- a/meshroom/nodes/aliceVision/ImageProcessing.py +++ b/meshroom/nodes/aliceVision/ImageProcessing.py @@ -9,6 +9,10 @@ class ImageProcessing(desc.CommandLineNode): # parallelization = desc.Parallelization(blockSize=40) # commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' +Convert or apply filtering to the input images. +''' + inputs = [ desc.File( name='input', @@ -20,8 +24,8 @@ class ImageProcessing(desc.CommandLineNode): desc.ChoiceParam( name='extension', label='File Extension', - description='File Extension.', - value='', + description='File Extension. If empty, use the input file type.', + value='exr', values=['', 'exr', 'jpg', 'tiff', 'png'], exclusive=True, uid=[0], @@ -33,6 +37,13 @@ class ImageProcessing(desc.CommandLineNode): value=False, uid=[0], ), + desc.BoolParam( + name='keepImageFilename', + label='Keep Image Filename', + description='Keep Image Filename instead of using View UID.', + value=False, + uid=[0], + ), desc.BoolParam( name='exposureCompensation', label='Exposure Compensation', From 6a62644a68fe7d3ed5e208de740b16df5fc97498 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Mar 2020 20:51:37 +0100 Subject: [PATCH 038/100] [nodes] ImageMatching: add option FrustumOrVocabularyTree --- meshroom/multiview.py | 3 ++- meshroom/nodes/aliceVision/ImageMatching.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 4d6ae6f752..c1d94ed4b1 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -152,7 +152,8 @@ def hdriPipeline(graph): imageMatching = graph.addNewNode('ImageMatching', input=panoramaInit.outSfMDataFilename, - featuresFolders=[featureExtraction.output]) + featuresFolders=[featureExtraction.output], + method='FrustumOrVocabularyTree') featureMatching = graph.addNewNode('FeatureMatching', input=imageMatching.input, featuresFolders=imageMatching.featuresFolders, diff --git a/meshroom/nodes/aliceVision/ImageMatching.py b/meshroom/nodes/aliceVision/ImageMatching.py index 5c7485265c..eea05f27a3 100644 --- a/meshroom/nodes/aliceVision/ImageMatching.py +++ b/meshroom/nodes/aliceVision/ImageMatching.py @@ -25,6 +25,8 @@ class ImageMatching(desc.CommandLineNode): Export all image pairs. * **Frustum** If images have known poses, computes the intersection between cameras frustums to create the list of image pairs. + * **FrustumOrVocabularyTree** +If images have known poses, use frustum intersection else use VocabularuTree. ## Online [https://alicevision.org/#photogrammetry/image_matching](https://alicevision.org/#photogrammetry/image_matching) @@ -53,9 +55,17 @@ class ImageMatching(desc.CommandLineNode): desc.ChoiceParam( name='method', label='Method', - description='Method used to select the image pairs to match.', + description='Method used to select the image pairs to match:\n' + ' * VocabularyTree: It uses image retrieval techniques to find images that share some content without the cost of resolving all \n' + 'feature matches in details. Each image is represented in a compact image descriptor which allows to compute the distance between all \n' + 'images descriptors very efficiently. If your scene contains less than "Voc Tree: Minimal Number of Images", all image pairs will be selected.\n' + ' * Sequential: If your input is a video sequence, you can use this option to link images between them over time.\n' + ' * SequentialAndVocabularyTree: Combines sequential approach with VocTree to enable connections between keyframes at different times.\n' + ' * Exhaustive: Export all image pairs.\n' + ' * Frustum: If images have known poses, computes the intersection between cameras frustums to create the list of image pairs.\n' + ' * FrustumOrVocabularyTree: If images have known poses, use frustum intersection else use VocabularuTree.\n', value='VocabularyTree', - values=['VocabularyTree', 'Sequential', 'SequentialAndVocabularyTree','Exhaustive','Frustum'], + values=['VocabularyTree', 'Sequential', 'SequentialAndVocabularyTree', 'Exhaustive', 'Frustum', 'FrustumOrVocabularyTree'], exclusive=True, uid=[0], ), From 8d6b0d73be4e126162b3fc7d16775511ee850154 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 30 Mar 2020 11:22:20 +0200 Subject: [PATCH 039/100] [ui] ImageGallery: avoid qml warning on undefined object access --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 975fd7f8c0..69d1aa1dde 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -357,7 +357,7 @@ Panel { text: MaterialIcons.hdr_on visible: _reconstruction.ldr2hdr enabled: _reconstruction.ldr2hdr && _reconstruction.ldr2hdr.isComputed - property string nodeID: _reconstruction.ldr2hdr.label + _reconstruction.ldr2hdr.isComputed + property string nodeID: _reconstruction.ldr2hdr ? (_reconstruction.ldr2hdr.label + _reconstruction.ldr2hdr.isComputed) : "" onNodeIDChanged: { if(checked) { From 375070e003caa75f8633f8fc5a2a9638ac67e008 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 30 Mar 2020 11:22:58 +0200 Subject: [PATCH 040/100] [ui] main: fix openFileDialog use fileUrl instead of file --- meshroom/ui/qml/main.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 32d92f0659..d82aea30b8 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -210,9 +210,9 @@ ApplicationWindow { title: "Open File" nameFilters: ["Meshroom Graphs (*.mg)"] onAccepted: { - if(_reconstruction.loadUrl(file)) + if(_reconstruction.loadUrl(fileUrl)) { - MeshroomApp.addRecentProjectFile(file.toString()) + MeshroomApp.addRecentProjectFile(fileUrl.toString()) } } } From 4bfbfc44d34543abb1083a21187c7bc9ec54115a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 30 Mar 2020 11:24:17 +0200 Subject: [PATCH 041/100] [nodes] Panorama/LDRToHDR: remove the "advanced" flag on some parameters --- meshroom/nodes/aliceVision/ImageMatching.py | 2 +- meshroom/nodes/aliceVision/LDRToHDR.py | 1 - meshroom/nodes/aliceVision/PanoramaEstimation.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/meshroom/nodes/aliceVision/ImageMatching.py b/meshroom/nodes/aliceVision/ImageMatching.py index eea05f27a3..c117548b1f 100644 --- a/meshroom/nodes/aliceVision/ImageMatching.py +++ b/meshroom/nodes/aliceVision/ImageMatching.py @@ -63,7 +63,7 @@ class ImageMatching(desc.CommandLineNode): ' * SequentialAndVocabularyTree: Combines sequential approach with VocTree to enable connections between keyframes at different times.\n' ' * Exhaustive: Export all image pairs.\n' ' * Frustum: If images have known poses, computes the intersection between cameras frustums to create the list of image pairs.\n' - ' * FrustumOrVocabularyTree: If images have known poses, use frustum intersection else use VocabularuTree.\n', + ' * FrustumOrVocabularyTree: If images have known poses, use frustum intersection else use VocabularyTree.\n', value='VocabularyTree', values=['VocabularyTree', 'Sequential', 'SequentialAndVocabularyTree', 'Exhaustive', 'Frustum', 'FrustumOrVocabularyTree'], exclusive=True, diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 770bfcc70e..5ebd24c7c5 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -64,7 +64,6 @@ class LDRToHDR(desc.CommandLineNode): value=0, range=(0, 10, 1), uid=[], - advanced=True, ), desc.FloatParam( name='highlightCorrectionFactor', diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index d58b0142a4..a26e6244cb 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -64,7 +64,6 @@ class PanoramaEstimation(desc.CommandLineNode): value=0.0, range=(-180.0, 180.0, 1.0), uid=[0], - advanced=True, ), desc.FloatParam( name='offsetLatitude', @@ -73,7 +72,6 @@ class PanoramaEstimation(desc.CommandLineNode): value=0.0, range=(-90.0, 90.0, 1.0), uid=[0], - advanced=True, ), desc.ChoiceParam( name='rotationAveraging', From 069b9118e9751193af59f1ef071895ec8492a317 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 4 May 2020 21:47:13 +0200 Subject: [PATCH 042/100] [multiview] add dpx file extension --- meshroom/multiview.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index c1d94ed4b1..87471c8707 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -6,7 +6,10 @@ from meshroom.core.graph import Graph, GraphModification # Supported image extensions -imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw') +imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', + '.rw2', '.cr2', '.nef', '.arw', + '.dpx', + ) videoExtensions = ('.avi', '.mov', '.qt', '.mkv', '.webm', '.mp4', '.mpg', '.mpeg', '.m2v', '.m4v', From e404fce7d1664fb53c08f5b82e7a5300cf39ae96 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 1 Jun 2020 16:10:38 +0200 Subject: [PATCH 043/100] [bin] meshroom_photogrammetry: add hdriFisheye --- bin/meshroom_photogrammetry | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index b7d8fecc6e..2447759287 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -114,7 +114,10 @@ with multiview.GraphModification(graph): multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph) elif args.pipeline.lower() == "hdri": # default hdri pipeline - graph = multiview.hdri(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph) + multiview.hdri(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph) + elif args.pipeline.lower() == "hdrifisheye": + # default hdriFisheye pipeline + multiview.hdriFisheye(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph) else: # custom pipeline graph.load(args.pipeline) From c214b48aa5b655b2d7cd2a4ac3a8384c05a5e843 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 23 Jun 2020 14:25:50 +0200 Subject: [PATCH 044/100] [core] fix Node.isComputed property --- meshroom/core/node.py | 2 +- meshroom/ui/qml/Viewer/Viewer2D.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index f188008485..6d1bb35e52 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -777,7 +777,7 @@ def __repr__(self): size = Property(int, getSize, notify=sizeChanged) globalStatusChanged = Signal() globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) - isComputed = Property(bool, lambda self: self._isComputed(), notify=globalStatusChanged) + isComputed = Property(bool, _isComputed, notify=globalStatusChanged) class Node(BaseNode): diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index b8eb967812..0556786a18 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -386,7 +386,7 @@ FocusScope { property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked property var activeNode: _reconstruction.sfm - property bool isComputed: activeNode && activeNode.isComputed() + property bool isComputed: activeNode && activeNode.isComputed active: false // It takes time to load tracks, so keep them looaded, if we may use it again. From 2bb6dbcef178626059c2ff7fb13190515c0f3341 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 25 Jun 2020 01:05:09 +0200 Subject: [PATCH 045/100] [nodes] PanoramaEstimation: Update output params --- meshroom/multiview.py | 2 +- meshroom/nodes/aliceVision/PanoramaEstimation.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index db197b10a4..f105f59032 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -168,7 +168,7 @@ def hdriPipeline(graph): matchesFolders=[featureMatching.output]) panoramaWarping = graph.addNewNode('PanoramaWarping', - input=panoramaEstimation.outSfMDataFilename) + input=panoramaEstimation.output) panoramaCompositing = graph.addNewNode('PanoramaCompositing', input=panoramaWarping.output) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index a26e6244cb..bab96d890e 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -128,16 +128,16 @@ class PanoramaEstimation(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output Folder', - description='', - value=desc.Node.internalFolder, + label='Output SfMData File', + description='Path to the output sfmdata file', + value=desc.Node.internalFolder + 'panorama.abc', uid=[], ), desc.File( - name='outSfMDataFilename', + name='outputViewsAndPoses', label='Output SfMData File', - description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'sfmData.abc', + description='''Path to the output sfmdata file with cameras (views and poses).''', + value=desc.Node.internalFolder + 'cameras.sfm', uid=[], ), ] From 9e03f67c9b4d8d920d3ee299c915769f3a206b4e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 25 Jun 2020 01:06:52 +0200 Subject: [PATCH 046/100] [ui] Viewer2D: bug fix, node.isComputed is a property and no more a slot --- meshroom/ui/qml/Viewer/Viewer2D.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 0556786a18..a8d90f5ee1 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -424,7 +424,7 @@ FocusScope { property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked property var activeNode: _reconstruction.featureMatching - property bool isComputed: activeNode && activeNode.isComputed() + property bool isComputed: activeNode && activeNode.isComputed active: false // It takes time to load tracks, so keep them looaded, if we may use it again. From 49c03bc239ec17f90d9e6e593f30a4c0e228ec4b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 25 Jun 2020 01:10:25 +0200 Subject: [PATCH 047/100] [ui] Reconstruction: the active sfm node can be a StructureFromMotion or a PanoramaEstimation node --- .../nodes/aliceVision/StructureFromMotion.py | 27 -------------- meshroom/ui/reconstruction.py | 35 +++++++++++++++++-- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 22bcc86333..a3724d6921 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -347,30 +347,3 @@ class StructureFromMotion(desc.CommandLineNode): uid=[], ), ] - - @staticmethod - def getResults(node): - """ - Parse SfM result and return views, poses and intrinsics as three dicts with viewId, poseId and intrinsicId as keys. - """ - reportFile = node.outputViewsAndPoses.value - if not os.path.exists(reportFile): - return {}, {}, {} - - with open(reportFile) as jsonFile: - report = json.load(jsonFile) - - views = dict() - poses = dict() - intrinsics = dict() - - for view in report['views']: - views[view['viewId']] = view - - for pose in report['poses']: - poses[pose['poseId']] = pose['pose'] - - for intrinsic in report['intrinsics']: - intrinsics[intrinsic['intrinsicId']] = intrinsic - - return views, poses, intrinsics diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index ff37150f08..8b9391f3f6 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -356,6 +356,32 @@ def undistortedImageSource(self): return QUrl.fromLocalFile(self._undistortedImagePath) +def parseSfMJsonFile(sfmJsonFile): + """ + Parse the SfM Json file and return views, poses and intrinsics as three dicts with viewId, poseId and intrinsicId as keys. + """ + if not os.path.exists(sfmJsonFile): + return {}, {}, {} + + with open(sfmJsonFile) as jsonFile: + report = json.load(jsonFile) + + views = dict() + poses = dict() + intrinsics = dict() + + for view in report['views']: + views[view['viewId']] = view + + for pose in report['poses']: + poses[pose['poseId']] = pose['pose'] + + for intrinsic in report['intrinsics']: + intrinsics[intrinsic['intrinsicId']] = intrinsic + + return views, poses, intrinsics + + class Reconstruction(UIGraph): """ Specialization of a UIGraph designed to manage a 3D reconstruction. @@ -504,6 +530,9 @@ def onGraphChanged(self): self.setSfm(self.lastSfmNode()) + if not self.sfm: + self.sfm = self.lastNodeOfType("PanoramaEstimation", self._cameraInit, Status.SUCCESS) + # TODO: listen specifically for cameraInit creation/deletion self._graph.nodes.countChanged.connect(self.updateCameraInits) @@ -763,7 +792,7 @@ def importImagesFromFolder(self, path, recursive=False): recursive: List files in folders recursively. """ - logging.warning("importImagesFromFolder: " + str(path)) + logging.debug("importImagesFromFolder: " + str(path)) filesByType = multiview.findFilesByTypeInFolder(path, recursive) if filesByType.images: self.buildIntrinsics(self.cameraInit, filesByType.images) @@ -910,7 +939,7 @@ def setBuildingIntrinsics(self, value): @Slot(QObject) def setActiveNodeOfType(self, node): """ Set node as the active node of its type. """ - if node.nodeType == "StructureFromMotion": + if node.nodeType == "StructureFromMotion" or node.nodeType == "PanoramaEstimation": self.sfm = node elif node.nodeType == "FeatureExtraction": self.featureExtraction = node @@ -936,7 +965,7 @@ def updateSfMResults(self): self._poses = dict() self._solvedIntrinsics = dict() else: - self._views, self._poses, self._solvedIntrinsics = self._sfm.nodeDesc.getResults(self._sfm) + self._views, self._poses, self._solvedIntrinsics = parseSfMJsonFile(self._sfm.outputViewsAndPoses.value) self.sfmReportChanged.emit() def getSfm(self): From 5650903252449ea948ad57bcb3fc54d331a9583b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 25 Jun 2020 01:11:10 +0200 Subject: [PATCH 048/100] [ui] minor comments update --- meshroom/ui/qml/Viewer/FeaturesViewer.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer/FeaturesViewer.qml b/meshroom/ui/qml/Viewer/FeaturesViewer.qml index 83b796cc52..3730747384 100644 --- a/meshroom/ui/qml/Viewer/FeaturesViewer.qml +++ b/meshroom/ui/qml/Viewer/FeaturesViewer.qml @@ -10,13 +10,13 @@ import Utils 1.0 Repeater { id: root - /// ViewID to display the features of + /// ViewID to display the features of a specific view property int viewId /// SfMData to display the data of SfM property var sfmData /// Folder containing the features files property string featureFolder - /// Folder containing the matches files + /// Tracks object loading all the matches files property var tracks /// The list of describer types to load property alias describerTypes: root.model From 06faca31a31ce92447a348f18062e9197ad2b1da Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 17:39:09 +0200 Subject: [PATCH 049/100] [nodes] PanoramaEstimation: expose more advanced params --- .../nodes/aliceVision/PanoramaEstimation.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index bab96d890e..809219ecbe 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -107,13 +107,47 @@ class PanoramaEstimation(desc.CommandLineNode): ), desc.BoolParam( name='lockAllIntrinsics', - label='Force Lock of All Intrinsic Camera Parameters.', + label='Force Lock of All Intrinsics', description='Force to keep constant all the intrinsics parameters of the cameras (focal length, \n' 'principal point, distortion if any) during the reconstruction.\n' 'This may be helpful if the input cameras are already fully calibrated.', value=False, uid=[0], ), + desc.FloatParam( + name='maxAngleToPrior', + label='Max Angle To Priors (deg.)', + description='''Maximal angle allowed regarding the input prior (in degrees).''', + value=5.0, + range=(0.0, 360.0, 1.0), + uid=[0], + advanced=True, + ), + desc.FloatParam( + name='maxAngularError', + label='Max Angular Error (deg.)', + description='''Maximal angular error in global rotation averging (in degrees).''', + value=100.0, + range=(0.0, 360.0, 1.0), + uid=[0], + advanced=True, + ), + desc.BoolParam( + name='intermediateRefineWithFocal', + label='Intermediate Refine: Focal', + description='Intermediate refine with rotation and focal length only.', + value=False, + uid=[0], + advanced=True, + ), + desc.BoolParam( + name='intermediateRefineWithFocalDist', + label='Intermediate Refine: Focal And Distortion', + description='Intermediate refine with rotation, focal length and distortion.', + value=False, + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 0f07dfe4824065235beadb14f461ecfeba51231f Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 15:53:08 +0200 Subject: [PATCH 050/100] [ui] Viewer: fix Node.isComputed is a property --- meshroom/ui/qml/Viewer/SfmGlobalStats.qml | 2 +- meshroom/ui/qml/Viewer/SfmStatsView.qml | 2 +- meshroom/ui/qml/Viewer/Viewer2D.qml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/qml/Viewer/SfmGlobalStats.qml b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml index 81f8aea843..e97d8c96e2 100644 --- a/meshroom/ui/qml/Viewer/SfmGlobalStats.qml +++ b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml @@ -20,7 +20,7 @@ FloatingPane { property var mTracks property color textColor: Colors.sysPalette.text - visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed()) ? root.visible : false + visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed) ? root.visible : false clip: true padding: 4 diff --git a/meshroom/ui/qml/Viewer/SfmStatsView.qml b/meshroom/ui/qml/Viewer/SfmStatsView.qml index 974078e3b0..d4ba8b2f3c 100644 --- a/meshroom/ui/qml/Viewer/SfmStatsView.qml +++ b/meshroom/ui/qml/Viewer/SfmStatsView.qml @@ -21,7 +21,7 @@ FloatingPane { property int viewId property color textColor: Colors.sysPalette.text - visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed()) ? root.visible : false + visible: (_reconstruction.sfm && _reconstruction.sfm.isComputed) ? root.visible : false clip: true padding: 4 diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index a8d90f5ee1..af02d620f1 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -382,7 +382,7 @@ FocusScope { Loader { id: msfmDataLoader - // active: _reconstruction.sfm && _reconstruction.sfm.isComputed() + // active: _reconstruction.sfm && _reconstruction.sfm.isComputed property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked property var activeNode: _reconstruction.sfm @@ -628,7 +628,7 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed() && _reconstruction.selectedViewId >= 0 + enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed && _reconstruction.selectedViewId >= 0 onCheckedChanged: { if(checked == true) { @@ -652,7 +652,7 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed() + enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed onCheckedChanged: { if(checked == true) { From 41ab68a9faca64ec799cccbf706970eb32b4ddeb Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 15:56:03 +0200 Subject: [PATCH 051/100] [nodes] LDRToHDR: fix the detection of the number of brackets - Detects correctly if there is only bracket (nothing to do) - Only nbBrackets invalidates the UID --- meshroom/nodes/aliceVision/LDRToHDR.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 5ebd24c7c5..7df19ae121 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -51,10 +51,10 @@ class LDRToHDR(desc.CommandLineNode): desc.IntParam( name='userNbBrackets', label='Number of Brackets', - description='Number of exposure brackets per HDR image (0 for automatic detection).', + description='Manually set the number of exposure brackets per HDR image. Set 0 for automatic detection.', value=0, range=(0, 15, 1), - uid=[0], + uid=[], group='user', # not used directly on the command line ), desc.IntParam( @@ -63,7 +63,7 @@ class LDRToHDR(desc.CommandLineNode): description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', value=0, range=(0, 10, 1), - uid=[], + uid=[0], ), desc.FloatParam( name='highlightCorrectionFactor', @@ -258,13 +258,16 @@ def update(cls, node): exposureGroups.append(exposures) exposures = None bracketSizes = set() - for expGroup in exposureGroups: - bracketSizes.add(len(expGroup)) - if len(bracketSizes) == 1: - node.nbBrackets.value = bracketSizes.pop() - # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) + if len(exposureGroups) == 1: + node.nbBrackets.value = 1 else: - node.nbBrackets.value = 0 + for expGroup in exposureGroups: + bracketSizes.add(len(expGroup)) + if len(bracketSizes) == 1: + node.nbBrackets.value = bracketSizes.pop() + # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) + else: + node.nbBrackets.value = 0 # logging.info("[LDRToHDR] Update end") From 6e6bb6f491856b7608ab733be86c9cdfdc7bb9cb Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 15:56:53 +0200 Subject: [PATCH 052/100] [nodes] ImageProcessing: remove old param keepImageFilename --- meshroom/nodes/aliceVision/ImageProcessing.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/meshroom/nodes/aliceVision/ImageProcessing.py b/meshroom/nodes/aliceVision/ImageProcessing.py index f167ffcfd1..b7352d0250 100644 --- a/meshroom/nodes/aliceVision/ImageProcessing.py +++ b/meshroom/nodes/aliceVision/ImageProcessing.py @@ -49,13 +49,6 @@ class ImageProcessing(desc.CommandLineNode): value=False, uid=[0], ), - desc.BoolParam( - name='keepImageFilename', - label='Keep Image Filename', - description='Keep Image Filename instead of using View UID.', - value=False, - uid=[0], - ), desc.BoolParam( name='exposureCompensation', label='Exposure Compensation', From 05a93c28a226d32a8a0b49aa8df8a5d39728a404 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 15:58:26 +0200 Subject: [PATCH 053/100] [multiview] Add 2 new nodes to the HDRI pipeline: SfMTransform and ImageProcessing --- meshroom/multiview.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index f105f59032..38ab59f082 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -146,8 +146,9 @@ def hdriPipeline(graph): input=cameraInit.output) featureExtraction = graph.addNewNode('FeatureExtraction', - input=ldr2hdr.outSfMDataFilename) - featureExtraction.describerPreset.value = 'high' + input=ldr2hdr.outSfMDataFilename, + describerPreset='high') + panoramaInit = graph.addNewNode('PanoramaInit', input=featureExtraction.input, dependency=[featureExtraction.output] # Workaround for tractor submission with a fake dependency @@ -157,6 +158,7 @@ def hdriPipeline(graph): input=panoramaInit.outSfMDataFilename, featuresFolders=[featureExtraction.output], method='FrustumOrVocabularyTree') + featureMatching = graph.addNewNode('FeatureMatching', input=imageMatching.input, featuresFolders=imageMatching.featuresFolders, @@ -167,11 +169,20 @@ def hdriPipeline(graph): featuresFolders=featureMatching.featuresFolders, matchesFolders=[featureMatching.output]) + panoramaOrientation = graph.addNewNode('SfMTransform', + input=panoramaEstimation.output, + method='from_single_camera') + panoramaWarping = graph.addNewNode('PanoramaWarping', - input=panoramaEstimation.output) + input=panoramaOrientation.output) panoramaCompositing = graph.addNewNode('PanoramaCompositing', - input=panoramaWarping.output) + input=panoramaWarping.output) + + imageProcessing = graph.addNewNode('ImageProcessing', + input=panoramaCompositing.output, + fillHoles=True, + extension='exr') return [ cameraInit, @@ -180,8 +191,10 @@ def hdriPipeline(graph): imageMatching, featureMatching, panoramaEstimation, + panoramaOrientation, panoramaWarping, panoramaCompositing, + imageProcessing, ] From bbc56cfb31b15666766cb36d7f9b679d0d4b6552 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 16:01:18 +0200 Subject: [PATCH 054/100] [core] Add support for lambda function in Attribute desc value Allows to dynamically configure the default value of the Attribute in python. --- meshroom/core/attribute.py | 5 ++++- meshroom/core/node.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 2880bb836f..1d9a88b5c4 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -3,6 +3,7 @@ import collections import re import weakref +import types from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot from meshroom.core import desc, pyCompatibility, hashValue @@ -189,7 +190,7 @@ def getExportValue(self): if self.isLink: return self.getLinkParam().asLinkExpr() if self.isOutput: - return self.desc.value + return self.defaultValue() return self._value def getValueStr(self): @@ -201,6 +202,8 @@ def getValueStr(self): return str(self.value) def defaultValue(self): + if isinstance(self.desc.value, types.FunctionType): + return self.desc.value(self) return self.desc.value def _isDefault(self): diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 6d1bb35e52..fa38e04d95 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -583,8 +583,9 @@ def _buildAttributeCmdVars(cmdVars, name, attr): if not isinstance(attr.attributeDesc, desc.File): continue - attr.value = attr.attributeDesc.value.format(**self._cmdVars) - attr._invalidationValue = attr.attributeDesc.value.format(**cmdVarsNoCache) + defaultValue = attr.defaultValue() + attr.value = defaultValue.format(**self._cmdVars) + attr._invalidationValue = defaultValue.format(**cmdVarsNoCache) v = attr.getValueStr() self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) From a5131eec44da12cda38a12406d9d013e278df11f Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 16:03:00 +0200 Subject: [PATCH 055/100] [nodes] ImageProcessing: define output values using lambda --- meshroom/nodes/aliceVision/ImageProcessing.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/ImageProcessing.py b/meshroom/nodes/aliceVision/ImageProcessing.py index b7352d0250..2d78fe3308 100644 --- a/meshroom/nodes/aliceVision/ImageProcessing.py +++ b/meshroom/nodes/aliceVision/ImageProcessing.py @@ -2,6 +2,8 @@ from meshroom.core import desc +import os.path + class ImageProcessing(desc.CommandLineNode): commandLine = 'aliceVision_utils_imageProcessing {allParams}' @@ -181,7 +183,7 @@ class ImageProcessing(desc.CommandLineNode): name='outSfMData', label='Output sfmData', description='Output sfmData.', - value=desc.Node.internalFolder + 'sfmData.abc', + value=lambda attr: (desc.Node.internalFolder + 'sfmData' + os.path.splitext(attr.node.input.value)[1]) if (os.path.splitext(attr.node.input.value)[1] in ['.abc', '.sfm']) else '', uid=[], ), desc.File( @@ -192,4 +194,12 @@ class ImageProcessing(desc.CommandLineNode): group='', # do not export on the command line uid=[], ), + desc.File( + name='outputImages', + label='Output Images', + description='Output Image Files.', + value=lambda attr: desc.Node.internalFolder + '*.' + (attr.node.extension.value or '*'), + group='', # do not export on the command line + uid=[], + ), ] From 202c1db5ffbf87ffa694744777baf60db8d97760 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 28 Jun 2020 19:22:10 +0200 Subject: [PATCH 056/100] [ui] Viewer: fix error when the image is not yet defined --- meshroom/ui/qml/Viewer/Viewer2D.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index af02d620f1..93dd3171bd 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -18,7 +18,9 @@ FocusScope { property alias useFloatImageViewer: displayHDR.checked property string loadingModules: { - var res = "" + if(!imgContainer.image) + return ""; + var res = ""; if(imgContainer.image.status === Image.Loading) res += " Image"; if(featuresViewerLoader.status === Loader.Ready) From 082f9b584375edd8b2a4bac2c7f075b73579cdb0 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 29 Jun 2020 12:53:11 +0200 Subject: [PATCH 057/100] [core] correct the actions order when loading the project file We now call applyExpr before updateInternals is called (triggered by setupProjectFile and/or end of GraphModification). So the input parameters expressions/connections are up-to-date (after applyExpr) when updateInternals will evaluate the input/output parameters. --- meshroom/core/graph.py | 17 +++++++++-------- meshroom/core/node.py | 9 +++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 9f73008763..7cde3e26aa 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -273,12 +273,13 @@ def load(self, filepath, setupProjectFile=True): # Add node to the graph with raw attributes values self._addNode(n, nodeName) - if setupProjectFile: - # Update filepath related members - self._setFilepath(filepath) + # Create graph edges by resolving attributes expressions + self._applyExpr() - # Create graph edges by resolving attributes expressions - self._applyExpr() + if setupProjectFile: + # Update filepath related members + # Note: needs to be done at the end as it will trigger an updateInternals. + self._setFilepath(filepath) return True @@ -871,13 +872,13 @@ def flowEdges(self, startNodes=None): flowEdges.append(link) return flowEdges - def nodesFromNode(self, startNode, filterType=None): + def nodesFromNode(self, startNode, filterTypes=None): """ Return the node chain from startNode to the graph leaves. Args: startNode (Node): the node to start the visit from. - filterType (str): (optional) only return the nodes of the given type + filterTypes (str list): (optional) only return the nodes of the given types (does not stop the visit, this is a post-process only) Returns: The list of nodes from startNode to the graph leaves following edges. @@ -887,7 +888,7 @@ def nodesFromNode(self, startNode, filterType=None): visitor = Visitor() def discoverVertex(vertex, graph): - if not filterType or vertex.nodeType == filterType: + if not filterTypes or vertex.nodeType in filterTypes: nodes.append(vertex) visitor.discoverVertex = discoverVertex diff --git a/meshroom/core/node.py b/meshroom/core/node.py index fa38e04d95..6b83c6c5d7 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -584,8 +584,13 @@ def _buildAttributeCmdVars(cmdVars, name, attr): continue defaultValue = attr.defaultValue() - attr.value = defaultValue.format(**self._cmdVars) - attr._invalidationValue = defaultValue.format(**cmdVarsNoCache) + try: + attr.value = defaultValue.format(**self._cmdVars) + attr._invalidationValue = defaultValue.format(**cmdVarsNoCache) + except KeyError as e: + logging.warning('Invalid expression with missing key on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) + except ValueError as e: + logging.warning('Invalid expression value on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) v = attr.getValueStr() self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) From 21fa9167aeaff099f456e427edcdd4f2b40bcb48 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 29 Jun 2020 13:59:53 +0200 Subject: [PATCH 058/100] [nodes] SfMTransform, SfMTransfer, SfMAlignment expose the same output params than the StructureFromMotion node --- .../nodes/aliceVision/PanoramaEstimation.py | 2 +- meshroom/nodes/aliceVision/SfMAlignment.py | 17 +++++++++++++---- meshroom/nodes/aliceVision/SfMTransfer.py | 15 ++++++++++++--- meshroom/nodes/aliceVision/SfMTransform.py | 15 ++++++++++++--- .../nodes/aliceVision/StructureFromMotion.py | 2 +- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index 809219ecbe..d3f37ca565 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -169,7 +169,7 @@ class PanoramaEstimation(desc.CommandLineNode): ), desc.File( name='outputViewsAndPoses', - label='Output SfMData File', + label='Output Poses', description='''Path to the output sfmdata file with cameras (views and poses).''', value=desc.Node.internalFolder + 'cameras.sfm', uid=[], diff --git a/meshroom/nodes/aliceVision/SfMAlignment.py b/meshroom/nodes/aliceVision/SfMAlignment.py index 3e277bffa9..0b21005175 100644 --- a/meshroom/nodes/aliceVision/SfMAlignment.py +++ b/meshroom/nodes/aliceVision/SfMAlignment.py @@ -1,7 +1,9 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc +import os.path + class SfMAlignment(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmAlignment {allParams}' @@ -107,9 +109,16 @@ class SfMAlignment(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output', - description='''Aligned SfMData file .''', - value=desc.Node.internalFolder + 'alignedSfM.abc', + label='Output SfMData File', + description='SfMData file.', + value=lambda attr: desc.Node.internalFolder + (os.path.splitext(os.path.basename(attr.node.input.value))[0] or 'sfmData') + '.abc', + uid=[], + ), + desc.File( + name='outputViewsAndPoses', + label='Output Poses', + description='''Path to the output sfmdata file with cameras (views and poses).''', + value=desc.Node.internalFolder + 'cameras.sfm', uid=[], ), ] diff --git a/meshroom/nodes/aliceVision/SfMTransfer.py b/meshroom/nodes/aliceVision/SfMTransfer.py index 9be8772b7b..3a112d52d6 100644 --- a/meshroom/nodes/aliceVision/SfMTransfer.py +++ b/meshroom/nodes/aliceVision/SfMTransfer.py @@ -1,7 +1,9 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc +import os.path + class SfMTransfer(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmTransfer {allParams}' @@ -90,9 +92,16 @@ class SfMTransfer(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output', + label='Output SfMData File', description='SfMData file.', - value=desc.Node.internalFolder + 'sfmData.abc', + value=lambda attr: desc.Node.internalFolder + (os.path.splitext(os.path.basename(attr.node.input.value))[0] or 'sfmData') + '.abc', + uid=[], + ), + desc.File( + name='outputViewsAndPoses', + label='Output Poses', + description='''Path to the output sfmdata file with cameras (views and poses).''', + value=desc.Node.internalFolder + 'cameras.sfm', uid=[], ), ] diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index e65bb56f6c..f0b1c61f54 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -1,7 +1,9 @@ -__version__ = "1.1" +__version__ = "2.0" from meshroom.core import desc +import os.path + class SfMTransform(desc.CommandLineNode): commandLine = 'aliceVision_utils_sfmTransform {allParams}' @@ -116,9 +118,16 @@ class SfMTransform(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output', + label='Output SfMData File', description='''Aligned SfMData file .''', - value=desc.Node.internalFolder + 'transformedSfM.abc', + value=lambda attr: desc.Node.internalFolder + (os.path.splitext(os.path.basename(attr.node.input.value))[0] or 'sfmData') + '.abc', + uid=[], + ), + desc.File( + name='outputViewsAndPoses', + label='Output Poses', + description='''Path to the output sfmdata file with cameras (views and poses).''', + value=desc.Node.internalFolder + 'cameras.sfm', uid=[], ), ] diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index a3724d6921..9d7c3147e5 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -334,7 +334,7 @@ class StructureFromMotion(desc.CommandLineNode): ), desc.File( name='outputViewsAndPoses', - label='Output SfMData File', + label='Output Poses', description='''Path to the output sfmdata file with cameras (views and poses).''', value=desc.Node.internalFolder + 'cameras.sfm', uid=[], From c3ae491baccf69271b2a72728465f36f3888c829 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 29 Jun 2020 14:00:38 +0200 Subject: [PATCH 059/100] [nodes] ImageProcessing: update output parameter changes --- meshroom/nodes/aliceVision/ImageProcessing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/ImageProcessing.py b/meshroom/nodes/aliceVision/ImageProcessing.py index 2d78fe3308..28c706b9fd 100644 --- a/meshroom/nodes/aliceVision/ImageProcessing.py +++ b/meshroom/nodes/aliceVision/ImageProcessing.py @@ -1,4 +1,4 @@ -__version__ = "1.1" +__version__ = "2.0" from meshroom.core import desc @@ -183,22 +183,22 @@ class ImageProcessing(desc.CommandLineNode): name='outSfMData', label='Output sfmData', description='Output sfmData.', - value=lambda attr: (desc.Node.internalFolder + 'sfmData' + os.path.splitext(attr.node.input.value)[1]) if (os.path.splitext(attr.node.input.value)[1] in ['.abc', '.sfm']) else '', + value=lambda attr: (desc.Node.internalFolder + os.path.basename(attr.node.input.value)) if (os.path.splitext(attr.node.input.value)[1] in ['.abc', '.sfm']) else '', uid=[], + group='', # do not export on the command line ), desc.File( - name='outputFolder', - label='Output Images Folder', + name='output', + label='Output Folder', description='Output Images Folder.', value=desc.Node.internalFolder, - group='', # do not export on the command line uid=[], ), desc.File( name='outputImages', label='Output Images', description='Output Image Files.', - value=lambda attr: desc.Node.internalFolder + '*.' + (attr.node.extension.value or '*'), + value=lambda attr: desc.Node.internalFolder + os.path.basename(attr.node.input.value) if (os.path.splitext(attr.node.input.value)[1] not in ['', '.abc', '.sfm']) else (desc.Node.internalFolder + '*.' + (attr.node.extension.value or '*')), group='', # do not export on the command line uid=[], ), From bcc7fc412370e7a8c669a88dfd21df53bb04f87d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 30 Jun 2020 13:18:13 +0200 Subject: [PATCH 060/100] [ui] Several nodes can hold the SfM StructureFromMotion, StructureFromMotion, PanoramaEstimation, SfMTransfer, SfMTransform, SfMAlignment, GlobalSfM --- meshroom/ui/reconstruction.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 8b9391f3f6..0967d0b362 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -382,6 +382,9 @@ def parseSfMJsonFile(sfmJsonFile): return views, poses, intrinsics +sfmHolderNodeTypes = ["StructureFromMotion", "GlobalSfM", "PanoramaEstimation", "SfMTransfer", "SfMTransform", "SfMAlignment"] + + class Reconstruction(UIGraph): """ Specialization of a UIGraph designed to manage a 3D reconstruction. @@ -530,9 +533,6 @@ def onGraphChanged(self): self.setSfm(self.lastSfmNode()) - if not self.sfm: - self.sfm = self.lastNodeOfType("PanoramaEstimation", self._cameraInit, Status.SUCCESS) - # TODO: listen specifically for cameraInit creation/deletion self._graph.nodes.countChanged.connect(self.updateCameraInits) @@ -569,19 +569,19 @@ def setCameraInitIndex(self, idx): 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 + 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 + 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 + self.depthMap = self.lastNodeOfType(['DepthMapFilter'], self.cameraInit) if self.cameraInit else None def updateLdr2hdrNode(self): """ Set the current LDR2HDR node based on the current CameraInit node. """ - self.ldr2hdr = self.lastNodeOfType('LDRToHDR', self.cameraInit) if self.cameraInit else None + self.ldr2hdr = self.lastNodeOfType(['LDRToHDR'], self.cameraInit) if self.cameraInit else None @Slot() def setupLDRToHDRCameraInit(self): @@ -624,19 +624,19 @@ def getAutoFisheyeCircle(self, panoramaInit): def updatePanoramaInitNode(self): """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.panoramaInit = self.lastNodeOfType('PanoramaInit', self.cameraInit) if self.cameraInit else None + self.panoramaInit = self.lastNodeOfType(["PanoramaInit"], self.cameraInit) if self.cameraInit else None def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ - return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS) + return self.lastNodeOfType(sfmHolderNodeTypes, self._cameraInit, Status.SUCCESS) - def lastNodeOfType(self, nodeType, startNode, preferredStatus=None): + def lastNodeOfType(self, nodeTypes, startNode, preferredStatus=None): """ Returns the last node of the given type starting from 'startNode'. If 'preferredStatus' is specified, the last node with this status will be considered in priority. Args: - nodeType (str): the node type + nodeTypes (str list): the node types startNode (Node): the node to start from preferredStatus (Status): (optional) the node status to prioritize @@ -645,7 +645,7 @@ def lastNodeOfType(self, nodeType, startNode, preferredStatus=None): """ if not startNode: return None - nodes = self._graph.nodesFromNode(startNode, nodeType)[0] + nodes = self._graph.nodesFromNode(startNode, nodeTypes)[0] if not nodes: return None node = nodes[-1] @@ -939,7 +939,7 @@ def setBuildingIntrinsics(self, value): @Slot(QObject) def setActiveNodeOfType(self, node): """ Set node as the active node of its type. """ - if node.nodeType == "StructureFromMotion" or node.nodeType == "PanoramaEstimation": + if node.nodeType in sfmHolderNodeTypes: self.sfm = node elif node.nodeType == "FeatureExtraction": self.featureExtraction = node @@ -960,7 +960,7 @@ def updateSfMResults(self): """ Update internal views, poses and solved intrinsics based on the current SfM node. """ - if not self._sfm: + if not self._sfm or ('outputViewsAndPoses' not in self._sfm.getAttributes().keys()): self._views = dict() self._poses = dict() self._solvedIntrinsics = dict() @@ -1003,8 +1003,8 @@ def setSfm(self, node): self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) - self.texturing = self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS) - self.prepareDenseScene = self.lastNodeOfType("PrepareDenseScene", self._sfm, Status.SUCCESS) + self.texturing = self.lastNodeOfType(["Texturing"], self._sfm, Status.SUCCESS) + self.prepareDenseScene = self.lastNodeOfType(["PrepareDenseScene"], self._sfm, Status.SUCCESS) @Slot(QObject, result=bool) def isInViews(self, viewpoint): From 778c88583c0de87fbe178c83349c8ffba52be956 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 30 Jun 2020 22:07:14 +0200 Subject: [PATCH 061/100] [ui] typo in comment --- meshroom/ui/qml/Viewer3D/ImageOverlay.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/ImageOverlay.qml b/meshroom/ui/qml/Viewer3D/ImageOverlay.qml index a6ddf90ac5..5f79d4df3c 100644 --- a/meshroom/ui/qml/Viewer3D/ImageOverlay.qml +++ b/meshroom/ui/qml/Viewer3D/ImageOverlay.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts 1.12 /** * ImageOverlay enables to display a Viewpoint image on top of a 3D View. * It takes the principal point correction into account and handle image ratio to - * correclty fit or crop according to original image ratio and parent Item ratio. + * correctly fit or crop according to original image ratio and parent Item ratio. */ Item { id: root From aa27123d308e6d431872f02bc21efda22eb09383 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 30 Jun 2020 22:09:11 +0200 Subject: [PATCH 062/100] [ui] Reconstruction: add an active SfMTransform node --- meshroom/ui/reconstruction.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 0967d0b362..093e05a00d 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -440,6 +440,10 @@ def __init__(self, defaultPipeline='', parent=None): self._panoramaInit = None self.cameraInitChanged.connect(self.updatePanoramaInitNode) + # - PanoramaInit + self._sfmTransform = None + self.cameraInitChanged.connect(self.updateSfMTransformNode) + # react to internal graph changes to update those variables self.graphChanged.connect(self.onGraphChanged) @@ -527,6 +531,7 @@ def onGraphChanged(self): self.ldr2hdr = None self.hdrCameraInit = None self.panoramaInit = None + self.sfmTransform = None self.updateCameraInits() if not self._graph: return @@ -626,6 +631,10 @@ def updatePanoramaInitNode(self): """ Set the current FeatureExtraction node based on the current CameraInit node. """ self.panoramaInit = self.lastNodeOfType(["PanoramaInit"], self.cameraInit) if self.cameraInit else None + def updateSfMTransformNode(self): + """ Set the current SfMTransform node based on the current CameraInit node. """ + self.sfmTransform = self.lastNodeOfType(["SfMTransform"], self.cameraInit) if self.cameraInit else None + def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ return self.lastNodeOfType(sfmHolderNodeTypes, self._cameraInit, Status.SUCCESS) @@ -941,7 +950,8 @@ def setActiveNodeOfType(self, node): """ Set node as the active node of its type. """ if node.nodeType in sfmHolderNodeTypes: self.sfm = node - elif node.nodeType == "FeatureExtraction": + + if node.nodeType == "FeatureExtraction": self.featureExtraction = node elif node.nodeType == "FeatureMatching": self.featureMatching = node @@ -955,6 +965,8 @@ def setActiveNodeOfType(self, node): self.ldr2hdr = node elif node.nodeType == "PanoramaInit": self.panoramaInit = node + elif node.nodeType == "SfMTransform": + self.sfmTransform = node def updateSfMResults(self): """ @@ -1126,7 +1138,7 @@ def getPoseRT(self, viewpoint): depthMap = makeProperty(QObject, "_depthMap", depthMapChanged, resetOnDestroy=True) texturingChanged = Signal() - texturing = makeProperty(QObject, "_texturing", notify=texturingChanged) + texturing = makeProperty(QObject, "_texturing", notify=texturingChanged, resetOnDestroy=True) ldr2hdrChanged = Signal() ldr2hdr = makeProperty(QObject, "_ldr2hdr", notify=ldr2hdrChanged, resetOnDestroy=True) @@ -1134,6 +1146,9 @@ def getPoseRT(self, viewpoint): panoramaInitChanged = Signal() panoramaInit = makeProperty(QObject, "_panoramaInit", notify=panoramaInitChanged, resetOnDestroy=True) + sfmTransformChanged = Signal() + sfmTransform = makeProperty(QObject, "_sfmTransform", notify=sfmTransformChanged, resetOnDestroy=True) + nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) # Signals to propagate high-level messages From b777f5ffc8f1205fa1214c6219b25c74a705f2f3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 30 Jun 2020 22:10:11 +0200 Subject: [PATCH 063/100] [ui] ImageGallery: add an entry in the menu to define the selected view as the center image --- meshroom/ui/qml/ImageGallery/ImageDelegate.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml index 9100216f63..c96a7a57bb 100644 --- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml +++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml @@ -56,6 +56,11 @@ Item { enabled: !root.readOnly onClicked: removeRequest() } + MenuItem { + text: "Define As Center Image" + enabled: !root.readOnly && _viewpoint.viewId != -1 && _reconstruction && _reconstruction.sfmTransform + onClicked: _reconstruction.sfmTransform.attribute("transformation").value = _viewpoint.viewId.toString() + } } ColumnLayout { From 33a0cec3f25875348ad6a0474d0c4311afbb5f47 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 30 Jun 2020 22:10:45 +0200 Subject: [PATCH 064/100] [ui] ImageGallery: add a display flag on the center image --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 69d1aa1dde..1fe1e72a02 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -20,6 +20,8 @@ Panel { readonly property alias currentItem: grid.currentItem readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : "" readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined + readonly property int centerViewId: (_reconstruction && _reconstruction.sfmTransform) ? parseInt(_reconstruction.sfmTransform.attribute("transformation").value) : 0 + property int defaultCellSize: 160 property int currentIndex: 0 property bool readOnly: false @@ -189,6 +191,16 @@ Panel { } } + // Center of SfMTransform + Loader { + id: sfmTransformIndicator + active: (viewpoint.get("viewId").value == centerViewId) + sourceComponent: ImageBadge { + text: MaterialIcons.gamepad + ToolTip.text: "Camera used to define the center of the scene." + } + } + Item { Layout.fillWidth: true } // Reconstruction status indicator From 7d99ba4b598b987ed7cb940649c6b762a148835a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 1 Jul 2020 01:54:16 +0200 Subject: [PATCH 065/100] [core] copy attribute desc default value to ensure that it is not editable if the value is a list --- meshroom/core/attribute.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 1d9a88b5c4..392d610c9e 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # coding:utf-8 import collections +import copy import re import weakref import types @@ -55,7 +56,7 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): self._node = weakref.ref(node) self.attributeDesc = attributeDesc self._isOutput = isOutput - self._value = attributeDesc.value + self._value = copy.copy(attributeDesc.value) self._label = attributeDesc.label # invalidation value for output attributes @@ -204,7 +205,8 @@ def getValueStr(self): def defaultValue(self): if isinstance(self.desc.value, types.FunctionType): return self.desc.value(self) - return self.desc.value + # Need to force a copy, for the case where the value is a list (avoid reference to the desc value) + return copy.copy(self.desc.value) def _isDefault(self): return self._value == self.defaultValue() From aa7fed41b5076263a3e488d8eb26bd1bdaeb65ec Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 1 Jul 2020 01:55:10 +0200 Subject: [PATCH 066/100] [multiview] do not allow fisheye4 camera model in the HDRI context --- meshroom/multiview.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 38ab59f082..2ec6b7f390 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -141,6 +141,11 @@ def hdriPipeline(graph): list of Node: the created nodes """ cameraInit = graph.addNewNode('CameraInit') + try: + # fisheye4 does not work well in the ParoramaEstimation, so here we avoid to use it. + cameraInit.attribute('allowedCameraModels').value.remove("fisheye4") + except ValueError: + pass ldr2hdr = graph.addNewNode('LDRToHDR', input=cameraInit.output) From 2d0e17b0265717ebccaccf013fb22f6c6ceb7f52 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 11:28:19 +0200 Subject: [PATCH 067/100] [nodes] ldrToHdr: improve metadata search --- meshroom/nodes/aliceVision/LDRToHDR.py | 28 +++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 7df19ae121..1743af2007 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -5,6 +5,23 @@ from meshroom.core import desc +def findMetadata(d, keys, defaultValue): + v = None + for key in keys: + v = d.get(key, None) + k = key.lower() + if v != None: + return v + for dk, dv in d: + dkm = dk.lower().replace(" ", "") + if dkm == key.lower(): + return dv + dkm = dkm.split(":")[-1] + dkm = dkm.split("/")[-1] + if dkm == k: + return dv + return defaultValue + class DividedInputNodeSize(desc.DynamicNodeSize): """ @@ -237,12 +254,13 @@ def update(cls, node): node.nbBrackets.value = 0 return d = json.loads(jsonMetadata) - fnumber = d.get("FNumber", d.get("Exif:ApertureValue", "")) - shutterSpeed = d.get("Exif:ShutterSpeedValue", "") # also "ExposureTime"? - iso = d.get("Exif:ISOSpeedRatings", "") + fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "") + shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "") + iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "") if not fnumber and not shutterSpeed: - # if one image without shutter or fnumber, we cannot found the number of brackets - node.nbBrackets.value = 0 + # If one image without shutter or fnumber, we cannot found the number of brackets. + # We assume that there is no multi-bracketing, so nothing to do. + node.nbBrackets.value = 1 return inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso))) inputs.sort() From 588e9fd335a8fa45f4b67c4f6afaa21f480b9138 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 11:28:54 +0200 Subject: [PATCH 068/100] [ui] Reconstruction: fix for python-2 compatibility --- meshroom/ui/reconstruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 093e05a00d..223dfee3f4 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -15,6 +15,12 @@ from meshroom.ui.graph import UIGraph from meshroom.ui.utils import makeProperty +# Python2 compatibility +try: + FileNotFoundError +except NameError: + FileNotFoundError = IOError + class Message(QObject): """ Simple structure wrapping a high-level message. """ From 224abce62f2bddea47522c8cf4302c4053ac10ab Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 11:29:20 +0200 Subject: [PATCH 069/100] [nodes] CameraInit: add some debug log --- meshroom/nodes/aliceVision/CameraInit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index e461f8623a..4f2ee49616 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -5,6 +5,7 @@ import psutil import shutil import tempfile +import logging from meshroom.core import desc @@ -257,7 +258,7 @@ def buildIntrinsics(self, node, additionalViews=()): os.makedirs(os.path.join(tmpCache, node.internalFolder)) self.createViewpointsFile(node, additionalViews) cmd = self.buildCommandLine(node.chunks[0]) - # logging.debug(' - commandLine:', cmd) + logging.debug(' - commandLine:', cmd) proc = psutil.Popen(cmd, stdout=None, stderr=None, shell=True) stdout, stderr = proc.communicate() # proc.wait() @@ -270,10 +271,13 @@ def buildIntrinsics(self, node, additionalViews=()): cameraInitSfM = node.output.value return readSfMData(cameraInitSfM) - except Exception: + except Exception as e: + logging.debug("[CameraInit] Error while building intrinsics: {}".format(str(e))) raise finally: - shutil.rmtree(tmpCache) + if os.path.exists(tmpCache): + logging.debug("[CameraInit] Remove temp files in: {}".format(tmpCache)) + shutil.rmtree(tmpCache) def createViewpointsFile(self, node, additionalViews=()): node.viewpointsFile = "" From 119bc2107318e0ff2d35fc68ff6eba833387b206 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 11:30:10 +0200 Subject: [PATCH 070/100] [multiview] hdri pipeline: update files to publish --- meshroom/multiview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 2ec6b7f390..9a899e69eb 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -115,8 +115,8 @@ def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), out cameraInit.intrinsics.extend(inputIntrinsics) if output: - stitching = nodes[-1] - graph.addNewNode('Publish', output=output, inputFiles=[stitching.output]) + imageProcessing = nodes[-1] + graph.addNewNode('Publish', output=output, inputFiles=[imageProcessing.outputImages]) return graph From 0ff28eed14e2d88a9d23debf706ab98c5082419c Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 11:56:30 +0200 Subject: [PATCH 071/100] [nodes] ldrToHdr: fix for python-2 compatibility --- meshroom/nodes/aliceVision/LDRToHDR.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 1743af2007..ec083e63ba 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -12,7 +12,7 @@ def findMetadata(d, keys, defaultValue): k = key.lower() if v != None: return v - for dk, dv in d: + for dk, dv in d.iteritems(): dkm = dk.lower().replace(" ", "") if dkm == key.lower(): return dv From 5c43f77d20dd97dc6d149f7e80b988a2b40cc352 Mon Sep 17 00:00:00 2001 From: fabien servant Date: Fri, 26 Jun 2020 14:50:45 +0200 Subject: [PATCH 072/100] [nodes] Update hdr pipeline with sampling, calibration and merge --- .../nodes/aliceVision/LdrToHdrCalibration.py | 77 +++++++++++++++++++ meshroom/nodes/aliceVision/LdrToHdrMerge.py | 62 +++++++++++++++ .../nodes/aliceVision/LdrToHdrSampling.py | 77 +++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 meshroom/nodes/aliceVision/LdrToHdrCalibration.py create mode 100644 meshroom/nodes/aliceVision/LdrToHdrMerge.py create mode 100644 meshroom/nodes/aliceVision/LdrToHdrSampling.py diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py new file mode 100644 index 0000000000..e44e0010e6 --- /dev/null +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -0,0 +1,77 @@ +__version__ = "2.0" + +from meshroom.core import desc + + +class LdrToHdrCalibration(desc.CommandLineNode): + commandLine = 'aliceVision_LdrToHdrCalibration {allParams}' + size = desc.DynamicNodeSize('input') + #parallelization = desc.Parallelization(blockSize=40) + #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + + documentation = ''' + Calibrate LDR to HDR response curve from samples +''' + + inputs = [ + desc.File( + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='samples', + label='Samples folder', + description='Samples folder', + value=desc.Node.internalFolder, + uid=[0], + ), + desc.ChoiceParam( + name='calibrationMethod', + label='Calibration Method', + description="Method used for camera calibration \n" + " * Linear: Disable the calibration and assumes a linear Camera Response Function. If images are encoded in a known colorspace (like sRGB for JPEG), the images will be automatically converted to linear. \n" + " * Debevec: This is the standard method for HDR calibration. \n" + " * Grossberg: Based on learned database of cameras, it allows to reduce the CRF to few parameters while keeping all the precision. \n" + " * Laguerre: Simple but robust method estimating the minimal number of parameters. \n" + " * Robertson: First method for HDR calibration in the literature. \n", + values=['linear', 'debevec', 'grossberg', 'laguerre'], + value='debevec', + exclusive=True, + uid=[0], + ), + desc.ChoiceParam( + name='calibrationWeight', + label='Calibration Weight', + description="Weight function used to calibrate camera response \n" + " * default (automatically selected according to the calibrationMethod) \n" + " * gaussian \n" + " * triangle \n" + " * plateau", + value='default', + values=['default', 'gaussian', 'triangle', 'plateau'], + exclusive=True, + uid=[0], + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='verbosity level (fatal, error, warning, info, debug, trace).', + value='info', + values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], + exclusive=True, + uid=[], + ) + ] + + outputs = [ + desc.File( + name='response', + label='Output response File', + description='Path to the output response file', + value=desc.Node.internalFolder + 'response.csv', + uid=[], + ) + ] \ No newline at end of file diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py new file mode 100644 index 0000000000..ece1329919 --- /dev/null +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -0,0 +1,62 @@ +__version__ = "2.0" + +from meshroom.core import desc + + +class LdrToHdrMerge(desc.CommandLineNode): + commandLine = 'aliceVision_LdrToHdrMerge {allParams}' + size = desc.DynamicNodeSize('input') + #parallelization = desc.Parallelization(blockSize=40) + #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + + documentation = ''' + Calibrate LDR to HDR response curve from samples +''' + + inputs = [ + desc.File( + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='response', + label='Response file', + description='Response file', + value='', + uid=[0], + ), + desc.ChoiceParam( + name='fusionWeight', + label='Fusion Weight', + description="Weight function used to fuse all LDR images together:\n" + " * gaussian \n" + " * triangle \n" + " * plateau", + value='gaussian', + values=['gaussian', 'triangle', 'plateau'], + exclusive=True, + uid=[0], + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='verbosity level (fatal, error, warning, info, debug, trace).', + value='info', + values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], + exclusive=True, + uid=[], + ) + ] + + outputs = [ + desc.File( + name='outSfMDataFilename', + label='Output SfMData File', + description='Path to the output sfmdata file', + value=desc.Node.internalFolder + 'sfmData.sfm', + uid=[], + ) + ] \ No newline at end of file diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py new file mode 100644 index 0000000000..4547a9d13a --- /dev/null +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -0,0 +1,77 @@ +__version__ = "2.0" + +from meshroom.core import desc + + +class LdrToHdrSampling(desc.CommandLineNode): + commandLine = 'aliceVision_LdrToHdrSampling {allParams}' + size = desc.DynamicNodeSize('input') + #parallelization = desc.Parallelization(blockSize=40) + #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + + documentation = ''' + Sample pixels from Low range images for HDR creation +''' + + inputs = [ + desc.File( + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.IntParam( + name='userNbBrackets', + label='Number of Brackets', + description='Number of exposure brackets per HDR image (0 for automatic detection).', + value=0, + range=(0, 15, 1), + uid=[0], + group='user', # not used directly on the command line + ), + desc.IntParam( + name='nbBrackets', + label='Automatic Nb Brackets', + description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', + value=0, + range=(0, 10, 1), + uid=[], + ), + desc.BoolParam( + name='byPass', + label='bypass convert', + description="Bypass HDR creation and use the medium bracket as the source for the next steps", + value=False, + uid=[0], + advanced=True, + ), + desc.IntParam( + name='channelQuantizationPower', + label='Channel Quantization Power', + description='Quantization level like 8 bits or 10 bits.', + value=10, + range=(8, 14, 1), + uid=[0], + advanced=True, + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='verbosity level (fatal, error, warning, info, debug, trace).', + value='info', + values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], + exclusive=True, + uid=[], + ) + ] + + outputs = [ + desc.File( + name='output', + label='Output Folder', + description='Output path for the samples.', + value=desc.Node.internalFolder, + uid=[], + ), + ] \ No newline at end of file From e6afe6ef58ff9353e9f3bf01740c05ccf44a1fcd Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sat, 27 Jun 2020 22:25:59 +0200 Subject: [PATCH 073/100] [nodes] Add new PanoramaPrepareImages to fix images orientation --- .../aliceVision/PanoramaPrepareImages.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 meshroom/nodes/aliceVision/PanoramaPrepareImages.py diff --git a/meshroom/nodes/aliceVision/PanoramaPrepareImages.py b/meshroom/nodes/aliceVision/PanoramaPrepareImages.py new file mode 100644 index 0000000000..e418feb923 --- /dev/null +++ b/meshroom/nodes/aliceVision/PanoramaPrepareImages.py @@ -0,0 +1,43 @@ +__version__ = "1.1" + +from meshroom.core import desc + + +class PanoramaPrepareImages(desc.CommandLineNode): + commandLine = 'aliceVision_panoramaPrepareImages {allParams}' + size = desc.DynamicNodeSize('input') + # parallelization = desc.Parallelization(blockSize=40) + # commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + + documentation = ''' +Prepare images for panorama Estimation +''' + + inputs = [ + desc.File( + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='verbosity level (fatal, error, warning, info, debug, trace).', + value='info', + values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], + exclusive=True, + uid=[], + ) + ] + + outputs = [ + desc.File( + name='outSfMData', + label='Output sfmData', + description='Output sfmData.', + value=desc.Node.internalFolder + 'sfmData.abc', + uid=[], + ), + ] From 45570b64879d696ef752632bcdfb91afbc29d35a Mon Sep 17 00:00:00 2001 From: fabien servant Date: Mon, 29 Jun 2020 19:49:26 +0200 Subject: [PATCH 074/100] [nodes] ldrToHdr: expose more parameters --- .../nodes/aliceVision/LdrToHdrCalibration.py | 34 +++++++++++++++++++ meshroom/nodes/aliceVision/LdrToHdrMerge.py | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index e44e0010e6..33503f723e 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -55,6 +55,40 @@ class LdrToHdrCalibration(desc.CommandLineNode): exclusive=True, uid=[0], ), + desc.IntParam( + name='userNbBrackets', + label='Number of Brackets', + description='Number of exposure brackets per HDR image (0 for automatic detection).', + value=0, + range=(0, 15, 1), + uid=[0], + group='user', # not used directly on the command line + ), + desc.IntParam( + name='nbBrackets', + label='Automatic Nb Brackets', + description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', + value=0, + range=(0, 10, 1), + uid=[], + ), + desc.BoolParam( + name='byPass', + label='bypass convert', + description="Bypass HDR creation and use the medium bracket as the source for the next steps", + value=False, + uid=[0], + advanced=True, + ), + desc.IntParam( + name='channelQuantizationPower', + label='Channel Quantization Power', + description='Quantization level like 8 bits or 10 bits.', + value=10, + range=(8, 14, 1), + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index ece1329919..ba099b8f8f 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -40,6 +40,40 @@ class LdrToHdrMerge(desc.CommandLineNode): exclusive=True, uid=[0], ), + desc.IntParam( + name='userNbBrackets', + label='Number of Brackets', + description='Number of exposure brackets per HDR image (0 for automatic detection).', + value=0, + range=(0, 15, 1), + uid=[0], + group='user', # not used directly on the command line + ), + desc.IntParam( + name='nbBrackets', + label='Automatic Nb Brackets', + description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', + value=0, + range=(0, 10, 1), + uid=[], + ), + desc.BoolParam( + name='byPass', + label='bypass convert', + description="Bypass HDR creation and use the medium bracket as the source for the next steps", + value=False, + uid=[0], + advanced=True, + ), + desc.IntParam( + name='channelQuantizationPower', + label='Channel Quantization Power', + description='Quantization level like 8 bits or 10 bits.', + value=10, + range=(8, 14, 1), + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From baac5015596ed802922bae3ef942ec1b1a595f6a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 18:14:41 +0200 Subject: [PATCH 075/100] [multiview] Update hdri pipeline with hdr sampling, calibration and merge --- meshroom/multiview.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 9a899e69eb..96dac95f1c 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -125,8 +125,6 @@ def hdriFisheye(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list graph = Graph('HDRI-Fisheye') with GraphModification(graph): hdri(inputImages, inputViewpoints, inputIntrinsics, output, graph) - for ldrToHdr in graph.nodesByType("LDRToHDR"): - ldrToHdr.attribute("fisheyeLens").value = True for panoramaInit in graph.nodesByType("PanoramaInit"): panoramaInit.attribute("useFisheye").value = True return graph @@ -147,11 +145,19 @@ def hdriPipeline(graph): except ValueError: pass - ldr2hdr = graph.addNewNode('LDRToHDR', + ldr2hdrSampling = graph.addNewNode('LdrToHdrSampling', input=cameraInit.output) + ldr2hdrCalibration = graph.addNewNode('LdrToHdrCalibration', + input=ldr2hdrSampling.input, + samples=ldr2hdrSampling.output) + + ldr2hdrMerge = graph.addNewNode('LdrToHdrMerge', + input=ldr2hdrCalibration.input, + response=ldr2hdrCalibration.response) + featureExtraction = graph.addNewNode('FeatureExtraction', - input=ldr2hdr.outSfMDataFilename, + input=ldr2hdrMerge.outSfMDataFilename, describerPreset='high') panoramaInit = graph.addNewNode('PanoramaInit', From 2401a366e2efe6dc918342e005f656befff0a3c7 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 18:15:03 +0200 Subject: [PATCH 076/100] [core] expose a recursive option in getLinkParam --- meshroom/core/attribute.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 392d610c9e..79b940aeb3 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -164,8 +164,15 @@ def isLinkExpression(value): """ return isinstance(value, pyCompatibility.basestring) and Attribute.stringIsLinkRe.match(value) - def getLinkParam(self): - return self.node.graph.edge(self).src if self.isLink else None + def getLinkParam(self, recursive=False): + if not self.isLink: + return None + linkParam = self.node.graph.edge(self).src + if not recursive: + return linkParam + if linkParam.isLink: + return linkParam.getLinkParam(recursive) + return linkParam def _applyExpr(self): """ From 1b52bd5cbcc5cffa7449f5ebd59ff8b6cb54aaec Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 2 Jul 2020 18:16:09 +0200 Subject: [PATCH 077/100] [nodes] ldrToHdr: automatic brackets estimation (temporary solution) --- .../nodes/aliceVision/LdrToHdrCalibration.py | 89 ++++++++++++++- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 103 +++++++++++++++++- .../nodes/aliceVision/LdrToHdrSampling.py | 84 +++++++++++++- 3 files changed, 270 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 33503f723e..feaa31f204 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -1,13 +1,32 @@ __version__ = "2.0" +import json +import os + from meshroom.core import desc +def findMetadata(d, keys, defaultValue): + v = None + for key in keys: + v = d.get(key, None) + k = key.lower() + if v != None: + return v + for dk, dv in d.iteritems(): + dkm = dk.lower().replace(" ", "") + if dkm == key.lower(): + return dv + dkm = dkm.split(":")[-1] + dkm = dkm.split("/")[-1] + if dkm == k: + return dv + return defaultValue + + class LdrToHdrCalibration(desc.CommandLineNode): commandLine = 'aliceVision_LdrToHdrCalibration {allParams}' size = desc.DynamicNodeSize('input') - #parallelization = desc.Parallelization(blockSize=40) - #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' documentation = ''' Calibrate LDR to HDR response curve from samples @@ -108,4 +127,68 @@ class LdrToHdrCalibration(desc.CommandLineNode): value=desc.Node.internalFolder + 'response.csv', uid=[], ) - ] \ No newline at end of file + ] + + @classmethod + def update(cls, node): + if not isinstance(node.nodeDesc, cls): + raise ValueError("Node {} is not an instance of type {}".format(node, cls)) + # TODO: use Node version for this test + if 'userNbBrackets' not in node.getAttributes().keys(): + # Old version of the node + return + if node.userNbBrackets.value != 0: + node.nbBrackets.value = node.userNbBrackets.value + return + # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion)) + cameraInitOutput = node.input.getLinkParam(recursive=True) + if not cameraInitOutput: + node.nbBrackets.value = 0 + return + print("LdrToHdrCalib cameraInitOutput: " + str(cameraInitOutput)) + viewpoints = cameraInitOutput.node.viewpoints.value + print("LdrToHdrCalib viewpoints: " + str(viewpoints)) + + # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) + inputs = [] + for viewpoint in viewpoints: + jsonMetadata = viewpoint.metadata.value + if not jsonMetadata: + # no metadata, we cannot found the number of brackets + node.nbBrackets.value = 0 + return + d = json.loads(jsonMetadata) + fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "") + shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "") + iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "") + if not fnumber and not shutterSpeed: + # If one image without shutter or fnumber, we cannot found the number of brackets. + # We assume that there is no multi-bracketing, so nothing to do. + node.nbBrackets.value = 1 + return + inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso))) + inputs.sort() + + exposureGroups = [] + exposures = [] + for path, exp in inputs: + if exposures and exp != exposures[-1] and exp == exposures[0]: + exposureGroups.append(exposures) + exposures = [exp] + else: + exposures.append(exp) + exposureGroups.append(exposures) + exposures = None + bracketSizes = set() + if len(exposureGroups) == 1: + node.nbBrackets.value = 1 + else: + for expGroup in exposureGroups: + bracketSizes.add(len(expGroup)) + if len(bracketSizes) == 1: + node.nbBrackets.value = bracketSizes.pop() + # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) + else: + node.nbBrackets.value = 0 + # logging.info("[LDRToHDR] Update end") + diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index ba099b8f8f..f63606cddc 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -1,11 +1,48 @@ __version__ = "2.0" +import json +import os + from meshroom.core import desc +def findMetadata(d, keys, defaultValue): + v = None + for key in keys: + v = d.get(key, None) + k = key.lower() + if v != None: + return v + for dk, dv in d.iteritems(): + dkm = dk.lower().replace(" ", "") + if dkm == key.lower(): + return dv + dkm = dkm.split(":")[-1] + dkm = dkm.split("/")[-1] + if dkm == k: + return dv + return defaultValue + + + +class DividedInputNodeSize(desc.DynamicNodeSize): + """ + The LDR2HDR will reduce the amount of views in the SfMData. + This class converts the number of LDR input views into the number of HDR output views. + """ + def __init__(self, param, divParam): + super(DividedInputNodeSize, self).__init__(param) + self._divParam = divParam + def computeSize(self, node): + s = super(DividedInputNodeSize, self).computeSize(node) + divParam = node.attribute(self._divParam) + if divParam.value == 0: + return s + return s / divParam.value + class LdrToHdrMerge(desc.CommandLineNode): commandLine = 'aliceVision_LdrToHdrMerge {allParams}' - size = desc.DynamicNodeSize('input') + size = DividedInputNodeSize('input', 'nbBrackets') #parallelization = desc.Parallelization(blockSize=40) #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' @@ -93,4 +130,66 @@ class LdrToHdrMerge(desc.CommandLineNode): value=desc.Node.internalFolder + 'sfmData.sfm', uid=[], ) - ] \ No newline at end of file + ] + + @classmethod + def update(cls, node): + if not isinstance(node.nodeDesc, cls): + raise ValueError("Node {} is not an instance of type {}".format(node, cls)) + # TODO: use Node version for this test + if 'userNbBrackets' not in node.getAttributes().keys(): + # Old version of the node + return + if node.userNbBrackets.value != 0: + node.nbBrackets.value = node.userNbBrackets.value + return + # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion)) + cameraInitOutput = node.input.getLinkParam(recursive=True) + if not cameraInitOutput: + node.nbBrackets.value = 0 + return + viewpoints = cameraInitOutput.node.viewpoints.value + + # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) + inputs = [] + for viewpoint in viewpoints: + jsonMetadata = viewpoint.metadata.value + if not jsonMetadata: + # no metadata, we cannot found the number of brackets + node.nbBrackets.value = 0 + return + d = json.loads(jsonMetadata) + fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "") + shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "") + iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "") + if not fnumber and not shutterSpeed: + # If one image without shutter or fnumber, we cannot found the number of brackets. + # We assume that there is no multi-bracketing, so nothing to do. + node.nbBrackets.value = 1 + return + inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso))) + inputs.sort() + + exposureGroups = [] + exposures = [] + for path, exp in inputs: + if exposures and exp != exposures[-1] and exp == exposures[0]: + exposureGroups.append(exposures) + exposures = [exp] + else: + exposures.append(exp) + exposureGroups.append(exposures) + exposures = None + bracketSizes = set() + if len(exposureGroups) == 1: + node.nbBrackets.value = 1 + else: + for expGroup in exposureGroups: + bracketSizes.add(len(expGroup)) + if len(bracketSizes) == 1: + node.nbBrackets.value = bracketSizes.pop() + # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) + else: + node.nbBrackets.value = 0 + # logging.info("[LDRToHDR] Update end") + diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index 4547a9d13a..ee481c7930 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -1,7 +1,27 @@ __version__ = "2.0" +import json +import os + from meshroom.core import desc +def findMetadata(d, keys, defaultValue): + v = None + for key in keys: + v = d.get(key, None) + k = key.lower() + if v != None: + return v + for dk, dv in d.iteritems(): + dkm = dk.lower().replace(" ", "") + if dkm == key.lower(): + return dv + dkm = dkm.split(":")[-1] + dkm = dkm.split("/")[-1] + if dkm == k: + return dv + return defaultValue + class LdrToHdrSampling(desc.CommandLineNode): commandLine = 'aliceVision_LdrToHdrSampling {allParams}' @@ -74,4 +94,66 @@ class LdrToHdrSampling(desc.CommandLineNode): value=desc.Node.internalFolder, uid=[], ), - ] \ No newline at end of file + ] + + @classmethod + def update(cls, node): + if not isinstance(node.nodeDesc, cls): + raise ValueError("Node {} is not an instance of type {}".format(node, cls)) + # TODO: use Node version for this test + if 'userNbBrackets' not in node.getAttributes().keys(): + # Old version of the node + return + if node.userNbBrackets.value != 0: + node.nbBrackets.value = node.userNbBrackets.value + return + # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion)) + cameraInitOutput = node.input.getLinkParam() + if not cameraInitOutput: + node.nbBrackets.value = 0 + return + viewpoints = cameraInitOutput.node.viewpoints.value + + # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) + inputs = [] + for viewpoint in viewpoints: + jsonMetadata = viewpoint.metadata.value + if not jsonMetadata: + # no metadata, we cannot found the number of brackets + node.nbBrackets.value = 0 + return + d = json.loads(jsonMetadata) + fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "") + shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "") + iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "") + if not fnumber and not shutterSpeed: + # If one image without shutter or fnumber, we cannot found the number of brackets. + # We assume that there is no multi-bracketing, so nothing to do. + node.nbBrackets.value = 1 + return + inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso))) + inputs.sort() + + exposureGroups = [] + exposures = [] + for path, exp in inputs: + if exposures and exp != exposures[-1] and exp == exposures[0]: + exposureGroups.append(exposures) + exposures = [exp] + else: + exposures.append(exp) + exposureGroups.append(exposures) + exposures = None + bracketSizes = set() + if len(exposureGroups) == 1: + node.nbBrackets.value = 1 + else: + for expGroup in exposureGroups: + bracketSizes.add(len(expGroup)) + if len(bracketSizes) == 1: + node.nbBrackets.value = bracketSizes.pop() + # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) + else: + node.nbBrackets.value = 0 + # logging.info("[LDRToHDR] Update end") + From a2390059011c15ffb8ffc16f1612534441a5cc04 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 5 Jul 2020 23:08:57 +0200 Subject: [PATCH 078/100] Update from LDRToHDR node to LdrToHdrMerge node --- meshroom/nodes/aliceVision/LDRToHDR.py | 291 ------------------------- meshroom/ui/reconstruction.py | 4 +- 2 files changed, 2 insertions(+), 293 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/LDRToHDR.py diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py deleted file mode 100644 index ec083e63ba..0000000000 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ /dev/null @@ -1,291 +0,0 @@ -__version__ = "2.0" - -import json -import os - -from meshroom.core import desc - -def findMetadata(d, keys, defaultValue): - v = None - for key in keys: - v = d.get(key, None) - k = key.lower() - if v != None: - return v - for dk, dv in d.iteritems(): - dkm = dk.lower().replace(" ", "") - if dkm == key.lower(): - return dv - dkm = dkm.split(":")[-1] - dkm = dkm.split("/")[-1] - if dkm == k: - return dv - return defaultValue - - -class DividedInputNodeSize(desc.DynamicNodeSize): - """ - The LDR2HDR will reduce the amount of views in the SfMData. - This class converts the number of LDR input views into the number of HDR output views. - """ - def __init__(self, param, divParam): - super(DividedInputNodeSize, self).__init__(param) - self._divParam = divParam - def computeSize(self, node): - s = super(DividedInputNodeSize, self).computeSize(node) - divParam = node.attribute(self._divParam) - if divParam.value == 0: - return s - return s / divParam.value - - -class LDRToHDR(desc.CommandLineNode): - commandLine = 'aliceVision_convertLDRToHDR {allParams}' - size = DividedInputNodeSize('input', 'nbBrackets') - - cpu = desc.Level.INTENSIVE - ram = desc.Level.NORMAL - - documentation=''' -This node fuse LDR (Low Dynamic Range) images with multi-bracketing into HDR (High Dynamic Range) images. - -It is done in 2 steps: - -1/ Estimation of the Camera Response Function (CRF) - -2/ HDR fusion relying on the CRF - -''' - - inputs = [ - desc.File( - name='input', - label='Input', - description="SfM Data File", - value='', - uid=[0], - ), - desc.IntParam( - name='userNbBrackets', - label='Number of Brackets', - description='Manually set the number of exposure brackets per HDR image. Set 0 for automatic detection.', - value=0, - range=(0, 15, 1), - uid=[], - group='user', # not used directly on the command line - ), - desc.IntParam( - name='nbBrackets', - label='Automatic Nb Brackets', - description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".', - value=0, - range=(0, 10, 1), - uid=[0], - ), - desc.FloatParam( - name='highlightCorrectionFactor', - label='Highlights Correction', - description='Pixels saturated in all input images have a partial information about their real luminance.\n' - 'We only know that the value should be >= to the standard hdr fusion.\n' - 'This parameter allows to perform a post-processing step to put saturated pixels to a constant ' - 'value defined by the `highlightsMaxLuminance` parameter.\n' - 'This parameter is float to enable to weight this correction.', - value=1.0, - range=(0.0, 1.0, 0.01), - uid=[0], - ), - desc.FloatParam( - name='highlightTargetLux', - label='Highlight Target Luminance (Lux)', - description='This is an arbitrary target value (in Lux) used to replace the unknown luminance value of the saturated pixels.\n' - '\n' - 'Some Outdoor Reference Light Levels:\n' - ' * 120,000 lux : Brightest sunlight\n' - ' * 110,000 lux : Bright sunlight\n' - ' * 20,000 lux : Shade illuminated by entire clear blue sky, midday\n' - ' * 1,000 lux : Typical overcast day, midday\n' - ' * 400 lux : Sunrise or sunset on a clear day\n' - ' * 40 lux : Fully overcast, sunset/sunrise\n' - '\n' - 'Some Indoor Reference Light Levels:\n' - ' * 20000 lux : Max Usually Used Indoor\n' - ' * 750 lux : Supermarkets\n' - ' * 500 lux : Office Work\n' - ' * 150 lux : Home\n', - value=120000.0, - range=(1000.0, 150000.0, 1.0), - uid=[0], - ), - desc.BoolParam( - name='fisheyeLens', - label='Fisheye Lens', - description="Enable if a fisheye lens has been used.\n " - "This will improve the estimation of the Camera's Response Function by considering only the pixels in the center of the image\n" - "and thus ignore undefined/noisy pixels outside the circle defined by the fisheye lens.", - value=False, - uid=[0], - ), - desc.BoolParam( - name='calibrationRefineExposures', - label='Refine Exposures', - description="Refine exposures provided by metadata (shutter speed, f-number, iso). Only available for 'laguerre' calibration method.", - value=False, - uid=[0], - ), - desc.BoolParam( - name='byPass', - label='bypass convert', - description="Bypass HDR creation and use the medium bracket as the source for the next steps", - value=False, - uid=[0], - advanced=True, - ), - desc.ChoiceParam( - name='calibrationMethod', - label='Calibration Method', - description="Method used for camera calibration \n" - " * Linear: Disable the calibration and assumes a linear Camera Response Function. If images are encoded in a known colorspace (like sRGB for JPEG), the images will be automatically converted to linear. \n" - " * Debevec: This is the standard method for HDR calibration. \n" - " * Grossberg: Based on learned database of cameras, it allows to reduce the CRF to few parameters while keeping all the precision. \n" - " * Laguerre: Simple but robust method estimating the minimal number of parameters. \n" - " * Robertson: First method for HDR calibration in the literature. \n", - values=['linear', 'debevec', 'grossberg', 'laguerre', 'robertson'], - value='debevec', - exclusive=True, - uid=[0], - ), - desc.ChoiceParam( - name='calibrationWeight', - label='Calibration Weight', - description="Weight function used to calibrate camera response \n" - " * default (automatically selected according to the calibrationMethod) \n" - " * gaussian \n" - " * triangle \n" - " * plateau", - value='default', - values=['default', 'gaussian', 'triangle', 'plateau'], - exclusive=True, - uid=[0], - ), - desc.ChoiceParam( - name='fusionWeight', - label='Fusion Weight', - description="Weight function used to fuse all LDR images together:\n" - " * gaussian \n" - " * triangle \n" - " * plateau", - value='gaussian', - values=['gaussian', 'triangle', 'plateau'], - exclusive=True, - uid=[0], - ), - desc.IntParam( - name='calibrationNbPoints', - label='Calibration Nb Points', - description='Internal number of points used for calibration.', - value=0, - range=(0, 10000000, 1000), - uid=[0], - advanced=True, - ), - desc.IntParam( - name='calibrationDownscale', - label='Calibration Downscale', - description='Scaling factor applied to images before calibration of the response function to reduce the impact of misalignment.', - value=4, - range=(1, 16, 1), - uid=[0], - advanced=True, - ), - desc.IntParam( - name='channelQuantizationPower', - label='Channel Quantization Power', - description='Quantization level like 8 bits or 10 bits.', - value=10, - range=(8, 14, 1), - uid=[0], - advanced=True, - ), - desc.ChoiceParam( - name='verboseLevel', - label='Verbose Level', - description='Verbosity level (fatal, error, warning, info, debug, trace).', - value='info', - values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], - exclusive=True, - uid=[], - ), - ] - - outputs = [ - desc.File( - name='outSfMDataFilename', - label='Output SfMData File', - description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'sfmData.sfm', - uid=[], - ) - ] - - @classmethod - def update(cls, node): - if not isinstance(node.nodeDesc, cls): - raise ValueError("Node {} is not an instance of type {}".format(node, cls)) - # TODO: use Node version for this test - if 'userNbBrackets' not in node.getAttributes().keys(): - # Old version of the node - return - if node.userNbBrackets.value != 0: - node.nbBrackets.value = node.userNbBrackets.value - return - # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion)) - cameraInitOutput = node.input.getLinkParam() - if not cameraInitOutput: - node.nbBrackets.value = 0 - return - viewpoints = cameraInitOutput.node.viewpoints.value - - # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) - inputs = [] - for viewpoint in viewpoints: - jsonMetadata = viewpoint.metadata.value - if not jsonMetadata: - # no metadata, we cannot found the number of brackets - node.nbBrackets.value = 0 - return - d = json.loads(jsonMetadata) - fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "") - shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "") - iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "") - if not fnumber and not shutterSpeed: - # If one image without shutter or fnumber, we cannot found the number of brackets. - # We assume that there is no multi-bracketing, so nothing to do. - node.nbBrackets.value = 1 - return - inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso))) - inputs.sort() - - exposureGroups = [] - exposures = [] - for path, exp in inputs: - if exposures and exp != exposures[-1] and exp == exposures[0]: - exposureGroups.append(exposures) - exposures = [exp] - else: - exposures.append(exp) - exposureGroups.append(exposures) - exposures = None - bracketSizes = set() - if len(exposureGroups) == 1: - node.nbBrackets.value = 1 - else: - for expGroup in exposureGroups: - bracketSizes.add(len(expGroup)) - if len(bracketSizes) == 1: - node.nbBrackets.value = bracketSizes.pop() - # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value)) - else: - node.nbBrackets.value = 0 - # logging.info("[LDRToHDR] Update end") - - diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 223dfee3f4..8eecb37f83 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -592,7 +592,7 @@ def updateDepthMapNode(self): def updateLdr2hdrNode(self): """ Set the current LDR2HDR node based on the current CameraInit node. """ - self.ldr2hdr = self.lastNodeOfType(['LDRToHDR'], self.cameraInit) if self.cameraInit else None + self.ldr2hdr = self.lastNodeOfType(['LdrToHdrMerge'], self.cameraInit) if self.cameraInit else None @Slot() def setupLDRToHDRCameraInit(self): @@ -967,7 +967,7 @@ def setActiveNodeOfType(self, node): self.prepareDenseScene = node elif node.nodeType in ("DepthMap", "DepthMapFilter"): self.depthMap = node - elif node.nodeType == "LDRToHDR": + elif node.nodeType == "LdrToHdrMerge": self.ldr2hdr = node elif node.nodeType == "PanoramaInit": self.panoramaInit = node From dc6ac5d67a9c1bdbc9e1c007cbddab8a9b51cb19 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sun, 5 Jul 2020 23:11:07 +0200 Subject: [PATCH 079/100] [nodes] add parallelization to LdrToHdrSampling/Merge and PanoramaWarping --- meshroom/multiview.py | 3 +- .../nodes/aliceVision/LdrToHdrCalibration.py | 21 +++---- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 59 ++++++++++++------- .../nodes/aliceVision/LdrToHdrSampling.py | 32 ++++++++-- .../nodes/aliceVision/PanoramaCompositing.py | 11 +++- meshroom/nodes/aliceVision/PanoramaWarping.py | 3 + 6 files changed, 90 insertions(+), 39 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 96dac95f1c..6d996bd573 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -188,7 +188,8 @@ def hdriPipeline(graph): input=panoramaOrientation.output) panoramaCompositing = graph.addNewNode('PanoramaCompositing', - input=panoramaWarping.output) + input=panoramaWarping.input, + warpingFolder=panoramaWarping.output) imageProcessing = graph.addNewNode('ImageProcessing', input=panoramaCompositing.output, diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index feaa31f204..931d16ccd6 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -91,14 +91,6 @@ class LdrToHdrCalibration(desc.CommandLineNode): range=(0, 10, 1), uid=[], ), - desc.BoolParam( - name='byPass', - label='bypass convert', - description="Bypass HDR creation and use the medium bracket as the source for the next steps", - value=False, - uid=[0], - advanced=True, - ), desc.IntParam( name='channelQuantizationPower', label='Channel Quantization Power', @@ -108,6 +100,17 @@ class LdrToHdrCalibration(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.IntParam( + name='maxTotalPoints', + label='Max Number of Points', + description='Max number of points selected by the sampling strategy.\n'' + 'This ensures that this sampling step will extract a number of pixels values\n' + 'that the calibration step can manage (in term of computation time and memory usage).', + value=1000000, + range=(8, 10000000, 1000), + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', @@ -145,9 +148,7 @@ def update(cls, node): if not cameraInitOutput: node.nbBrackets.value = 0 return - print("LdrToHdrCalib cameraInitOutput: " + str(cameraInitOutput)) viewpoints = cameraInitOutput.node.viewpoints.value - print("LdrToHdrCalib viewpoints: " + str(viewpoints)) # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) inputs = [] diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index f63606cddc..1ebb5f52d0 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -23,28 +23,11 @@ def findMetadata(d, keys, defaultValue): return defaultValue - -class DividedInputNodeSize(desc.DynamicNodeSize): - """ - The LDR2HDR will reduce the amount of views in the SfMData. - This class converts the number of LDR input views into the number of HDR output views. - """ - def __init__(self, param, divParam): - super(DividedInputNodeSize, self).__init__(param) - self._divParam = divParam - def computeSize(self, node): - s = super(DividedInputNodeSize, self).computeSize(node) - divParam = node.attribute(self._divParam) - if divParam.value == 0: - return s - return s / divParam.value - - class LdrToHdrMerge(desc.CommandLineNode): commandLine = 'aliceVision_LdrToHdrMerge {allParams}' - size = DividedInputNodeSize('input', 'nbBrackets') - #parallelization = desc.Parallelization(blockSize=40) - #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=2) + commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' documentation = ''' Calibrate LDR to HDR response curve from samples @@ -97,7 +80,7 @@ class LdrToHdrMerge(desc.CommandLineNode): desc.BoolParam( name='byPass', label='bypass convert', - description="Bypass HDR creation and use the medium bracket as the source for the next steps", + description="Bypass HDR creation and use the medium bracket as the source for the next steps.", value=False, uid=[0], advanced=True, @@ -111,6 +94,40 @@ class LdrToHdrMerge(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.FloatParam( + name='highlightCorrectionFactor', + label='Highlights Correction', + description='Pixels saturated in all input images have a partial information about their real luminance.\n' + 'We only know that the value should be >= to the standard hdr fusion.\n' + 'This parameter allows to perform a post-processing step to put saturated pixels to a constant ' + 'value defined by the `highlightsMaxLuminance` parameter.\n' + 'This parameter is float to enable to weight this correction.', + value=1.0, + range=(0.0, 1.0, 0.01), + uid=[0], + ), + desc.FloatParam( + name='highlightTargetLux', + label='Highlight Target Luminance (Lux)', + description='This is an arbitrary target value (in Lux) used to replace the unknown luminance value of the saturated pixels.\n' + '\n' + 'Some Outdoor Reference Light Levels:\n' + ' * 120,000 lux : Brightest sunlight\n' + ' * 110,000 lux : Bright sunlight\n' + ' * 20,000 lux : Shade illuminated by entire clear blue sky, midday\n' + ' * 1,000 lux : Typical overcast day, midday\n' + ' * 400 lux : Sunrise or sunset on a clear day\n' + ' * 40 lux : Fully overcast, sunset/sunrise\n' + '\n' + 'Some Indoor Reference Light Levels:\n' + ' * 20000 lux : Max Usually Used Indoor\n' + ' * 750 lux : Supermarkets\n' + ' * 500 lux : Office Work\n' + ' * 150 lux : Home\n', + value=120000.0, + range=(1000.0, 150000.0, 1.0), + uid=[0], + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index ee481c7930..048979403c 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -5,6 +5,7 @@ from meshroom.core import desc + def findMetadata(d, keys, defaultValue): v = None for key in keys: @@ -23,11 +24,27 @@ def findMetadata(d, keys, defaultValue): return defaultValue +class DividedInputNodeSize(desc.DynamicNodeSize): + """ + The LDR2HDR will reduce the amount of views in the SfMData. + This class converts the number of LDR input views into the number of HDR output views. + """ + def __init__(self, param, divParam): + super(DividedInputNodeSize, self).__init__(param) + self._divParam = divParam + def computeSize(self, node): + s = super(DividedInputNodeSize, self).computeSize(node) + divParam = node.attribute(self._divParam) + if divParam.value == 0: + return s + return s / divParam.value + + class LdrToHdrSampling(desc.CommandLineNode): commandLine = 'aliceVision_LdrToHdrSampling {allParams}' - size = desc.DynamicNodeSize('input') - #parallelization = desc.Parallelization(blockSize=40) - #commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + size = DividedInputNodeSize('input', 'nbBrackets') + parallelization = desc.Parallelization(blockSize=2) + commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' documentation = ''' Sample pixels from Low range images for HDR creation @@ -64,7 +81,7 @@ class LdrToHdrSampling(desc.CommandLineNode): description="Bypass HDR creation and use the medium bracket as the source for the next steps", value=False, uid=[0], - advanced=True, + group='internal', ), desc.IntParam( name='channelQuantizationPower', @@ -96,6 +113,11 @@ class LdrToHdrSampling(desc.CommandLineNode): ), ] + def processChunk(self, chunk): + if chunk.node.byPass.value: + return + super(LdrToHdrSampling, self).processChunk(chunk) + @classmethod def update(cls, node): if not isinstance(node.nodeDesc, cls): @@ -108,7 +130,7 @@ def update(cls, node): node.nbBrackets.value = node.userNbBrackets.value return # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion)) - cameraInitOutput = node.input.getLinkParam() + cameraInitOutput = node.input.getLinkParam(recursive=True) if not cameraInitOutput: node.nbBrackets.value = 0 return diff --git a/meshroom/nodes/aliceVision/PanoramaCompositing.py b/meshroom/nodes/aliceVision/PanoramaCompositing.py index eea2ae0f13..5b34fc247a 100644 --- a/meshroom/nodes/aliceVision/PanoramaCompositing.py +++ b/meshroom/nodes/aliceVision/PanoramaCompositing.py @@ -20,8 +20,15 @@ class PanoramaCompositing(desc.CommandLineNode): inputs = [ desc.File( name='input', - label='Input', - description="Panorama Warping result", + label='Input SfMData', + description="Input SfMData.", + value='', + uid=[0], + ), + desc.File( + name='warpingFolder', + label='Warping Folder', + description="Panorama Warping results", value='', uid=[0], ), diff --git a/meshroom/nodes/aliceVision/PanoramaWarping.py b/meshroom/nodes/aliceVision/PanoramaWarping.py index c44e7b7a17..7cba255f74 100644 --- a/meshroom/nodes/aliceVision/PanoramaWarping.py +++ b/meshroom/nodes/aliceVision/PanoramaWarping.py @@ -10,6 +10,9 @@ class PanoramaWarping(desc.CommandLineNode): commandLine = 'aliceVision_panoramaWarping {allParams}' size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=5) + commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + documentation = ''' Compute the image warping for each input image in the panorama coordinate system. ''' From 396c285c767b2a7ca76740e26d9c45415de62408 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 6 Jul 2020 00:39:32 +0200 Subject: [PATCH 080/100] [multiview] HDRI: add PanoramaPrepareImages in default HDRI pipeline - update access to CameraInit from LdrToHdr nodes - update PanoramaPrepareImages param names --- meshroom/core/node.py | 3 +++ meshroom/multiview.py | 5 ++++- meshroom/nodes/aliceVision/LdrToHdrCalibration.py | 5 ++++- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 5 ++++- meshroom/nodes/aliceVision/LdrToHdrSampling.py | 3 +++ meshroom/nodes/aliceVision/PanoramaPrepareImages.py | 10 +++++----- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 6b83c6c5d7..a14df45a45 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -498,6 +498,9 @@ def attribute(self, name): def getAttributes(self): return self._attributes + def hasAttribute(self, name): + return name in self._attributes.keys() + def _applyExpr(self): for attr in self._attributes: attr._applyExpr() diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 6d996bd573..6564295dd8 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -145,9 +145,12 @@ def hdriPipeline(graph): except ValueError: pass - ldr2hdrSampling = graph.addNewNode('LdrToHdrSampling', + panoramaPrepareImages = graph.addNewNode('PanoramaPrepareImages', input=cameraInit.output) + ldr2hdrSampling = graph.addNewNode('LdrToHdrSampling', + input=panoramaPrepareImages.output) + ldr2hdrCalibration = graph.addNewNode('LdrToHdrCalibration', input=ldr2hdrSampling.input, samples=ldr2hdrSampling.output) diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 931d16ccd6..c4650139eb 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -103,7 +103,7 @@ class LdrToHdrCalibration(desc.CommandLineNode): desc.IntParam( name='maxTotalPoints', label='Max Number of Points', - description='Max number of points selected by the sampling strategy.\n'' + description='Max number of points selected by the sampling strategy.\n' 'This ensures that this sampling step will extract a number of pixels values\n' 'that the calibration step can manage (in term of computation time and memory usage).', value=1000000, @@ -148,6 +148,9 @@ def update(cls, node): if not cameraInitOutput: node.nbBrackets.value = 0 return + if not cameraInitOutput.node.hasAttribute('viewpoints'): + if cameraInitOutput.node.hasAttribute('input'): + cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True) viewpoints = cameraInitOutput.node.viewpoints.value # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index 1ebb5f52d0..f57064677d 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -99,7 +99,7 @@ class LdrToHdrMerge(desc.CommandLineNode): label='Highlights Correction', description='Pixels saturated in all input images have a partial information about their real luminance.\n' 'We only know that the value should be >= to the standard hdr fusion.\n' - 'This parameter allows to perform a post-processing step to put saturated pixels to a constant ' + 'This parameter allows to perform a post-processing step to put saturated pixels to a constant\n' 'value defined by the `highlightsMaxLuminance` parameter.\n' 'This parameter is float to enable to weight this correction.', value=1.0, @@ -165,6 +165,9 @@ def update(cls, node): if not cameraInitOutput: node.nbBrackets.value = 0 return + if not cameraInitOutput.node.hasAttribute('viewpoints'): + if cameraInitOutput.node.hasAttribute('input'): + cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True) viewpoints = cameraInitOutput.node.viewpoints.value # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index 048979403c..a68538f315 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -134,6 +134,9 @@ def update(cls, node): if not cameraInitOutput: node.nbBrackets.value = 0 return + if not cameraInitOutput.node.hasAttribute('viewpoints'): + if cameraInitOutput.node.hasAttribute('input'): + cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True) viewpoints = cameraInitOutput.node.viewpoints.value # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints))) diff --git a/meshroom/nodes/aliceVision/PanoramaPrepareImages.py b/meshroom/nodes/aliceVision/PanoramaPrepareImages.py index e418feb923..67a6357bb3 100644 --- a/meshroom/nodes/aliceVision/PanoramaPrepareImages.py +++ b/meshroom/nodes/aliceVision/PanoramaPrepareImages.py @@ -2,15 +2,15 @@ from meshroom.core import desc +import os.path + class PanoramaPrepareImages(desc.CommandLineNode): commandLine = 'aliceVision_panoramaPrepareImages {allParams}' size = desc.DynamicNodeSize('input') - # parallelization = desc.Parallelization(blockSize=40) - # commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' documentation = ''' -Prepare images for panorama Estimation +Prepare images for Panorama pipeline: ensures that images orientations are coherent. ''' inputs = [ @@ -34,10 +34,10 @@ class PanoramaPrepareImages(desc.CommandLineNode): outputs = [ desc.File( - name='outSfMData', + name='output', label='Output sfmData', description='Output sfmData.', - value=desc.Node.internalFolder + 'sfmData.abc', + value=lambda attr: desc.Node.internalFolder + os.path.basename(attr.node.input.value), uid=[], ), ] From 2ec218c08613fc4ea5d91823820b90784c24791d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 6 Jul 2020 11:43:28 +0200 Subject: [PATCH 081/100] [nodes] LdrToHdrSampling: expose new advanced params --- .../nodes/aliceVision/LdrToHdrSampling.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index a68538f315..7222308edd 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -92,6 +92,33 @@ class LdrToHdrSampling(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.IntParam( + name='blockSize', + label='Block Size', + description='Size of the image tile to extract a sample.', + value=256, + range=(8, 1024, 1), + uid=[0], + advanced=True, + ), + desc.IntParam( + name='radius', + label='Patch Radius', + description='Radius of the patch used to analyze the sample statistics.', + value=5, + range=(0, 10, 1), + uid=[0], + advanced=True, + ), + desc.IntParam( + name='maxCountSample', + label='Max Number of Samples', + description='Max number of samples per image group.', + value=200, + range=(10, 1000, 10), + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From ea5b6392457fe809158802cd2bab0f036a01a62e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 7 Jul 2020 11:35:40 +0200 Subject: [PATCH 082/100] [ui] minor fix when viewpoints is None --- meshroom/ui/reconstruction.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 8eecb37f83..2906da435d 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -1083,7 +1083,10 @@ def setSelectedViewpoint(self, viewpointAttribute): def reconstructedCamerasCount(self): """ Get the number of reconstructed cameras in the current context. """ - return len([v for v in self.getViewpoints() if self.isReconstructed(v)]) + viewpoints = self.getViewpoints() + if not viewpoints: + return 0 + return len([v for v in viewpoints if self.isReconstructed(v)]) @Slot(QObject, result="QVariant") def getSolvedIntrinsics(self, viewpoint): From ec67c772fa0b6f6c6372ea96fe9e659b50f437b2 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 7 Jul 2020 22:05:53 +0200 Subject: [PATCH 083/100] [ui] new generic way to manage "active" nodes per node type --- meshroom/core/graph.py | 2 +- .../ui/qml/ImageGallery/ImageDelegate.qml | 5 +- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 119 +++++++--- meshroom/ui/qml/MaterialIcons/MLabel.qml | 23 ++ .../qml/MaterialIcons/MaterialToolButton.qml | 2 + .../qml/MaterialIcons/MaterialToolLabel.qml | 45 ++++ .../MaterialIcons/MaterialToolLabelButton.qml | 51 +++++ meshroom/ui/qml/MaterialIcons/qmldir | 3 + meshroom/ui/qml/Viewer/Viewer2D.qml | 82 +++---- meshroom/ui/qml/WorkspaceView.qml | 4 +- meshroom/ui/qml/main.qml | 3 +- meshroom/ui/reconstruction.py | 204 +++++++----------- 12 files changed, 341 insertions(+), 202 deletions(-) create mode 100644 meshroom/ui/qml/MaterialIcons/MLabel.qml create mode 100644 meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml create mode 100644 meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 7cde3e26aa..7cf21eebdf 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -881,7 +881,7 @@ def nodesFromNode(self, startNode, filterTypes=None): filterTypes (str list): (optional) only return the nodes of the given types (does not stop the visit, this is a post-process only) Returns: - The list of nodes from startNode to the graph leaves following edges. + The list of nodes and edges, from startNode to the graph leaves following edges. """ nodes = [] edges = [] diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml index c96a7a57bb..bc5beb116b 100644 --- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml +++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml @@ -58,8 +58,9 @@ Item { } MenuItem { text: "Define As Center Image" - enabled: !root.readOnly && _viewpoint.viewId != -1 && _reconstruction && _reconstruction.sfmTransform - onClicked: _reconstruction.sfmTransform.attribute("transformation").value = _viewpoint.viewId.toString() + property var activeNode: _reconstruction.activeNodes.get("SfMTransform").node + enabled: !root.readOnly && _viewpoint.viewId != -1 && _reconstruction && activeNode + onClicked: activeNode.attribute("transformation").value = _viewpoint.viewId.toString() } } diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 1fe1e72a02..d27b97167d 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -16,7 +16,7 @@ Panel { property variant cameraInits property variant cameraInit - property variant hdrCameraInit + property variant tempCameraInit readonly property alias currentItem: grid.currentItem readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : "" readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined @@ -38,7 +38,7 @@ Panel { QtObject { id: m - property variant currentCameraInit: displayHDR.checked ? _reconstruction.hdrCameraInit : root.cameraInit + property variant currentCameraInit: _reconstruction.tempCameraInit ? _reconstruction.tempCameraInit : root.cameraInit property variant viewpoints: currentCameraInit ? currentCameraInit.attribute('viewpoints').value : undefined property bool readOnly: root.readOnly || displayHDR.checked } @@ -194,7 +194,7 @@ Panel { // Center of SfMTransform Loader { id: sfmTransformIndicator - active: (viewpoint.get("viewId").value == centerViewId) + active: viewpoint && (viewpoint.get("viewId").value == centerViewId) sourceComponent: ImageBadge { text: MaterialIcons.gamepad ToolTip.text: "Camera used to define the center of the scene." @@ -343,54 +343,114 @@ Panel { } footerContent: RowLayout { + // Images count + MaterialToolLabel { + ToolTip.text: grid.model.count + " Input Images" + iconText: MaterialIcons.image + label: grid.model.count.toString() + // enabled: grid.model.count > 0 + // margin: 4 + } + // cameras count + MaterialToolLabel { + ToolTip.text: label + " Estimated Cameras" + iconText: MaterialIcons.videocam + label: _reconstruction ? _reconstruction.nbCameras.toString() : "0" + // margin: 4 + // enabled: _reconstruction.cameraInit && _reconstruction.nbCameras + } - // Image count - RowLayout { - Layout.fillWidth: true - spacing: 8 - RowLayout { - MaterialLabel { text: MaterialIcons.image } - Label { text: grid.model.count } + Item { Layout.fillHeight: true; Layout.fillWidth: true } + + MaterialToolLabelButton { + id: displayHDR + property var activeNode: _reconstruction.activeNodes.get("LdrToHdrMerge").node + ToolTip.text: "Visualize HDR images: " + (activeNode ? activeNode.label : "No Node") + iconText: MaterialIcons.filter + label: activeNode ? activeNode.attribute("nbBrackets").value : "" + // visible: activeNode + enabled: activeNode && activeNode.isComputed + property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : "" + onNodeIDChanged: { + if(checked) { + open(); + } } - RowLayout { - visible: _reconstruction.cameraInit && _reconstruction.nbCameras - MaterialLabel { text: MaterialIcons.videocam } - Label { text: _reconstruction.cameraInit ? _reconstruction.nbCameras : 0 } + onEnabledChanged: { + // Reset the toggle to avoid getting stuck + // with the HDR node checked but disabled. + if(checked) { + checked = false; + close(); + } + } + checkable: true + checked: false + onClicked: { + if(checked) { + open(); + } else { + close(); + } + } + function open() { + if(imageProcessing.checked) + imageProcessing.checked = false; + _reconstruction.setupTempCameraInit(activeNode, "outSfMDataFilename"); + } + function close() { + _reconstruction.clearTempCameraInit(); } } MaterialToolButton { - id: displayHDR - font.pointSize: 20 + id: imageProcessing + property var activeNode: _reconstruction.activeNodes.get("ImageProcessing").node + font.pointSize: 15 padding: 0 - anchors.margins: 0 - implicitHeight: 14 - ToolTip.text: "Visualize HDR images: " + (_reconstruction.ldr2hdr ? _reconstruction.ldr2hdr.label : "No Node") - text: MaterialIcons.hdr_on - visible: _reconstruction.ldr2hdr - enabled: _reconstruction.ldr2hdr && _reconstruction.ldr2hdr.isComputed - property string nodeID: _reconstruction.ldr2hdr ? (_reconstruction.ldr2hdr.label + _reconstruction.ldr2hdr.isComputed) : "" + ToolTip.text: "Preprocessed Images: " + (activeNode ? activeNode.label : "No Node") + text: MaterialIcons.wallpaper + visible: activeNode && activeNode.attribute("outSfMData").value + enabled: activeNode && activeNode.isComputed + property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : "" onNodeIDChanged: { - if(checked) - { - _reconstruction.setupLDRToHDRCameraInit(); + if(checked) { + open(); } } onEnabledChanged: { // Reset the toggle to avoid getting stuck // with the HDR node checked but disabled. - checked = false; + if(checked) { + checked = false; + close(); + } } checkable: true checked: false - onClicked: { _reconstruction.setupLDRToHDRCameraInit(); } + onClicked: { + if(checked) { + open(); + } else { + close(); + } + } + function open() { + if(displayHDR.checked) + displayHDR.checked = false; + _reconstruction.setupTempCameraInit(activeNode, "outSfMData"); + } + function close() { + _reconstruction.clearTempCameraInit(); + } } - Item { Layout.fillHeight: true; Layout.fillWidth: true } + Item { Layout.fillHeight: true; width: 1 } // Thumbnail size icon and slider MaterialToolButton { text: MaterialIcons.photo_size_select_large + ToolTip.text: "Thumbnails Scale" padding: 0 anchors.margins: 0 font.pointSize: 11 @@ -404,5 +464,4 @@ Panel { implicitWidth: 70 } } - } diff --git a/meshroom/ui/qml/MaterialIcons/MLabel.qml b/meshroom/ui/qml/MaterialIcons/MLabel.qml new file mode 100644 index 0000000000..2251a6b6ed --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MLabel.qml @@ -0,0 +1,23 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.4 + + +/** + * MLabel is a standard Label. + * If ToolTip.text is set, it shows up a tooltip when hovered. + */ +Label { + padding: 4 + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + ToolTip.visible: mouseArea.containsMouse + ToolTip.delay: 500 + background: Rectangle { + anchors.fill: parent + color: mouseArea.containsMouse ? Qt.darker(parent.palette.base, 0.6) : "transparent" + } +} diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml index 2eea541122..b24e2ad457 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolButton.qml @@ -1,5 +1,6 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 /** @@ -7,6 +8,7 @@ import QtQuick.Controls 2.3 * It also shows up its tooltip when hovered. */ ToolButton { + id: control font.family: MaterialIcons.fontFamily padding: 4 font.pointSize: 13 diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml new file mode 100644 index 0000000000..b32df53d2e --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml @@ -0,0 +1,45 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + + +/** + * MaterialToolLabel is a Label with an icon (using MaterialIcons). + * It shows up its tooltip when hovered. + */ +Item { + id: control + property alias iconText: icon.text + property alias iconSize: icon.font.pointSize + property alias label: labelItem.text + width: childrenRect.width + height: childrenRect.height + + RowLayout { + Label { + id: icon + font.family: MaterialIcons.fontFamily + font.pointSize: 13 + padding: 0 + text: "" + color: palette.text + } + Label { + id: labelItem + text: "" + color: palette.text + } + Item { + width: 5 + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + ToolTip.visible: mouseArea.containsMouse + ToolTip.delay: 500 +} diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml new file mode 100644 index 0000000000..6613dd5133 --- /dev/null +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabelButton.qml @@ -0,0 +1,51 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + + +/** + * MaterialToolButton is a standard ToolButton using MaterialIcons font. + * It also shows up its tooltip when hovered. + */ +ToolButton { + id: control + property alias iconText: icon.text + property alias iconSize: icon.font.pointSize + property alias label: labelItem.text + padding: 0 + ToolTip.visible: ToolTip.text && hovered + ToolTip.delay: 100 + width: childrenRect.width + height: childrenRect.height + contentItem: RowLayout { + Layout.margins: 0 + Label { + id: icon + font.family: MaterialIcons.fontFamily + font.pointSize: 13 + padding: 0 + text: "" + color: (checked ? palette.highlight : palette.text) + } + Label { + id: labelItem + text: "" + padding: 0 + color: (checked ? palette.highlight : palette.text) + } + } + background: Rectangle { + color: { + if(pressed || checked || hovered) + { + if(pressed || checked) + return Qt.darker(parent.palette.base, 1.3) + if(hovered) + return Qt.darker(parent.palette.base, 0.6) + } + return "transparent"; + } + + border.color: checked ? Qt.darker(parent.palette.base, 1.4) : "transparent" + } +} diff --git a/meshroom/ui/qml/MaterialIcons/qmldir b/meshroom/ui/qml/MaterialIcons/qmldir index c3d64e4b28..4160608111 100644 --- a/meshroom/ui/qml/MaterialIcons/qmldir +++ b/meshroom/ui/qml/MaterialIcons/qmldir @@ -1,4 +1,7 @@ module MaterialIcons singleton MaterialIcons 2.2 MaterialIcons.qml MaterialToolButton 2.2 MaterialToolButton.qml +MaterialToolLabelButton 2.2 MaterialToolLabelButton.qml +MaterialToolLabel 2.2 MaterialToolLabel.qml MaterialLabel 2.2 MaterialLabel.qml +MLabel 2.2 MLabel.qml diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 93dd3171bd..03b80089dc 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -104,10 +104,11 @@ FocusScope { } function getImageFile(type) { + var depthMapNode = _reconstruction.activeNodes.get('allDepthMap').node; 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"); + } else if (depthMapNode != undefined && _reconstruction.selectedViewId >= 0) { + return Filepath.stringToUrl(depthMapNode.internalFolder+_reconstruction.selectedViewId+"_"+type+"Map.exr"); } return ""; } @@ -245,8 +246,8 @@ FocusScope { // note: requires QtAliceVision plugin - use a Loader to evaluate plugin availability at runtime Loader { id: featuresViewerLoader - active: displayFeatures.checked + property var activeNode: _reconstruction.activeNodes.get("FeatureExtraction").node // handle rotation/position based on available metadata rotation: { @@ -265,8 +266,8 @@ FocusScope { // 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) : ""; }), + 'model': Qt.binding(function() { return activeNode ? activeNode.attribute("describerTypes").value : ""; }), + 'featureFolder': Qt.binding(function() { return activeNode ? Filepath.stringToUrl(activeNode.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; }), }) @@ -281,7 +282,8 @@ FocusScope { // note: use a Loader to evaluate if a PanoramaInit node exist and displayFisheyeCircle checked at runtime Loader { anchors.centerIn: parent - active: (displayFisheyeCircleLoader.checked && _reconstruction.panoramaInit) + property var activeNode: _reconstruction.activeNodes.get("PanoramaInit").node + active: (displayFisheyeCircleLoader.checked && activeNode) // handle rotation/position based on available metadata rotation: { @@ -294,28 +296,28 @@ FocusScope { } sourceComponent: CircleGizmo { - property bool useAuto: _reconstruction.panoramaInit.attribute("estimateFisheyeCircle").value + property bool useAuto: activeNode.attribute("estimateFisheyeCircle").value readOnly: useAuto - visible: (!useAuto) || _reconstruction.panoramaInit.isComputed - property real userFisheyeRadius: _reconstruction.panoramaInit.attribute("fisheyeRadius").value - property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(_reconstruction.panoramaInit) + visible: (!useAuto) || activeNode.isComputed + property real userFisheyeRadius: activeNode.attribute("fisheyeRadius").value + property variant fisheyeAutoParams: _reconstruction.getAutoFisheyeCircle(activeNode) - x: useAuto ? fisheyeAutoParams.x : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value - y: useAuto ? fisheyeAutoParams.y : _reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value + x: useAuto ? fisheyeAutoParams.x : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x").value + y: useAuto ? fisheyeAutoParams.y : activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y").value radius: useAuto ? fisheyeAutoParams.z : ((imgContainer.image ? Math.min(imgContainer.image.width, imgContainer.image.height) : 1.0) * 0.5 * (userFisheyeRadius * 0.01)) border.width: Math.max(1, (3.0 / imgContainer.scale)) onMoved: { if(!useAuto) { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) + _reconstruction.setAttribute(activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_x"), x) + _reconstruction.setAttribute(activeNode.attribute("fisheyeCenterOffset.fisheyeCenterOffset_y"), y) } } onIncrementRadius: { if(!useAuto) { - _reconstruction.setAttribute(_reconstruction.panoramaInit.attribute("fisheyeRadius"), _reconstruction.panoramaInit.attribute("fisheyeRadius").value + radiusOffset) + _reconstruction.setAttribute(activeNode.attribute("fisheyeRadius"), activeNode.attribute("fisheyeRadius").value + radiusOffset) } } } @@ -352,8 +354,9 @@ FocusScope { // show which depthmap node is active Label { id: depthMapNodeName - visible: (_reconstruction.depthMap != undefined) && (imageType.type != "image") - text: (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "") + property var activeNode: _reconstruction.activeNodes.get("allDepthMap").node + visible: (activeNode != undefined) && (imageType.type != "image") + text: (activeNode != undefined ? activeNode.label : "") font.pointSize: 8 horizontalAlignment: TextInput.AlignLeft @@ -422,10 +425,9 @@ FocusScope { } Loader { id: mtracksLoader - // active: _reconstruction.featureMatching property bool isUsed: displayFeatures.checked || displaySfmStatsView.checked || displaySfmDataGlobalStats.checked - property var activeNode: _reconstruction.featureMatching + property var activeNode: _reconstruction.activeNodes.get('FeatureMatching').node property bool isComputed: activeNode && activeNode.isComputed active: false @@ -498,7 +500,7 @@ FocusScope { active: displayFeatures.checked && featuresViewerLoader.status === Loader.Ready sourceComponent: FeaturesInfoOverlay { - featureExtractionNode: _reconstruction.featureExtraction + featureExtractionNode: _reconstruction.activeNodes.get('FeatureExtraction').node pluginStatus: featuresViewerLoader.status featuresViewer: featuresViewerLoader.item } @@ -514,9 +516,8 @@ FocusScope { anchors.fill: parent // zoom label - Label { + MLabel { text: ((imgContainer.image && (imgContainer.image.status === Image.Ready)) ? imgContainer.scale.toFixed(2) : "1.00") + "x" - state: "xsmall" MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton @@ -533,6 +534,7 @@ FocusScope { } } } + ToolTip.text: "Zoom" } MaterialToolButton { id: displayAlphaBackground @@ -566,31 +568,30 @@ FocusScope { } MaterialToolButton { id: displayFisheyeCircleLoader - ToolTip.text: "Display Fisheye Circle: " + (_reconstruction.panoramaInit ? _reconstruction.panoramaInit.label : "No Node") - text: MaterialIcons.panorama_fish_eye + property var activeNode: _reconstruction.activeNodes.get('PanoramaInit').node + 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: _reconstruction.panoramaInit && _reconstruction.panoramaInit.attribute("useFisheye").value - visible: _reconstruction.panoramaInit + enabled: activeNode && activeNode.attribute("useFisheye").value + visible: activeNode } Label { id: resolutionLabel Layout.fillWidth: true - text: imgContainer.image ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" + text: (imgContainer.image && imgContainer.image.sourceSize.width > 0) ? (imgContainer.image.sourceSize.width + "x" + imgContainer.image.sourceSize.height) : "" elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter - /*Rectangle { - anchors.fill: parent - color: "blue" - }*/ } ComboBox { id: imageType + property var activeNode: _reconstruction.activeNodes.get('allDepthMap').node // set min size to 5 characters + one margin for the combobox clip: true Layout.minimumWidth: 0 @@ -601,12 +602,13 @@ FocusScope { property string type: enabled ? types[currentIndex] : types[0] model: types - enabled: _reconstruction.depthMap != undefined + enabled: activeNode } MaterialToolButton { - enabled: _reconstruction.depthMap != undefined - ToolTip.text: "View Depth Map in 3D (" + (_reconstruction.depthMap != undefined ? _reconstruction.depthMap.label : "No DepthMap Node Selected") + ")" + property var activeNode: _reconstruction.activeNodes.get('allDepthMap').node + enabled: activeNode + ToolTip.text: "View Depth Map in 3D (" + (activeNode ? activeNode.label : "No DepthMap Node Selected") + ")" text: MaterialIcons.input font.pointSize: 11 Layout.minimumWidth: 0 @@ -618,6 +620,7 @@ FocusScope { MaterialToolButton { id: displaySfmStatsView + property var activeNode: _reconstruction.activeNodes.get('sfm').node font.family: MaterialIcons.fontFamily text: MaterialIcons.assessment @@ -630,10 +633,9 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed && _reconstruction.selectedViewId >= 0 + enabled: activeNode && activeNode.isComputed && _reconstruction.selectedViewId >= 0 onCheckedChanged: { - if(checked == true) - { + if(checked == true) { displaySfmDataGlobalStats.checked = false metadataCB.checked = false } @@ -642,6 +644,7 @@ FocusScope { MaterialToolButton { id: displaySfmDataGlobalStats + property var activeNode: _reconstruction.activeNodes.get('sfm').node font.family: MaterialIcons.fontFamily text: MaterialIcons.language @@ -654,10 +657,9 @@ FocusScope { smooth: false flat: true checkable: enabled - enabled: _reconstruction.sfm && _reconstruction.sfm.isComputed + enabled: activeNode && activeNode.isComputed onCheckedChanged: { - if(checked == true) - { + if(checked == true) { displaySfmStatsView.checked = false metadataCB.checked = false } diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 3a5e80fb18..4caafc0204 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -65,7 +65,7 @@ Item { readOnly: root.readOnly cameraInits: root.cameraInits cameraInit: reconstruction.cameraInit - hdrCameraInit: reconstruction.hdrCameraInit + tempCameraInit: reconstruction.tempCameraInit currentIndex: reconstruction.cameraInitIndex onRemoveImageRequest: reconstruction.removeAttribute(attribute) onFilesDropped: reconstruction.handleFilesDrop(drop, augmentSfm ? null : cameraInit) @@ -191,7 +191,7 @@ Item { mediaLibrary: viewer3D.library camera: viewer3D.mainCamera uigraph: reconstruction - onNodeActivated: _reconstruction.setActiveNodeOfType(node) + onNodeActivated: _reconstruction.setActiveNode(node) } } } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index d82aea30b8..c4249fbd73 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -703,7 +703,6 @@ ApplicationWindow { } } - GraphEditor { id: graphEditor @@ -713,7 +712,7 @@ ApplicationWindow { readOnly: graphLocked onNodeDoubleClicked: { - _reconstruction.setActiveNodeOfType(node); + _reconstruction.setActiveNode(node); let viewable = false; for(var i=0; i < node.attributes.count; ++i) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 2906da435d..be17bd3de9 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -8,6 +8,7 @@ from PySide2.QtGui import QMatrix4x4, QMatrix3x3, QQuaternion, QVector3D, QVector2D import meshroom.core +import meshroom.common from meshroom import multiview from meshroom.common.qt import QObjectListModel from meshroom.core import Version @@ -196,6 +197,7 @@ def __init__(self, viewpointAttribute, reconstruction): self._reconstructed = False # PrepareDenseScene self._undistortedImagePath = '' + self._activeNode_PrepareDenseScene = self._reconstruction.activeNodes.get("PrepareDenseScene") # update internally cached variables self._updateInitialParams() @@ -205,7 +207,7 @@ def __init__(self, viewpointAttribute, reconstruction): # trigger internal members updates when reconstruction members changes self._reconstruction.cameraInitChanged.connect(self._updateInitialParams) self._reconstruction.sfmReportChanged.connect(self._updateSfMParams) - self._reconstruction.prepareDenseSceneChanged.connect(self._updateDenseSceneParams) + self._activeNode_PrepareDenseScene.nodeChanged.connect(self._updateDenseSceneParams) def _updateInitialParams(self): """ Update internal members depending on CameraInit. """ @@ -235,11 +237,11 @@ def _updateSfMParams(self): def _updateDenseSceneParams(self): """ Update internal members depending on PrepareDenseScene. """ # undistorted image path - if not self._reconstruction.prepareDenseScene: + if not self._activeNode_PrepareDenseScene.node: self._undistortedImagePath = '' else: - filename = "{}.{}".format(self._viewpoint.viewId.value, self._reconstruction.prepareDenseScene.outputFileType.value) - self._undistortedImagePath = os.path.join(self._reconstruction.prepareDenseScene.output.value, filename) + filename = "{}.{}".format(self._viewpoint.viewId.value, self._activeNode_PrepareDenseScene.node.outputFileType.value) + self._undistortedImagePath = os.path.join(self._activeNode_PrepareDenseScene.node.output.value, filename) self.denseSceneParamsChanged.emit() @Property(type=QObject, constant=True) @@ -388,36 +390,49 @@ def parseSfMJsonFile(sfmJsonFile): return views, poses, intrinsics -sfmHolderNodeTypes = ["StructureFromMotion", "GlobalSfM", "PanoramaEstimation", "SfMTransfer", "SfMTransform", "SfMAlignment"] +class ActiveNode(QObject): + """ + Hold one active node for a given NodeType. + """ + def __init__(self, nodeType, parent=None): + super(ActiveNode, self).__init__(parent) + self.nodeType = nodeType + self._node = None + + nodeChanged = Signal() + node = makeProperty(QObject, "_node", nodeChanged, resetOnDestroy=True) class Reconstruction(UIGraph): """ Specialization of a UIGraph designed to manage a 3D reconstruction. """ + activeNodeCategories = { + "sfm": ["StructureFromMotion", "GlobalSfM", "PanoramaEstimation", "SfMTransfer", "SfMTransform", + "SfMAlignment"], + "undistort": ["PrepareDenseScene", "PanoramaWarping"], + "allDepthMap": ["DepthMap", "DepthMapFilter"], + } def __init__(self, defaultPipeline='', parent=None): super(Reconstruction, self).__init__(parent) # initialize member variables for key steps of the 3D reconstruction pipeline + self._activeNodes = meshroom.common.DictModel(keyAttrName="nodeType") + self.initActiveNodes() + # - CameraInit self._cameraInit = None # current CameraInit node self._cameraInits = QObjectListModel(parent=self) # all CameraInit nodes self._buildingIntrinsics = False self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) - self._hdrCameraInit = None + self.cameraInitChanged.connect(self.onCameraInitChanged) - self.importImagesFailed.connect(self.onImportImagesFailed) + self._tempCameraInit = None - # - Feature Extraction - self._featureExtraction = None - self.cameraInitChanged.connect(self.updateFeatureExtraction) - - # - Feature Matching - self._featureMatching = None - self.cameraInitChanged.connect(self.updateFeatureMatching) + self.importImagesFailed.connect(self.onImportImagesFailed) # - SfM self._sfm = None @@ -428,28 +443,6 @@ def __init__(self, defaultPipeline='', parent=None): self._selectedViewpoint = None self._liveSfmManager = LiveSfmManager(self) - # - Prepare Dense Scene (undistorted images) - self._prepareDenseScene = None - - # - Depth Map - self._depthMap = None - self.cameraInitChanged.connect(self.updateDepthMapNode) - - # - Texturing - self._texturing = None - - # - LDR2HDR - self._ldr2hdr = None - self.cameraInitChanged.connect(self.updateLdr2hdrNode) - - # - PanoramaInit - self._panoramaInit = None - self.cameraInitChanged.connect(self.updatePanoramaInitNode) - - # - PanoramaInit - self._sfmTransform = None - self.cameraInitChanged.connect(self.updateSfMTransformNode) - # react to internal graph changes to update those variables self.graphChanged.connect(self.onGraphChanged) @@ -458,6 +451,18 @@ def __init__(self, defaultPipeline='', parent=None): def setDefaultPipeline(self, defaultPipeline): self._defaultPipeline = defaultPipeline + def initActiveNodes(self): + # Create all possible entries + for category, _ in self.activeNodeCategories.iteritems(): + self._activeNodes.add(ActiveNode(category, self)) + for nodeType, _ in meshroom.core.nodesDesc.iteritems(): + self._activeNodes.add(ActiveNode(nodeType, self)) + + def onCameraInitChanged(self): + # Update active nodes when CameraInit changes + nodes = self._graph.nodesFromNode(self._cameraInit)[0] + self.setActiveNodes(nodes) + @Slot() @Slot(str) def new(self, pipeline=None): @@ -528,16 +533,8 @@ def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() self.selectedViewId = "-1" - self.featureExtraction = None - self.featureMatching = None self.sfm = None - self.prepareDenseScene = None - self.depthMap = None - self.texturing = None - self.ldr2hdr = None - self.hdrCameraInit = None - self.panoramaInit = None - self.sfmTransform = None + self.tempCameraInit = None self.updateCameraInits() if not self._graph: return @@ -578,35 +575,23 @@ def setCameraInitIndex(self, idx): camInit = self._cameraInits[idx] if self._cameraInits else None self.cameraInit = camInit - def updateFeatureExtraction(self): - """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.featureExtraction = self.lastNodeOfType(['FeatureExtraction'], self.cameraInit) if self.cameraInit else None - - def 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 - - def updateLdr2hdrNode(self): - """ Set the current LDR2HDR node based on the current CameraInit node. """ - self.ldr2hdr = self.lastNodeOfType(['LdrToHdrMerge'], self.cameraInit) if self.cameraInit else None - @Slot() - def setupLDRToHDRCameraInit(self): - if not self.ldr2hdr: - self.hdrCameraInit = Node("CameraInit") + def clearTempCameraInit(self): + self.tempCameraInit = None + + @Slot(QObject, str) + def setupTempCameraInit(self, node, attrName): + if not node or not attrName: + self.tempCameraInit = None return - sfmFile = self.ldr2hdr.attribute("outSfMDataFilename").value + sfmFile = node.attribute(attrName).value if not sfmFile or not os.path.isfile(sfmFile): - self.hdrCameraInit = Node("CameraInit") + self.tempCameraInit = None return nodeDesc = meshroom.core.nodesDesc["CameraInit"]() views, intrinsics = nodeDesc.readSfMData(sfmFile) tmpCameraInit = Node("CameraInit", viewpoints=views, intrinsics=intrinsics) - self.hdrCameraInit = tmpCameraInit + self.tempCameraInit = tmpCameraInit @Slot(QObject, result=QVector3D) def getAutoFisheyeCircle(self, panoramaInit): @@ -633,17 +618,9 @@ def getAutoFisheyeCircle(self, panoramaInit): float(intrinsic.get("fisheyeCircleRadius", 0.0))) return res - def updatePanoramaInitNode(self): - """ Set the current FeatureExtraction node based on the current CameraInit node. """ - self.panoramaInit = self.lastNodeOfType(["PanoramaInit"], self.cameraInit) if self.cameraInit else None - - def updateSfMTransformNode(self): - """ Set the current SfMTransform node based on the current CameraInit node. """ - self.sfmTransform = self.lastNodeOfType(["SfMTransform"], self.cameraInit) if self.cameraInit else None - def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ - return self.lastNodeOfType(sfmHolderNodeTypes, self._cameraInit, Status.SUCCESS) + return self.lastNodeOfType(self.activeNodeCategories['sfm'], self._cameraInit, Status.SUCCESS) def lastNodeOfType(self, nodeTypes, startNode, preferredStatus=None): """ @@ -938,10 +915,11 @@ def setBuildingIntrinsics(self, value): self._buildingIntrinsics = value self.buildingIntrinsicsChanged.emit() + activeNodes = makeProperty(QObject, "_activeNodes", resetOnDestroy=True) cameraInitChanged = Signal() cameraInit = makeProperty(QObject, "_cameraInit", cameraInitChanged, resetOnDestroy=True) - hdrCameraInitChanged = Signal() - hdrCameraInit = makeProperty(QObject, "_hdrCameraInit", hdrCameraInitChanged, resetOnDestroy=True) + tempCameraInitChanged = Signal() + tempCameraInit = makeProperty(QObject, "_tempCameraInit", tempCameraInitChanged, resetOnDestroy=True) cameraInitIndex = Property(int, getCameraInitIndex, setCameraInitIndex, notify=cameraInitChanged) viewpoints = Property(QObject, getViewpoints, notify=cameraInitChanged) cameraInits = Property(QObject, lambda self: self._cameraInits, constant=True) @@ -952,27 +930,30 @@ def setBuildingIntrinsics(self, value): liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) @Slot(QObject) - def setActiveNodeOfType(self, node): + def setActiveNode(self, node): """ Set node as the active node of its type. """ - if node.nodeType in sfmHolderNodeTypes: - self.sfm = node - - if node.nodeType == "FeatureExtraction": - self.featureExtraction = node - elif node.nodeType == "FeatureMatching": - self.featureMatching = node - elif node.nodeType == "CameraInit": - self.cameraInit = node - elif node.nodeType == "PrepareDenseScene": - self.prepareDenseScene = node - elif node.nodeType in ("DepthMap", "DepthMapFilter"): - self.depthMap = node - elif node.nodeType == "LdrToHdrMerge": - self.ldr2hdr = node - elif node.nodeType == "PanoramaInit": - self.panoramaInit = node - elif node.nodeType == "SfMTransform": - self.sfmTransform = node + for category, nodeTypes in self.activeNodeCategories.iteritems(): + if node.nodeType in nodeTypes: + self.activeNodes.get(category).node = node + if category == 'sfm': + self.setSfm(node) + self.activeNodes.get(node.nodeType).node = node + + @Slot(QObject) + def setActiveNodes(self, nodes): + """ Set node as the active node of its type. """ + # Setup the active node per category only once, on the last one + nodesByCategory = {} + for node in nodes: + for category, nodeTypes in self.activeNodeCategories.iteritems(): + if node.nodeType in nodeTypes: + nodesByCategory[category] = node + for category, node in nodesByCategory.iteritems(): + self.activeNodes.get(category).node = node + if category == 'sfm': + self.setSfm(node) + for node in nodes: + self.activeNodes.get(node.nodeType).node = node def updateSfMResults(self): """ @@ -1021,9 +1002,6 @@ def setSfm(self, node): self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) - self.texturing = self.lastNodeOfType(["Texturing"], self._sfm, Status.SUCCESS) - self.prepareDenseScene = self.lastNodeOfType(["PrepareDenseScene"], self._sfm, Status.SUCCESS) - @Slot(QObject, result=bool) def isInViews(self, viewpoint): if not viewpoint: @@ -1129,35 +1107,11 @@ def getPoseRT(self, viewpoint): sfmChanged = Signal() sfm = Property(QObject, getSfm, setSfm, notify=sfmChanged) - 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) sfmAugmented = Signal(Node, Node) - prepareDenseSceneChanged = Signal() - prepareDenseScene = makeProperty(QObject, "_prepareDenseScene", notify=prepareDenseSceneChanged, resetOnDestroy=True) - - depthMapChanged = Signal() - depthMap = makeProperty(QObject, "_depthMap", depthMapChanged, resetOnDestroy=True) - - texturingChanged = Signal() - texturing = makeProperty(QObject, "_texturing", notify=texturingChanged, resetOnDestroy=True) - - ldr2hdrChanged = Signal() - ldr2hdr = makeProperty(QObject, "_ldr2hdr", notify=ldr2hdrChanged, resetOnDestroy=True) - - panoramaInitChanged = Signal() - panoramaInit = makeProperty(QObject, "_panoramaInit", notify=panoramaInitChanged, resetOnDestroy=True) - - sfmTransformChanged = Signal() - sfmTransform = makeProperty(QObject, "_sfmTransform", notify=sfmTransformChanged, resetOnDestroy=True) - nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) # Signals to propagate high-level messages From 55bec77b2d398dd9c3ff52bce18a896ec56d5c8e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 14:08:43 +0200 Subject: [PATCH 084/100] [nodes] LdrToHdrMerge: rename output param --- meshroom/multiview.py | 2 +- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 6564295dd8..b6e9d7a7f0 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -160,7 +160,7 @@ def hdriPipeline(graph): response=ldr2hdrCalibration.response) featureExtraction = graph.addNewNode('FeatureExtraction', - input=ldr2hdrMerge.outSfMDataFilename, + input=ldr2hdrMerge.outSfMData, describerPreset='high') panoramaInit = graph.addNewNode('PanoramaInit', diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index f57064677d..767612361a 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -141,7 +141,7 @@ class LdrToHdrMerge(desc.CommandLineNode): outputs = [ desc.File( - name='outSfMDataFilename', + name='outSfMData', label='Output SfMData File', description='Path to the output sfmdata file', value=desc.Node.internalFolder + 'sfmData.sfm', From a8e75024739db1786889eafa6f1bb53fb5f89887 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 14:09:04 +0200 Subject: [PATCH 085/100] [nodes] LdrToHdrMerge: add offsetRefBracketIndex param --- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index 767612361a..ca770258bc 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -48,6 +48,14 @@ class LdrToHdrMerge(desc.CommandLineNode): value='', uid=[0], ), + desc.IntParam( + name='offsetRefBracketIndex', + label='Offset Ref Bracket Index', + description='Zero to use the center bracket. +N to use a more exposed bracket or -N to use a less exposed backet.', + value=0, + range=(-4, 4, 1), + uid=[0], + ), desc.ChoiceParam( name='fusionWeight', label='Fusion Weight', From a1f3ace5d591aa3e960acdbbe8b148f4db270107 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 14:09:50 +0200 Subject: [PATCH 086/100] [ui] HdrImageToolbar: change gamma range of values --- meshroom/ui/qml/Viewer/HdrImageToolbar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer/HdrImageToolbar.qml b/meshroom/ui/qml/Viewer/HdrImageToolbar.qml index 88f11317b8..53a6cb956a 100644 --- a/meshroom/ui/qml/Viewer/HdrImageToolbar.qml +++ b/meshroom/ui/qml/Viewer/HdrImageToolbar.qml @@ -118,7 +118,7 @@ FloatingPane { id: gammaCtrl Layout.fillWidth: true from: 0.01 - to: 4 + to: 16 value: 1 stepSize: 0.01 } From 93e5a02cd73bb0a937e70cbbaead0ccf5eab7810 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 14:10:23 +0200 Subject: [PATCH 087/100] [ui] declare slot to avoid type errors with the property using it --- meshroom/ui/reconstruction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index be17bd3de9..f29ab272aa 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -550,6 +550,7 @@ def runAsync(func, args=(), kwargs=None): thread.start() return thread + @Slot(QObject) def getViewpoints(self): """ Return the Viewpoints model. """ # TODO: handle multiple Viewpoints models From d242d7801bcb5550daedc234edc653b5d0a70032 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 14:11:33 +0200 Subject: [PATCH 088/100] [nodes] PanoramaEstimation: change default value for maxAngleToPrior Avoid to cut too much connections. --- meshroom/nodes/aliceVision/PanoramaEstimation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PanoramaEstimation.py b/meshroom/nodes/aliceVision/PanoramaEstimation.py index d3f37ca565..23dac100cc 100644 --- a/meshroom/nodes/aliceVision/PanoramaEstimation.py +++ b/meshroom/nodes/aliceVision/PanoramaEstimation.py @@ -118,7 +118,7 @@ class PanoramaEstimation(desc.CommandLineNode): name='maxAngleToPrior', label='Max Angle To Priors (deg.)', description='''Maximal angle allowed regarding the input prior (in degrees).''', - value=5.0, + value=20.0, range=(0.0, 360.0, 1.0), uid=[0], advanced=True, From 3cc50d5277ffeadc26648e8f29202bd5e45c06ac Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 16:19:09 +0200 Subject: [PATCH 089/100] [nodes] remove old CameraDownscale node --- meshroom/nodes/aliceVision/CameraDownscale.py | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/CameraDownscale.py diff --git a/meshroom/nodes/aliceVision/CameraDownscale.py b/meshroom/nodes/aliceVision/CameraDownscale.py deleted file mode 100644 index 894c3cc3c6..0000000000 --- a/meshroom/nodes/aliceVision/CameraDownscale.py +++ /dev/null @@ -1,49 +0,0 @@ -__version__ = "1.0" - -import json -import os - -from meshroom.core import desc - - -class CameraDownscale(desc.CommandLineNode): - commandLine = 'aliceVision_cameraDownscale {allParams}' - size = desc.DynamicNodeSize('input') - - inputs = [ - desc.File( - name='input', - label='Input', - description="SfM Data File", - value='', - uid=[0], - ), - desc.FloatParam( - name='rescalefactor', - label='RescaleFactor', - description='Newsize = rescalefactor * oldsize', - value=0.5, - range=(0.0, 1.0, 0.1), - uid=[0], - advanced=True, - ), - desc.ChoiceParam( - name='verboseLevel', - label='Verbose Level', - description='Verbosity level (fatal, error, warning, info, debug, trace).', - value='info', - values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], - exclusive=True, - uid=[], - ), - ] - - outputs = [ - desc.File( - name='outSfMDataFilename', - label='Output SfMData File', - description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'sfmData.abc', - uid=[], - ) - ] From 278e9a5fe9e5f7c544c7d6afe59d0fb50c5721c9 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 16:20:02 +0200 Subject: [PATCH 090/100] [ui] ImageGallery: param name of LdrToHdr has been updated --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index d27b97167d..3029a2411c 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -396,7 +396,7 @@ Panel { function open() { if(imageProcessing.checked) imageProcessing.checked = false; - _reconstruction.setupTempCameraInit(activeNode, "outSfMDataFilename"); + _reconstruction.setupTempCameraInit(activeNode, "outSfMData"); } function close() { _reconstruction.clearTempCameraInit(); From 9ba35eedda8f6c2678551df111e0a45b0b3c2141 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 16:20:35 +0200 Subject: [PATCH 091/100] [nodes] GlobalSfM: update output param names --- meshroom/nodes/aliceVision/GlobalSfM.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/meshroom/nodes/aliceVision/GlobalSfM.py b/meshroom/nodes/aliceVision/GlobalSfM.py index 42996b938b..fb06535161 100644 --- a/meshroom/nodes/aliceVision/GlobalSfM.py +++ b/meshroom/nodes/aliceVision/GlobalSfM.py @@ -104,16 +104,23 @@ class GlobalSfM(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output Folder', - description='', - value=desc.Node.internalFolder, + label='Output SfMData File', + description='Path to the output sfmdata file', + value=desc.Node.internalFolder + 'sfm.abc', uid=[], ), desc.File( - name='outSfMDataFilename', - label='Output SfMData File', - description='Path to the output sfmdata file', - value=desc.Node.internalFolder + 'SfmData.abc', + name='outputViewsAndPoses', + label='Output Poses', + description='''Path to the output sfmdata file with cameras (views and poses).''', + value=desc.Node.internalFolder + 'cameras.sfm', + uid=[], + ), + desc.File( + name='extraInfoFolder', + label='Output Folder', + description='Folder for intermediate reconstruction files and additional reconstruction information files.', + value=desc.Node.internalFolder, uid=[], ), ] From 049122effe02ec0340f8bd4582dbf5d5b342d64e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 9 Jul 2020 16:21:13 +0200 Subject: [PATCH 092/100] [nodes] PanoramaInit: update output param name --- meshroom/multiview.py | 2 +- meshroom/nodes/aliceVision/PanoramaInit.py | 2 +- meshroom/ui/reconstruction.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index b6e9d7a7f0..3cc0bfbcad 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -169,7 +169,7 @@ def hdriPipeline(graph): ) imageMatching = graph.addNewNode('ImageMatching', - input=panoramaInit.outSfMDataFilename, + input=panoramaInit.outSfMData, featuresFolders=[featureExtraction.output], method='FrustumOrVocabularyTree') diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index 066deec0a5..844628386a 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -103,7 +103,7 @@ class PanoramaInit(desc.CommandLineNode): outputs = [ desc.File( - name='outSfMDataFilename', + name='outSfMData', label='Output SfMData File', description='Path to the output sfmdata file', value=desc.Node.internalFolder + 'sfmData.sfm', diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index f29ab272aa..a67b13cd3e 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -601,7 +601,7 @@ def getAutoFisheyeCircle(self, panoramaInit): if not panoramaInit.attribute("estimateFisheyeCircle").value: return QVector3D(0.0, 0.0, 0.0) - sfmFile = panoramaInit.attribute('outSfMDataFilename').value + sfmFile = panoramaInit.attribute('outSfMData').value if not os.path.exists(sfmFile): return QVector3D(0.0, 0.0, 0.0) import io # use io.open for Python2/3 compatibility (allow to specify encoding + errors handling) From 6cef1a525b21966566f920dd02d581509594947e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 10 Jul 2020 12:48:11 +0200 Subject: [PATCH 093/100] [nodes] LdrToHdrMerge: change default value for the center bracket index --- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index ca770258bc..757a57d5ba 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -52,7 +52,7 @@ class LdrToHdrMerge(desc.CommandLineNode): name='offsetRefBracketIndex', label='Offset Ref Bracket Index', description='Zero to use the center bracket. +N to use a more exposed bracket or -N to use a less exposed backet.', - value=0, + value=1, range=(-4, 4, 1), uid=[0], ), From 4717d73e6f30512c2f7f6dbd30f4b7233f242ceb Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 10 Jul 2020 13:02:15 +0200 Subject: [PATCH 094/100] [ui] fix type error with Qt Property --- meshroom/ui/reconstruction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index a67b13cd3e..154ad1dcb3 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -3,6 +3,7 @@ import math import os from threading import Thread +from collections import Iterable from PySide2.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF from PySide2.QtGui import QMatrix4x4, QMatrix3x3, QQuaternion, QVector3D, QVector2D @@ -1063,7 +1064,8 @@ def setSelectedViewpoint(self, viewpointAttribute): def reconstructedCamerasCount(self): """ Get the number of reconstructed cameras in the current context. """ viewpoints = self.getViewpoints() - if not viewpoints: + # Check that the object is iterable to avoid error with undefined Qt Property + if not isinstance(viewpoints, Iterable): return 0 return len([v for v in viewpoints if self.isReconstructed(v)]) From b439bf638e32ac8ca544f66e0a38a5425b34e6e8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 13 Jul 2020 10:36:01 +0200 Subject: [PATCH 095/100] [ui] CompatibilityNodes cannot be active nodes --- meshroom/ui/reconstruction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 154ad1dcb3..83f4285b2f 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -13,7 +13,7 @@ from meshroom import multiview from meshroom.common.qt import QObjectListModel from meshroom.core import Version -from meshroom.core.node import Node, Status, Position +from meshroom.core.node import Node, CompatibilityNode, Status, Position from meshroom.ui.graph import UIGraph from meshroom.ui.utils import makeProperty @@ -955,7 +955,8 @@ def setActiveNodes(self, nodes): if category == 'sfm': self.setSfm(node) for node in nodes: - self.activeNodes.get(node.nodeType).node = node + if not isinstance(node, CompatibilityNode): + self.activeNodes.get(node.nodeType).node = node def updateSfMResults(self): """ From e0b8bc3691007f21b14db0fc6450641164276cc7 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 13 Jul 2020 10:36:51 +0200 Subject: [PATCH 096/100] [ui] python-3 compatibility --- meshroom/ui/reconstruction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 83f4285b2f..7c8ca6a3ea 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -454,9 +454,9 @@ def setDefaultPipeline(self, defaultPipeline): def initActiveNodes(self): # Create all possible entries - for category, _ in self.activeNodeCategories.iteritems(): + for category, _ in self.activeNodeCategories.items(): self._activeNodes.add(ActiveNode(category, self)) - for nodeType, _ in meshroom.core.nodesDesc.iteritems(): + for nodeType, _ in meshroom.core.nodesDesc.items(): self._activeNodes.add(ActiveNode(nodeType, self)) def onCameraInitChanged(self): @@ -934,7 +934,7 @@ def setBuildingIntrinsics(self, value): @Slot(QObject) def setActiveNode(self, node): """ Set node as the active node of its type. """ - for category, nodeTypes in self.activeNodeCategories.iteritems(): + for category, nodeTypes in self.activeNodeCategories.items(): if node.nodeType in nodeTypes: self.activeNodes.get(category).node = node if category == 'sfm': @@ -947,10 +947,10 @@ def setActiveNodes(self, nodes): # Setup the active node per category only once, on the last one nodesByCategory = {} for node in nodes: - for category, nodeTypes in self.activeNodeCategories.iteritems(): + for category, nodeTypes in self.activeNodeCategories.items(): if node.nodeType in nodeTypes: nodesByCategory[category] = node - for category, node in nodesByCategory.iteritems(): + for category, node in nodesByCategory.items(): self.activeNodes.get(category).node = node if category == 'sfm': self.setSfm(node) From b760a1f4f6ebb27c3dde50e79d3387bcb18a565b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 16 Jul 2020 10:07:39 +0200 Subject: [PATCH 097/100] [tests] fix param change --- tests/test_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_graph.py b/tests/test_graph.py index c7fa6e4da5..b2793d05c8 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -180,7 +180,7 @@ def test_graph_reverse_dfs(): nodes = graph.nodesFromNode(B)[0] assert set(nodes) == {B, D, C, E, F} # Get all nodes of type AppendText from B - nodes = graph.nodesFromNode(B, filterType='AppendText')[0] + nodes = graph.nodesFromNode(B, filterTypes=['AppendText'])[0] assert set(nodes) == {B, D, C, F} # Get all nodes from C (order guaranteed) nodes = graph.nodesFromNode(C)[0] From c8526d4d6b045caa2cf86adcce8036549259087e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 16 Jul 2020 10:49:49 +0200 Subject: [PATCH 098/100] fix warnings: unused import --- meshroom/nodes/aliceVision/LdrToHdrCalibration.py | 1 - meshroom/nodes/aliceVision/LdrToHdrSampling.py | 1 - meshroom/nodes/aliceVision/PanoramaInit.py | 3 --- 3 files changed, 5 deletions(-) diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index c4650139eb..06ad416c64 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -1,7 +1,6 @@ __version__ = "2.0" import json -import os from meshroom.core import desc diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index 7222308edd..5f65b6e97c 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -1,7 +1,6 @@ __version__ = "2.0" import json -import os from meshroom.core import desc diff --git a/meshroom/nodes/aliceVision/PanoramaInit.py b/meshroom/nodes/aliceVision/PanoramaInit.py index 844628386a..9abcdcd871 100644 --- a/meshroom/nodes/aliceVision/PanoramaInit.py +++ b/meshroom/nodes/aliceVision/PanoramaInit.py @@ -1,8 +1,5 @@ __version__ = "1.0" -import json -import os - from meshroom.core import desc From 82342a7f61826b5ccf6536b163a533f37d5cd0d8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 16 Jul 2020 10:50:37 +0200 Subject: [PATCH 099/100] fix warnings: avoid list as default value in function params --- meshroom/multiview.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 3cc0bfbcad..6332061de8 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -93,7 +93,7 @@ def findFilesByTypeInFolder(folder, recursive=False): return output -def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output='', graph=None): +def hdri(inputImages=None, inputViewpoints=None, inputIntrinsics=None, output='', graph=None): """ Create a new Graph with a complete HDRI pipeline. @@ -110,9 +110,12 @@ def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), out with GraphModification(graph): nodes = hdriPipeline(graph) cameraInit = nodes[0] - cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) - cameraInit.viewpoints.extend(inputViewpoints) - cameraInit.intrinsics.extend(inputIntrinsics) + if inputImages: + cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) + if inputViewpoints: + cameraInit.viewpoints.extend(inputViewpoints) + if inputIntrinsics: + cameraInit.intrinsics.extend(inputIntrinsics) if output: imageProcessing = nodes[-1] @@ -120,7 +123,7 @@ def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), out return graph -def hdriFisheye(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output='', graph=None): +def hdriFisheye(inputImages=None, inputViewpoints=None, inputIntrinsics=None, output='', graph=None): if not graph: graph = Graph('HDRI-Fisheye') with GraphModification(graph): From 91f53341bcd136e027d5a1dad1ac808f6f9a38b9 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 16 Jul 2020 11:04:43 +0200 Subject: [PATCH 100/100] warning fixes --- meshroom/core/graph.py | 9 ++++----- meshroom/core/node.py | 2 +- meshroom/nodes/aliceVision/CameraInit.py | 2 +- meshroom/nodes/aliceVision/LdrToHdrCalibration.py | 2 +- meshroom/nodes/aliceVision/LdrToHdrMerge.py | 3 +-- meshroom/nodes/aliceVision/LdrToHdrSampling.py | 2 +- meshroom/nodes/aliceVision/StructureFromMotion.py | 3 --- 7 files changed, 9 insertions(+), 14 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 7cf21eebdf..c994dc0cef 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -561,7 +561,7 @@ def findNode(self, nodeExpr): candidates = self.findNodeCandidates('^' + nodeExpr) if not candidates: raise KeyError('No node candidate for "{}"'.format(nodeExpr)) - elif len(candidates) > 1: + if len(candidates) > 1: raise KeyError('Multiple node candidates for "{}": {}'.format(nodeExpr, str([c.name for c in candidates]))) return candidates[0] @@ -681,11 +681,11 @@ def _dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst): # (u,v) is a tree edge self.dfsVisit(v, visitor, colors, nodeChildren, longestPathFirst) # TODO: avoid recursion elif colors[v] == GRAY: + # (u,v) is a back edge visitor.backEdge((u, v), self) - pass # (u,v) is a back edge elif colors[v] == BLACK: + # (u,v) is a cross or forward edge visitor.forwardOrCrossEdge((u, v), self) - pass # (u,v) is a cross or forward edge visitor.finishEdge((u, v), self) colors[u] = BLACK visitor.finishVertex(u, self) @@ -740,8 +740,7 @@ def finishVertex(vertex, graph): def finishEdge(edge, graph): if edge[0].hasStatus(Status.SUCCESS) or edge[1].hasStatus(Status.SUCCESS): return - else: - edges.append(edge) + edges.append(edge) visitor.finishVertex = finishVertex visitor.finishEdge = finishEdge diff --git a/meshroom/core/node.py b/meshroom/core/node.py index a14df45a45..a59e4b1cd2 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -547,7 +547,7 @@ def _computeUids(self): def _buildCmdVars(self): def _buildAttributeCmdVars(cmdVars, name, attr): - if attr.attributeDesc.group != None: + if attr.attributeDesc.group is not None: # if there is a valid command line "group" v = attr.getValueStr() cmdVars[name] = '--{name} {value}'.format(name=name, value=v) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 4f2ee49616..1d632a3ddf 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -258,7 +258,7 @@ def buildIntrinsics(self, node, additionalViews=()): os.makedirs(os.path.join(tmpCache, node.internalFolder)) self.createViewpointsFile(node, additionalViews) cmd = self.buildCommandLine(node.chunks[0]) - logging.debug(' - commandLine:', cmd) + logging.debug(' - commandLine: {}'.format(cmd)) proc = psutil.Popen(cmd, stdout=None, stderr=None, shell=True) stdout, stderr = proc.communicate() # proc.wait() diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 06ad416c64..62e31283cc 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -9,7 +9,7 @@ def findMetadata(d, keys, defaultValue): for key in keys: v = d.get(key, None) k = key.lower() - if v != None: + if v is not None: return v for dk, dv in d.iteritems(): dkm = dk.lower().replace(" ", "") diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index 757a57d5ba..b58537bdf0 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -1,7 +1,6 @@ __version__ = "2.0" import json -import os from meshroom.core import desc @@ -10,7 +9,7 @@ def findMetadata(d, keys, defaultValue): for key in keys: v = d.get(key, None) k = key.lower() - if v != None: + if v is not None: return v for dk, dv in d.iteritems(): dkm = dk.lower().replace(" ", "") diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index 5f65b6e97c..4e9387e3dd 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -10,7 +10,7 @@ def findMetadata(d, keys, defaultValue): for key in keys: v = d.get(key, None) k = key.lower() - if v != None: + if v is not None: return v for dk, dv in d.iteritems(): dkm = dk.lower().replace(" ", "") diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 9d7c3147e5..685aa94534 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -1,8 +1,5 @@ __version__ = "2.0" -import json -import os - from meshroom.core import desc