From ce25724023ea23986a460b9b00274d32d73bd7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 8 Aug 2018 18:33:05 +0200 Subject: [PATCH 001/293] [multiview] Use SfMData as input in MVS pipeline --- meshroom/multiview.py | 20 +++++++++----- .../nodes/aliceVision/CameraConnection.py | 27 +++++++++++++++---- meshroom/nodes/aliceVision/DepthMap.py | 22 ++++++++++++--- meshroom/nodes/aliceVision/DepthMapFilter.py | 21 ++++++++++----- meshroom/nodes/aliceVision/Meshing.py | 15 ++++++++--- .../nodes/aliceVision/PrepareDenseScene.py | 11 ++------ meshroom/nodes/aliceVision/Texturing.py | 13 ++++++--- 7 files changed, 90 insertions(+), 39 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 8a1fcc2014..346f4a7e14 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -122,20 +122,26 @@ def mvsPipeline(graph, sfm=None): prepareDenseScene = graph.addNewNode('PrepareDenseScene', input=sfm.output if sfm else "") cameraConnection = graph.addNewNode('CameraConnection', - ini=prepareDenseScene.ini) + input=prepareDenseScene.input, + imagesFolder=prepareDenseScene.output) depthMap = graph.addNewNode('DepthMap', - ini=cameraConnection.ini) + input=cameraConnection.input, + cameraPairsMatrixFolder=cameraConnection.output, + imagesFolder=cameraConnection.imagesFolder) depthMapFilter = graph.addNewNode('DepthMapFilter', - depthMapFolder=depthMap.output, - ini=depthMap.ini) + input=depthMap.input, + cameraPairsMatrixFolder=depthMap.cameraPairsMatrixFolder, + depthMapFolder=depthMap.output) meshing = graph.addNewNode('Meshing', + input=depthMapFilter.input, + cameraPairsMatrixFolder=depthMapFilter.cameraPairsMatrixFolder, depthMapFolder=depthMapFilter.depthMapFolder, - depthMapFilterFolder=depthMapFilter.output, - ini=depthMapFilter.ini) + depthMapFilterFolder=depthMapFilter.output) meshFiltering = graph.addNewNode('MeshFiltering', input=meshing.output) texturing = graph.addNewNode('Texturing', - ini=meshing.ini, + input=meshing.input, + imagesFolder=depthMap.imagesFolder, inputDenseReconstruction=meshing.outputDenseReconstruction, inputMesh=meshFiltering.output) diff --git a/meshroom/nodes/aliceVision/CameraConnection.py b/meshroom/nodes/aliceVision/CameraConnection.py index e0c2d21796..1f103c376b 100644 --- a/meshroom/nodes/aliceVision/CameraConnection.py +++ b/meshroom/nodes/aliceVision/CameraConnection.py @@ -6,20 +6,27 @@ class CameraConnection(desc.CommandLineNode): internalFolder = desc.Node.internalFolder commandLine = 'aliceVision_cameraConnection {allParams}' - size = desc.DynamicNodeSize('ini') + size = desc.DynamicNodeSize('input') inputs = [ desc.File( - name="ini", - label='MVS Configuration file', - description='', + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='imagesFolder', + label='Images Folder', + description='Use images from a specific folder instead of those specify in the SfMData file.\nFilename should be the image uid.', value='', uid=[0], ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', - description='''verbosity level (fatal, error, warning, info, debug, trace).''', + description='verbosity level (fatal, error, warning, info, debug, trace).', value='info', values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], exclusive=True, @@ -27,3 +34,13 @@ class CameraConnection(desc.CommandLineNode): ), ] + outputs = [ + desc.File( + name='output', + label='Output', + description='Output folder for the camera pairs matrix file.', + value=desc.Node.internalFolder, + uid=[], + ) +] + diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index d49e8046b9..039670be5c 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -6,15 +6,29 @@ class DepthMap(desc.CommandLineNode): commandLine = 'aliceVision_depthMapEstimation {allParams}' gpu = desc.Level.INTENSIVE - size = desc.DynamicNodeSize('ini') + size = desc.DynamicNodeSize('input') parallelization = desc.Parallelization(blockSize=3) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' inputs = [ desc.File( - name="ini", - label='MVS Configuration File', - description='', + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='cameraPairsMatrixFolder', + label='Camera Pairs Matrix Folder', + description='Camera pairs matrix folder.', + value='', + uid=[0], + ), + desc.File( + name='imagesFolder', + label='Images Folder', + description='Use images from a specific folder instead of those specify in the SfMData file.\nFilename should be the image uid.', value='', uid=[0], ), diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index d960617c09..f112696f69 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -6,25 +6,32 @@ class DepthMapFilter(desc.CommandLineNode): commandLine = 'aliceVision_depthMapFiltering {allParams}' gpu = desc.Level.NORMAL - size = desc.DynamicNodeSize('ini') + size = desc.DynamicNodeSize('input') parallelization = desc.Parallelization(blockSize=10) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' inputs = [ desc.File( - name="ini", - label="MVS Configuration file", - description="", - value="", + name='input', + label='Input', + description='SfMData file.', + value='', uid=[0], - ), + ), + desc.File( + name='cameraPairsMatrixFolder', + label='Camera Pairs Matrix Folder', + description='Camera pairs matrix folder.', + value='', + uid=[0], + ), desc.File( name="depthMapFolder", label="Depth Map Folder", description="Input depth map folder", value="", uid=[0], - ), + ), desc.IntParam( name="nNearestCams", label="Number of Nearest Cameras", diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 5f4c3cb0ad..412d02e12b 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -11,12 +11,19 @@ class Meshing(desc.CommandLineNode): inputs = [ desc.File( - name="ini", - label='MVS Configuration file', - description='', + name='input', + label='Input', + description='SfMData file.', value='', uid=[0], - ), + ), + desc.File( + name='cameraPairsMatrixFolder', + label='Camera Pairs Matrix Folder', + description='Camera pairs matrix folder.', + value='', + uid=[0], + ), desc.File( name="depthMapFolder", label='Depth Maps Folder', diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index 529e7b867a..cf80923a31 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -6,6 +6,8 @@ class PrepareDenseScene(desc.CommandLineNode): commandLine = 'aliceVision_prepareDenseScene {allParams}' size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=40) + commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' inputs = [ desc.File( @@ -27,15 +29,6 @@ class PrepareDenseScene(desc.CommandLineNode): ] outputs = [ - desc.File( - name='ini', - label='MVS Configuration file', - description='', - value=desc.Node.internalFolder + 'mvs.ini', - uid=[], - group='', # not a command line arg - ), - desc.File( name='output', label='Output', diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 896b2df787..923339cb3d 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -9,9 +9,16 @@ class Texturing(desc.CommandLineNode): ram = desc.Level.INTENSIVE inputs = [ desc.File( - name='ini', - label='MVS Configuration file', - description='', + name='input', + label='Input', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='imagesFolder', + label='Images Folder', + description='Use images from a specific folder instead of those specify in the SfMData file.\nFilename should be the image uid.', value='', uid=[0], ), From 36aec4e5dae1c2b7ee57d97f4917f397ebadd1f0 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Sep 2018 12:21:24 +0200 Subject: [PATCH 002/293] [requirements] bump to PySide2 5.11.1 Update to the latest PySide2 version available. Make Meshroom compatible with Python 3.7. --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3748804745..20a6a62700 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # runtime psutil enum34;python_version<"3.4" -PySide2==5.11.0 -markdown==2.6.11 \ No newline at end of file +PySide2==5.11.1 +markdown==2.6.11 From f07628e5bdf22e921a05a10317354faa1f2d3dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 1 Oct 2018 15:36:20 +0200 Subject: [PATCH 003/293] [nodes] `KeyframeSelection` Add / update options KeyframeSelection node is now 2.0 --- .../nodes/aliceVision/KeyframeSelection.py | 161 +++++++++++++----- 1 file changed, 115 insertions(+), 46 deletions(-) diff --git a/meshroom/nodes/aliceVision/KeyframeSelection.py b/meshroom/nodes/aliceVision/KeyframeSelection.py index c41c3c8f5a..8d85dff80a 100644 --- a/meshroom/nodes/aliceVision/KeyframeSelection.py +++ b/meshroom/nodes/aliceVision/KeyframeSelection.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" import os from meshroom.core import desc @@ -8,74 +8,126 @@ class KeyframeSelection(desc.CommandLineNode): commandLine = 'aliceVision_utils_keyframeSelection {allParams}' inputs = [ - desc.File( + desc.ListAttribute( + elementDesc=desc.File( + name="mediaPath", + label="Media Path", + description="Media path.", + value="", + uid=[0], + ), name='mediaPaths', label='Media Paths', - description='''Input video files or image sequence directories.''', - value='', - uid=[0], + description='Input video files or image sequence directories.', + ), + desc.ListAttribute( + elementDesc=desc.File( + name="brand", + label="Brand", + description="Camera brand.", + value="", + uid=[0], ), - desc.IntParam( - name='maxNbOutFrame', - label='Max Nb Out Frame', - description='''maximum number of output frames (0 = no limit)''', - value=1000, - range=(0, 10000, 1), - uid=[], + name="brands", + label="Brands", + description="Camera brands." + ), + desc.ListAttribute( + elementDesc=desc.File( + name="model", + label="Model", + description="Camera model.", + value="", + uid=[0], ), + name="models", + label="Models", + description="Camera models." + ), + desc.ListAttribute( + elementDesc=desc.FloatParam( + name="mmFocal", + label="mmFocal", + description="Focal in mm (will be use if not 0).", + value=0.0, + range=(0.0, 500.0, 1.0), + uid=[0], + ), + name="mmFocals", + label="mmFocals", + description="Focals in mm (will be use if not 0)." + ), + desc.ListAttribute( + elementDesc=desc.FloatParam( + name="pxFocal", + label="pxFocal", + description="Focal in px (will be use and convert in mm if not 0).", + value=0.0, + range=(0.0, 500.0, 1.0), + uid=[0], + ), + name="pxFocals", + label="pxFocals", + description="Focals in px (will be use and convert in mm if not 0)." + ), + desc.ListAttribute( + elementDesc=desc.IntParam( + name="frameOffset", + label="Frame Offset", + description="Frame offset.", + value=0, + range=(0, 100.0, 1.0), + uid=[0], + ), + name="frameOffsets", + label="Frame Offsets", + description="Frame offsets." + ), desc.File( name='sensorDbPath', label='Sensor Db Path', description='''Camera sensor width database path.''', value=os.environ.get('ALICEVISION_SENSOR_DB', ''), uid=[0], - ), + ), desc.File( name='voctreePath', label='Voctree Path', description='''Vocabulary tree path.''', value=os.environ.get('ALICEVISION_VOCTREE', ''), uid=[0], - ), - desc.StringParam( - name='brands', - label='Brands', - description='''Camera brands.''', - value='', - uid=[0], - ), - desc.StringParam( - name='models', - label='Models', - description='''Camera models.''', - value='', + ), + desc.BoolParam( + name='useSparseDistanceSelection', + label='Use Sparse Distance Selection', + description='Use sparseDistance selection in order to avoid similar keyframes.', + value=True, uid=[0], - ), - desc.FloatParam( - name='mmFocals', - label='Mm Focals', - description='''Focals in mm (will be use if not 0).''', - value=0.0, - range=(0.0, 500.0, 1.0), + ), + desc.BoolParam( + name='useSharpnessSelection', + label='Use Sharpness Selection', + description='Use frame sharpness score for keyframe selection.', + value=True, uid=[0], - ), + ), desc.FloatParam( - name='pxFocals', - label='Px Focals', - description='''Focals in px (will be use and convert in mm if not 0).''', - value=0.0, - range=(0.0, 500.0, 1.0), + name='sparseDistMaxScore', + label='Sparse Distance Max Score', + description='Maximum number of strong common points between two keyframes.', + value=100.0, + range=(1.0, 200.0, 1.0), uid=[0], - ), + ), desc.ChoiceParam( name='sharpnessPreset', label='Sharpness Preset', - description='''Preset for sharpnessSelection : {ultra, high, normal, low, very_low, none}''', + description='Preset for sharpnessSelection : {ultra, high, normal, low, very_low, none}', value='normal', values=['ultra', 'high', 'normal', 'low', 'very_low', 'none'], exclusive=True, uid=[0], - ), + ), desc.IntParam( name='sharpSubset', label='Sharp Subset', @@ -83,7 +135,7 @@ class KeyframeSelection(desc.CommandLineNode): value=4, range=(1, 100, 1), uid=[0], - ), + ), desc.IntParam( name='minFrameStep', label='Min Frame Step', @@ -91,7 +143,7 @@ class KeyframeSelection(desc.CommandLineNode): value=12, range=(1, 100, 1), uid=[0], - ), + ), desc.IntParam( name='maxFrameStep', label='Max Frame Step', @@ -99,7 +151,24 @@ class KeyframeSelection(desc.CommandLineNode): value=36, range=(2, 1000, 1), uid=[0], - ), + ), + desc.IntParam( + name='maxNbOutFrame', + label='Max Nb Out Frame', + description='''maximum number of output frames (0 = no limit)''', + value=0, + range=(0, 10000, 1), + 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 = [ @@ -109,6 +178,6 @@ class KeyframeSelection(desc.CommandLineNode): description='''Output keyframes folder for extracted frames.''', value=desc.Node.internalFolder, uid=[], - ), + ), ] From 465d1fb6c83a1da27516c7184c178501c5562cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 1 Oct 2018 15:37:21 +0200 Subject: [PATCH 004/293] [nodes] `StructureFromMotion` Add `useRigsCalibration` option StructureFromMotion node is now 2.0 --- meshroom/nodes/aliceVision/StructureFromMotion.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 4ceb1b8850..283ac7f93c 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" import json import os @@ -161,6 +161,13 @@ class StructureFromMotion(desc.CommandLineNode): value=False, uid=[], ), + desc.BoolParam( + name='useRigsCalibration', + label='Use Rigs Calibration', + description='Enable/Disable rigs calibration.', + value=True, + uid=[0], + ), desc.File( name='initialPairA', label='Initial Pair A', From d95cdb0faf2b2cb28b9ff3484eb7c5f4a54c130c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 10 Oct 2018 14:41:25 +0200 Subject: [PATCH 005/293] [nodes] `CameraConnection` fix `imagesFolder` option description --- meshroom/nodes/aliceVision/CameraConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/CameraConnection.py b/meshroom/nodes/aliceVision/CameraConnection.py index 1f103c376b..ec3358c346 100644 --- a/meshroom/nodes/aliceVision/CameraConnection.py +++ b/meshroom/nodes/aliceVision/CameraConnection.py @@ -19,7 +19,7 @@ class CameraConnection(desc.CommandLineNode): desc.File( name='imagesFolder', label='Images Folder', - description='Use images from a specific folder instead of those specify in the SfMData file.\nFilename should be the image uid.', + description='Use images from a specific folder. Filename should be the image uid.', value='', uid=[0], ), From 9ce4b5e4ce714f4fe7e2f235a2c27f92079a664e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 10 Oct 2018 14:43:08 +0200 Subject: [PATCH 006/293] [nodes] `Mashing` add `imagesFolder` option for meshing without depth maps --- meshroom/nodes/aliceVision/Meshing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 412d02e12b..554e6e27a9 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -24,6 +24,13 @@ class Meshing(desc.CommandLineNode): value='', uid=[0], ), + desc.File( + name='imagesFolder', + label='Images Folder', + description='Use images from a specific folder. Filename should be the image uid.', + value='', + uid=[0], + ), desc.File( name="depthMapFolder", label='Depth Maps Folder', From 7c2255c090e911bd5df4f00b4d4bc9b4002c019f Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 11 Oct 2018 20:22:26 +0200 Subject: [PATCH 007/293] [ui] get reconstruction statuses from updated poseIds in sfm result * compatibility with rigs, where poseId of views are updated after SfM * count the number of reconstructed viewpoints based on this --- meshroom/ui/reconstruction.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3c9d7b2d3e..4cecbb0afc 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -444,8 +444,8 @@ def updateViewsAndPoses(self): Update internal views and poses based on the current SfM node. """ if not self._sfm: - self._views = [] - self._poses = [] + self._views = dict() + self._poses = dict() else: self._views, self._poses = self._sfm.nodeDesc.getViewsAndPoses(self._sfm) self.sfmReportChanged.emit() @@ -492,8 +492,9 @@ def isInViews(self, viewpoint): @Slot(QObject, result=bool) def isReconstructed(self, viewpoint): - # keys are strings (faster lookup) - return str(viewpoint.poseId.value) in self._poses + # fetch up-to-date poseId from sfm result (in case of rigs, poseId might have changed) + view = self._views.get(str(viewpoint.poseId.value), None) # keys are strings (faster lookup) + return view.get('poseId', -1) in self._poses if view else False @Slot(QObject, result=bool) def hasValidIntrinsic(self, viewpoint): @@ -512,6 +513,11 @@ def setSelectedViewId(self, viewId): self._selectedViewId = viewId self.selectedViewIdChanged.emit() + 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)]) + + selectedViewIdChanged = Signal() selectedViewId = Property(str, lambda self: self._selectedViewId, setSelectedViewId, notify=selectedViewIdChanged) @@ -522,7 +528,7 @@ def setSelectedViewId(self, viewId): sfmReport = Property(bool, lambda self: len(self._poses) > 0, notify=sfmReportChanged) sfmAugmented = Signal(Node, Node) - nbCameras = Property(int, lambda self: len(self._poses), notify=sfmReportChanged) + nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) # Signals to propagate high-level messages error = Signal(Message) From 490ebfe63089026495bcc8cecd9834f023e686b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 15 Oct 2018 15:49:40 +0200 Subject: [PATCH 008/293] [nodes] Remove node `ExportUndistortedImages` --- .../aliceVision/ExportUndistortedImages.py | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/ExportUndistortedImages.py diff --git a/meshroom/nodes/aliceVision/ExportUndistortedImages.py b/meshroom/nodes/aliceVision/ExportUndistortedImages.py deleted file mode 100644 index af69c67d3e..0000000000 --- a/meshroom/nodes/aliceVision/ExportUndistortedImages.py +++ /dev/null @@ -1,44 +0,0 @@ -__version__ = "1.0" - -from meshroom.core import desc - -class ExportUndistortedImages(desc.CommandLineNode): - commandLine = 'aliceVision_exportUndistortedImages {allParams}' - - inputs = [ - desc.File( - name='input', - label='Input SfMData', - description='SfMData file containing a complete SfM.', - value='', - uid=[0], - ), - desc.ChoiceParam( - name='outputFileType', - label='Output File Type', - description='Output file type for the undistorted images.', - value='exr', - values=['jpg', 'png', 'tif', 'exr'], - 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='output', - label='Output Folder', - description='Output folder for the undistorted images.', - value=desc.Node.internalFolder, - uid=[], - ), - ] From 88f4f068ec81ea3bd8072f60faa1647533a3ff82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 15 Oct 2018 15:50:21 +0200 Subject: [PATCH 009/293] [nodes] `PrepareDenseScene` Add options --- .../nodes/aliceVision/PrepareDenseScene.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index cf80923a31..f91171866e 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -17,6 +17,29 @@ class PrepareDenseScene(desc.CommandLineNode): value='', uid=[0], ), + desc.ChoiceParam( + name='outputFileType', + label='Output File Type', + description='Output file type for the undistorted images.', + value='exr', + values=['jpg', 'png', 'tif', 'exr'], + exclusive=True, + uid=[0], + ), + desc.BoolParam( + name='saveMetadata', + label='Save Metadata', + description='Save projections and intrinsics informations in images metadata (only for .exr images).', + value=True, + uid=[0], + ), + desc.BoolParam( + name='saveMatricesTxtFiles', + label='Save Matrices Text Files', + description='Save projections and intrinsics informations in text files.', + value=False, + uid=[0], + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 52a7f475c301bc919e0e5bef85921ca1cfb65dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 15 Oct 2018 18:03:00 +0200 Subject: [PATCH 010/293] [nodes] Remove `CameraConnection` node --- meshroom/multiview.py | 11 +---- .../nodes/aliceVision/CameraConnection.py | 46 ------------------- meshroom/nodes/aliceVision/DepthMap.py | 7 --- meshroom/nodes/aliceVision/DepthMapFilter.py | 9 +--- meshroom/nodes/aliceVision/Meshing.py | 7 --- 5 files changed, 3 insertions(+), 77 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/CameraConnection.py diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 346f4a7e14..0c46a74203 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -121,20 +121,14 @@ def mvsPipeline(graph, sfm=None): prepareDenseScene = graph.addNewNode('PrepareDenseScene', input=sfm.output if sfm else "") - cameraConnection = graph.addNewNode('CameraConnection', - input=prepareDenseScene.input, - imagesFolder=prepareDenseScene.output) depthMap = graph.addNewNode('DepthMap', - input=cameraConnection.input, - cameraPairsMatrixFolder=cameraConnection.output, - imagesFolder=cameraConnection.imagesFolder) + input=prepareDenseScene.input, + imagesFolder=prepareDenseScene.output) depthMapFilter = graph.addNewNode('DepthMapFilter', input=depthMap.input, - cameraPairsMatrixFolder=depthMap.cameraPairsMatrixFolder, depthMapFolder=depthMap.output) meshing = graph.addNewNode('Meshing', input=depthMapFilter.input, - cameraPairsMatrixFolder=depthMapFilter.cameraPairsMatrixFolder, depthMapFolder=depthMapFilter.depthMapFolder, depthMapFilterFolder=depthMapFilter.output) meshFiltering = graph.addNewNode('MeshFiltering', @@ -147,7 +141,6 @@ def mvsPipeline(graph, sfm=None): return [ prepareDenseScene, - cameraConnection, depthMap, depthMapFilter, meshing, diff --git a/meshroom/nodes/aliceVision/CameraConnection.py b/meshroom/nodes/aliceVision/CameraConnection.py deleted file mode 100644 index ec3358c346..0000000000 --- a/meshroom/nodes/aliceVision/CameraConnection.py +++ /dev/null @@ -1,46 +0,0 @@ -__version__ = "1.0" - -from meshroom.core import desc - - -class CameraConnection(desc.CommandLineNode): - internalFolder = desc.Node.internalFolder - commandLine = 'aliceVision_cameraConnection {allParams}' - size = desc.DynamicNodeSize('input') - - inputs = [ - desc.File( - name='input', - label='Input', - description='SfMData file.', - value='', - uid=[0], - ), - desc.File( - name='imagesFolder', - label='Images Folder', - description='Use images from a specific folder. Filename should be the image uid.', - 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='output', - label='Output', - description='Output folder for the camera pairs matrix file.', - value=desc.Node.internalFolder, - uid=[], - ) -] - diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index 039670be5c..b3a578a930 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -17,13 +17,6 @@ class DepthMap(desc.CommandLineNode): description='SfMData file.', value='', uid=[0], - ), - desc.File( - name='cameraPairsMatrixFolder', - label='Camera Pairs Matrix Folder', - description='Camera pairs matrix folder.', - value='', - uid=[0], ), desc.File( name='imagesFolder', diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index f112696f69..e00ce63f0b 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -17,14 +17,7 @@ class DepthMapFilter(desc.CommandLineNode): description='SfMData file.', value='', uid=[0], - ), - desc.File( - name='cameraPairsMatrixFolder', - label='Camera Pairs Matrix Folder', - description='Camera pairs matrix folder.', - value='', - uid=[0], - ), + ), desc.File( name="depthMapFolder", label="Depth Map Folder", diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 554e6e27a9..6014746862 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -17,13 +17,6 @@ class Meshing(desc.CommandLineNode): value='', uid=[0], ), - desc.File( - name='cameraPairsMatrixFolder', - label='Camera Pairs Matrix Folder', - description='Camera pairs matrix folder.', - value='', - uid=[0], - ), desc.File( name='imagesFolder', label='Images Folder', From 3a0c6d79b31787129cd673435ed4a35db70bdfd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 22 Oct 2018 16:39:48 +0200 Subject: [PATCH 011/293] [nodes] `ConvertSfMFormat` Add `describerTypes` option --- meshroom/nodes/aliceVision/ConvertSfMFormat.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshroom/nodes/aliceVision/ConvertSfMFormat.py b/meshroom/nodes/aliceVision/ConvertSfMFormat.py index 4e762405ac..ac702acb7d 100644 --- a/meshroom/nodes/aliceVision/ConvertSfMFormat.py +++ b/meshroom/nodes/aliceVision/ConvertSfMFormat.py @@ -24,6 +24,16 @@ class ConvertSfMFormat(desc.CommandLineNode): uid=[0], group='', # exclude from command line ), + desc.ChoiceParam( + name='describerTypes', + label='Describer Types', + description='Describer types to keep.', + value=['sift'], + values=['sift', 'sift_float', 'sift_upright', 'akaze', 'akaze_liop', 'akaze_mldb', 'cctag3', 'cctag4', 'sift_ocv', 'akaze_ocv'], + exclusive=False, + uid=[0], + joinChar=',', + ), desc.BoolParam( name='views', label='Views', From 4ec5bbe255bed92401cb91c477b83dc81dfc5367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:25:56 +0100 Subject: [PATCH 012/293] [nodes] `Meshing` Add option `minObservations` Minimum number of observations for SfM space estimation. --- meshroom/nodes/aliceVision/Meshing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 6014746862..badeeb03c8 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -38,6 +38,14 @@ class Meshing(desc.CommandLineNode): value='', uid=[0], ), + desc.IntParam( + name='minObservations', + label='Min Observations For SfM Space Estimation', + description='Minimum number of observations for SfM space estimation.', + value=3, + range=(0, 100, 1), + uid=[0], + ), desc.IntParam( name='maxInputPoints', label='Max Input Points', From 640f46fdbdf65bb1d9a4f02e3d678413c45b492e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:27:16 +0100 Subject: [PATCH 013/293] [nodes] `Meshing` Add option `estimateSpaceFromSfM` Estimate the 3d space from the SfM. --- meshroom/nodes/aliceVision/Meshing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index badeeb03c8..4b6e6546c1 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -38,6 +38,13 @@ class Meshing(desc.CommandLineNode): value='', uid=[0], ), + desc.BoolParam( + name='estimateSpaceFromSfM', + label='Estimate Space From SfM', + description='Estimate the 3d space from the SfM', + value=True, + uid=[0], + ), desc.IntParam( name='minObservations', label='Min Observations For SfM Space Estimation', From ad4aa908ee77f3068f67036bd285ba80905ef85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:28:52 +0100 Subject: [PATCH 014/293] [ui] Allows `.arw` file extension --- meshroom/ui/reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3c9d7b2d3e..5b89e9e2a7 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -159,7 +159,7 @@ class Reconstruction(UIGraph): Specialization of a UIGraph designed to manage a 3D reconstruction. """ - imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef') + imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw') def __init__(self, graphFilepath='', parent=None): super(Reconstruction, self).__init__(graphFilepath, parent) From b9b2198ee045d5f66d24b2f5d744604b16e649fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:42:47 +0100 Subject: [PATCH 015/293] [nodes] `PrepareDenseScene` version is now 2.0 --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index f91171866e..e04bebebcd 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc From 3c49a605e6d4d90841ca3956b47996f269a9cceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:43:01 +0100 Subject: [PATCH 016/293] [nodes] `Meshing` version is now 2.0 --- meshroom/nodes/aliceVision/Meshing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 4b6e6546c1..57d786230b 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc From e4ffd54e8fd21de651fa730176527f25056de4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Tue, 30 Oct 2018 11:43:21 +0100 Subject: [PATCH 017/293] [nodes] `ConvertSfMFormat` version is now 2.0 --- meshroom/nodes/aliceVision/ConvertSfMFormat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/ConvertSfMFormat.py b/meshroom/nodes/aliceVision/ConvertSfMFormat.py index 4e762405ac..85a670af0c 100644 --- a/meshroom/nodes/aliceVision/ConvertSfMFormat.py +++ b/meshroom/nodes/aliceVision/ConvertSfMFormat.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc From 123c59ff4e9d6a7533c692d1b3578e96883c30ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 31 Oct 2018 15:21:36 +0100 Subject: [PATCH 018/293] [nodes] `Meshing` Rename option `minObservations` to `estimateSpaceMinObservations` --- meshroom/nodes/aliceVision/Meshing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 57d786230b..7ff0e7f0e9 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -46,7 +46,7 @@ class Meshing(desc.CommandLineNode): uid=[0], ), desc.IntParam( - name='minObservations', + name='estimateSpaceMinObservations', label='Min Observations For SfM Space Estimation', description='Minimum number of observations for SfM space estimation.', value=3, From 400835d8f8d1bc47206a704d61181287d24a7e75 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 16 Nov 2018 16:15:23 +0100 Subject: [PATCH 019/293] [core] Attribute: expose 'fullName' as property --- meshroom/core/attribute.py | 9 +++++---- meshroom/core/graph.py | 10 +++++----- meshroom/ui/commands.py | 18 +++++++++--------- meshroom/ui/graph.py | 4 ++-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index d55fadc247..8b3dbd2329 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -71,17 +71,17 @@ def root(self): def absoluteName(self): return '{}.{}.{}'.format(self.node.graph.name, self.node.name, self._name) - def fullName(self): + def getFullName(self): """ Name inside the Graph: nodeName.name """ if isinstance(self.root, ListAttribute): - return '{}[{}]'.format(self.root.fullName(), self.root.index(self)) + return '{}[{}]'.format(self.root.getFullName(), self.root.index(self)) elif isinstance(self.root, GroupAttribute): - return '{}.{}'.format(self.root.fullName(), self._name) + return '{}.{}'.format(self.root.getFullName(), self._name) return '{}.{}'.format(self.node.name, self._name) def asLinkExpr(self): """ Return link expression for this Attribute """ - return "{" + self.fullName() + "}" + return "{" + self.getFullName() + "}" def getName(self): """ Attribute name """ @@ -209,6 +209,7 @@ def getPrimitiveValue(self, exportDefault=True): return self._value name = Property(str, getName, constant=True) + fullName = Property(str, getFullName, constant=True) label = Property(str, getLabel, constant=True) type = Property(str, getType, constant=True) desc = Property(desc.Attribute, lambda self: self.attributeDesc, constant=True) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 565ecb27e8..72722584d2 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -410,7 +410,7 @@ def nodeOutEdges(self, node): def removeNode(self, nodeName): """ Remove the node identified by 'nodeName' from the graph - and return in and out edges removed by this operation in two dicts {dstAttr.fullName(), srcAttr.fullName()} + and return in and out edges removed by this operation in two dicts {dstAttr.getFullName(), srcAttr.getFullName()} """ node = self.node(nodeName) inEdges = {} @@ -420,10 +420,10 @@ def removeNode(self, nodeName): with GraphModification(self): for edge in self.nodeOutEdges(node): self.removeEdge(edge.dst) - outEdges[edge.dst.fullName()] = edge.src.fullName() + outEdges[edge.dst.getFullName()] = edge.src.getFullName() for edge in self.nodeInEdges(node): self.removeEdge(edge.dst) - inEdges[edge.dst.fullName()] = edge.src.fullName() + inEdges[edge.dst.getFullName()] = edge.src.getFullName() self._nodes.remove(node) self.update() @@ -570,7 +570,7 @@ def addEdge(self, srcAttr, dstAttr): if srcAttr.node.graph != self or dstAttr.node.graph != self: raise RuntimeError('The attributes of the edge should be part of a common graph.') if dstAttr in self.edges.keys(): - raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.fullName())) + raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullName())) edge = Edge(srcAttr, dstAttr) self.edges.add(edge) self.markNodesDirty(dstAttr.node) @@ -586,7 +586,7 @@ def addEdges(self, *edges): @changeTopology def removeEdge(self, dstAttr): if dstAttr not in self.edges.keys(): - raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.fullName())) + raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.getFullName())) self.edges.pop(dstAttr) self.markNodesDirty(dstAttr.node) dstAttr.valueChanged.emit() diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index f0737d3f2f..c327b29349 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -168,10 +168,10 @@ def undoImpl(self): class SetAttributeCommand(GraphCommand): def __init__(self, graph, attribute, value, parent=None): super(SetAttributeCommand, self).__init__(graph, parent) - self.attrName = attribute.fullName() + self.attrName = attribute.getFullName() self.value = value self.oldValue = attribute.getExportValue() - self.setText("Set Attribute '{}'".format(attribute.fullName())) + self.setText("Set Attribute '{}'".format(attribute.getFullName())) def redoImpl(self): if self.value == self.oldValue: @@ -186,8 +186,8 @@ def undoImpl(self): class AddEdgeCommand(GraphCommand): def __init__(self, graph, src, dst, parent=None): super(AddEdgeCommand, self).__init__(graph, parent) - self.srcAttr = src.fullName() - self.dstAttr = dst.fullName() + self.srcAttr = src.getFullName() + self.dstAttr = dst.getFullName() self.setText("Connect '{}'->'{}'".format(self.srcAttr, self.dstAttr)) def redoImpl(self): @@ -201,8 +201,8 @@ def undoImpl(self): class RemoveEdgeCommand(GraphCommand): def __init__(self, graph, edge, parent=None): super(RemoveEdgeCommand, self).__init__(graph, parent) - self.srcAttr = edge.src.fullName() - self.dstAttr = edge.dst.fullName() + self.srcAttr = edge.src.getFullName() + self.dstAttr = edge.dst.getFullName() self.setText("Disconnect '{}'->'{}'".format(self.srcAttr, self.dstAttr)) def redoImpl(self): @@ -218,7 +218,7 @@ class ListAttributeAppendCommand(GraphCommand): def __init__(self, graph, listAttribute, value, parent=None): super(ListAttributeAppendCommand, self).__init__(graph, parent) assert isinstance(listAttribute, ListAttribute) - self.attrName = listAttribute.fullName() + self.attrName = listAttribute.getFullName() self.index = None self.count = 1 self.value = value if value else None @@ -244,10 +244,10 @@ def __init__(self, graph, attribute, parent=None): super(ListAttributeRemoveCommand, self).__init__(graph, parent) listAttribute = attribute.root assert isinstance(listAttribute, ListAttribute) - self.listAttrName = listAttribute.fullName() + self.listAttrName = listAttribute.getFullName() self.index = listAttribute.index(attribute) self.value = attribute.getExportValue() - self.setText("Remove {}".format(attribute.fullName())) + self.setText("Remove {}".format(attribute.getFullName())) def redoImpl(self): listAttribute = self.graph.attribute(self.listAttrName) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 3c1efcf494..49d03eb8f6 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -389,7 +389,7 @@ def removeNode(self, node): @Slot(Attribute, Attribute) def addEdge(self, src, dst): if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute): - with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.fullName())): + with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullName())): self.appendAttribute(dst) self.push(commands.AddEdgeCommand(self._graph, src, dst.at(-1))) else: @@ -398,7 +398,7 @@ def addEdge(self, src, dst): @Slot(Edge) def removeEdge(self, edge): if isinstance(edge.dst.root, ListAttribute): - with self.groupedGraphModification("Remove Edge and Delete {}".format(edge.dst.fullName())): + with self.groupedGraphModification("Remove Edge and Delete {}".format(edge.dst.getFullName())): self.push(commands.RemoveEdgeCommand(self._graph, edge)) self.removeAttribute(edge.dst) else: From 3dad1e3aacfcdc94d9fe7ed5f79bc0b0451876c8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 16 Nov 2018 16:46:37 +0100 Subject: [PATCH 020/293] [core] Attribute: expose 'node' and 'linkParam' as properties --- meshroom/core/attribute.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 8b3dbd2329..3377a74c38 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -219,6 +219,8 @@ def getPrimitiveValue(self, exportDefault=True): isLinkChanged = Signal() isLink = Property(bool, isLink.fget, notify=isLinkChanged) isDefault = Property(bool, _isDefault, notify=valueChanged) + linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged) + node = Property(BaseObject, node.fget, constant=True) def raiseIfLink(func): From d2aba176dcd75c214eb066efd45e3d8028a6d7fc Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 16 Nov 2018 16:56:33 +0100 Subject: [PATCH 021/293] [core] Node: add 'label' property high-level label for nodes, without '_' and including index if > 1 --- meshroom/core/node.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 90d59cb423..a118bcf526 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -362,6 +362,14 @@ def __getattr__(self, k): def getName(self): return self._name + def getLabel(self): + """ + Returns: + str: the high-level label of this node + """ + t, idx = self._name.split("_") + return "{}{}".format(t, idx if int(idx) > 1 else "") + @property def packageFullName(self): return '-'.join([self.packageName, self.packageVersion]) @@ -615,6 +623,7 @@ def __repr__(self): return self.name name = Property(str, getName, constant=True) + label = Property(str, getLabel, constant=True) nodeType = Property(str, nodeType.fget, constant=True) positionChanged = Signal() position = Property(Variant, position.fget, position.fset, notify=positionChanged) From e80bdd11615c6d9dcb8f3c5e395fe1cec222e1f0 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 16 Nov 2018 20:01:39 +0100 Subject: [PATCH 022/293] [core] Node: introduce 'globalStatus' * add Node.globalStatus property: indicates the status of a Node based on the status of its own NodeChunks * remove obsolete 'statusNames' and 'getStatus' methods --- meshroom/core/node.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index a118bcf526..6736449d2e 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -600,16 +600,31 @@ def process(self, forceCompute=False): def endSequence(self): pass - def getStatus(self): - return self.status + def getGlobalStatus(self): + """ + Get node global status based on the status of its chunks. + + Returns: + Status: the node global status + """ + chunksStatus = [chunk.status.status for chunk in self._chunks] + + anyOf = (Status.ERROR, Status.STOPPED, Status.KILLED, + Status.RUNNING, Status.SUBMITTED) + allOf = (Status.SUCCESS,) + + for status in anyOf: + if any(s == status for s in chunksStatus): + return status + for status in allOf: + if all(s == status for s in chunksStatus): + return status + + return Status.NONE def getChunks(self): return self._chunks - @property - def statusNames(self): - return [s.status.name for s in self.status] - def getSize(self): return self._size @@ -639,6 +654,8 @@ def __repr__(self): chunks = Property(Variant, getChunks, notify=chunksChanged) sizeChanged = Signal() size = Property(int, getSize, notify=sizeChanged) + globalStatusChanged = Signal() + globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) class Node(BaseNode): @@ -698,6 +715,8 @@ def _updateChunks(self): ranges = self.nodeDesc.parallelization.getRanges(self) if len(ranges) != len(self._chunks): self._chunks.setObjectList([NodeChunk(self, range) for range in ranges]) + for c in self._chunks: + c.statusChanged.connect(self.globalStatusChanged) else: for chunk, range in zip(self._chunks, ranges): chunk.range = range @@ -708,6 +727,7 @@ def _updateChunks(self): else: if len(self._chunks) != 1: self._chunks.setObjectList([NodeChunk(self, desc.Range())]) + self._chunks[0].statusChanged.connect(self.globalStatusChanged) else: self._chunks[0].range = desc.Range() From 7db4beea89bd5b2916b344f5bea9ecea1a3b657b Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 16 Nov 2018 20:02:48 +0100 Subject: [PATCH 023/293] [ui] use Node label in GraphEditor --- meshroom/ui/qml/GraphEditor/AttributeEditor.qml | 2 +- meshroom/ui/qml/GraphEditor/Node.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index 8a4d48aad1..cd075ecf06 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -29,7 +29,7 @@ ColumnLayout { Label { Layout.fillWidth: true elide: Text.ElideMiddle - text: node.nodeType + text: node.label horizontalAlignment: Text.AlignHCenter padding: 6 } diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 5d12860df0..6f02192992 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -80,7 +80,7 @@ Item { width: parent.width horizontalAlignment: Text.AlignHCenter padding: 4 - text: node.nodeType + text: node.label color: "#EEE" font.pointSize: 8 background: Rectangle { From 7415c1d391975ddffba5f8e18c32fec8c663c655 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 14:24:09 +0100 Subject: [PATCH 024/293] [ui] split 2D and 3D viewers into separate modules + remove historical MayaCameraController --- .../ui/qml/Viewer/MayaCameraController.qml | 128 ------------------ meshroom/ui/qml/Viewer/qmldir | 6 - .../{Viewer => Viewer3D}/AlembicLoader.qml | 0 .../DefaultCameraController.qml | 0 .../{Viewer => Viewer3D}/DepthMapLoader.qml | 0 .../ui/qml/{Viewer => Viewer3D}/Grid3D.qml | 0 .../ui/qml/{Viewer => Viewer3D}/Locator3D.qml | 0 .../{Viewer => Viewer3D}/MaterialSwitcher.qml | 0 .../Materials/WireframeEffect.qml | 0 .../Materials/WireframeMaterial.qml | 0 .../Materials/shaders/robustwireframe.frag | 0 .../Materials/shaders/robustwireframe.geom | 0 .../Materials/shaders/robustwireframe.vert | 0 .../ui/qml/{Viewer => Viewer3D}/Viewer3D.qml | 0 meshroom/ui/qml/Viewer3D/qmldir | 6 + meshroom/ui/qml/WorkspaceView.qml | 1 + 16 files changed, 7 insertions(+), 134 deletions(-) delete mode 100644 meshroom/ui/qml/Viewer/MayaCameraController.qml rename meshroom/ui/qml/{Viewer => Viewer3D}/AlembicLoader.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/DefaultCameraController.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/DepthMapLoader.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Grid3D.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Locator3D.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/MaterialSwitcher.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Materials/WireframeEffect.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Materials/WireframeMaterial.qml (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Materials/shaders/robustwireframe.frag (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Materials/shaders/robustwireframe.geom (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Materials/shaders/robustwireframe.vert (100%) rename meshroom/ui/qml/{Viewer => Viewer3D}/Viewer3D.qml (100%) create mode 100644 meshroom/ui/qml/Viewer3D/qmldir diff --git a/meshroom/ui/qml/Viewer/MayaCameraController.qml b/meshroom/ui/qml/Viewer/MayaCameraController.qml deleted file mode 100644 index b5a5ea39b0..0000000000 --- a/meshroom/ui/qml/Viewer/MayaCameraController.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick 2.7 -import Qt3D.Core 2.1 -import Qt3D.Render 2.1 -import Qt3D.Input 2.1 -//import Qt3D.Extras 2.0 -import Qt3D.Logic 2.0 -import QtQml 2.2 - -Entity { - - id: root - property Camera camera - property real translateSpeed: 100.0 - property real tiltSpeed: 500.0 - property real panSpeed: 500.0 - property bool moving: false - - signal mousePressed(var mouse) - signal mouseReleased(var mouse) - signal mouseWheeled(var wheel) - - KeyboardDevice { id: keyboardSourceDevice } - MouseDevice { id: mouseSourceDevice; sensitivity: 0.1 } - - MouseHandler { - - sourceDevice: mouseSourceDevice - onPressed: mousePressed(mouse) - onReleased: mouseReleased(mouse) - onWheel: { - var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.05; - var tz = (wheel.angleDelta.y / 120) * d; - root.camera.translate(Qt.vector3d(0, 0, tz), Camera.DontTranslateViewCenter) - } - } - - LogicalDevice { - id: cameraControlDevice - actions: [ - Action { - id: actionLMB - inputs: [ - ActionInput { - sourceDevice: mouseSourceDevice - buttons: [MouseEvent.LeftButton] - } - ] - }, - Action { - id: actionRMB - inputs: [ - ActionInput { - sourceDevice: mouseSourceDevice - buttons: [MouseEvent.RightButton] - } - ] - }, - Action { - id: actionMMB - inputs: [ - ActionInput { - sourceDevice: mouseSourceDevice - buttons: [MouseEvent.MiddleButton] - } - ] - }, - Action { - id: actionAlt - onActiveChanged: root.moving = active - inputs: [ - ActionInput { - sourceDevice: keyboardSourceDevice - buttons: [Qt.Key_Alt] - } - ] - } - ] - axes: [ - Axis { - id: axisMX - inputs: [ - AnalogAxisInput { - sourceDevice: mouseSourceDevice - axis: MouseDevice.X - } - ] - }, - Axis { - id: axisMY - inputs: [ - AnalogAxisInput { - sourceDevice: mouseSourceDevice - axis: MouseDevice.Y - } - ] - } - ] - } - - components: [ - FrameAction { - onTriggered: { - if(!actionAlt.active) - return; - if(actionLMB.active) { // rotate - var rx = -axisMX.value; - var ry = -axisMY.value; - root.camera.panAboutViewCenter(root.panSpeed * rx * dt, Qt.vector3d(0,1,0)) - root.camera.tiltAboutViewCenter(root.tiltSpeed * ry * dt) - return; - } - if(actionMMB.active) { // translate - var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03; - var tx = axisMX.value * root.translateSpeed * d; - var ty = axisMY.value * root.translateSpeed * d; - root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt)) - return; - } - if(actionRMB.active) { // zoom - var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.05; - var tz = axisMX.value * root.translateSpeed * d; - root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter) - return; - } - } - } - ] -} diff --git a/meshroom/ui/qml/Viewer/qmldir b/meshroom/ui/qml/Viewer/qmldir index f078802745..f715f431ee 100644 --- a/meshroom/ui/qml/Viewer/qmldir +++ b/meshroom/ui/qml/Viewer/qmldir @@ -2,9 +2,3 @@ module Viewer Viewer2D 1.0 Viewer2D.qml ImageMetadataView 1.0 ImageMetadataView.qml -Viewer3D 1.0 Viewer3D.qml -DefaultCameraController 1.0 DefaultCameraController.qml -MayaCameraController 1.0 MayaCameraController.qml -Locator3D 1.0 Locator3D.qml -Grid3D 1.0 Grid3D.qml - diff --git a/meshroom/ui/qml/Viewer/AlembicLoader.qml b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml similarity index 100% rename from meshroom/ui/qml/Viewer/AlembicLoader.qml rename to meshroom/ui/qml/Viewer3D/AlembicLoader.qml diff --git a/meshroom/ui/qml/Viewer/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml similarity index 100% rename from meshroom/ui/qml/Viewer/DefaultCameraController.qml rename to meshroom/ui/qml/Viewer3D/DefaultCameraController.qml diff --git a/meshroom/ui/qml/Viewer/DepthMapLoader.qml b/meshroom/ui/qml/Viewer3D/DepthMapLoader.qml similarity index 100% rename from meshroom/ui/qml/Viewer/DepthMapLoader.qml rename to meshroom/ui/qml/Viewer3D/DepthMapLoader.qml diff --git a/meshroom/ui/qml/Viewer/Grid3D.qml b/meshroom/ui/qml/Viewer3D/Grid3D.qml similarity index 100% rename from meshroom/ui/qml/Viewer/Grid3D.qml rename to meshroom/ui/qml/Viewer3D/Grid3D.qml diff --git a/meshroom/ui/qml/Viewer/Locator3D.qml b/meshroom/ui/qml/Viewer3D/Locator3D.qml similarity index 100% rename from meshroom/ui/qml/Viewer/Locator3D.qml rename to meshroom/ui/qml/Viewer3D/Locator3D.qml diff --git a/meshroom/ui/qml/Viewer/MaterialSwitcher.qml b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml similarity index 100% rename from meshroom/ui/qml/Viewer/MaterialSwitcher.qml rename to meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml diff --git a/meshroom/ui/qml/Viewer/Materials/WireframeEffect.qml b/meshroom/ui/qml/Viewer3D/Materials/WireframeEffect.qml similarity index 100% rename from meshroom/ui/qml/Viewer/Materials/WireframeEffect.qml rename to meshroom/ui/qml/Viewer3D/Materials/WireframeEffect.qml diff --git a/meshroom/ui/qml/Viewer/Materials/WireframeMaterial.qml b/meshroom/ui/qml/Viewer3D/Materials/WireframeMaterial.qml similarity index 100% rename from meshroom/ui/qml/Viewer/Materials/WireframeMaterial.qml rename to meshroom/ui/qml/Viewer3D/Materials/WireframeMaterial.qml diff --git a/meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.frag b/meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.frag similarity index 100% rename from meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.frag rename to meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.frag diff --git a/meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.geom b/meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.geom similarity index 100% rename from meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.geom rename to meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.geom diff --git a/meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.vert b/meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.vert similarity index 100% rename from meshroom/ui/qml/Viewer/Materials/shaders/robustwireframe.vert rename to meshroom/ui/qml/Viewer3D/Materials/shaders/robustwireframe.vert diff --git a/meshroom/ui/qml/Viewer/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml similarity index 100% rename from meshroom/ui/qml/Viewer/Viewer3D.qml rename to meshroom/ui/qml/Viewer3D/Viewer3D.qml diff --git a/meshroom/ui/qml/Viewer3D/qmldir b/meshroom/ui/qml/Viewer3D/qmldir new file mode 100644 index 0000000000..81a62925e2 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/qmldir @@ -0,0 +1,6 @@ +module Viewer3D + +Viewer3D 1.0 Viewer3D.qml +DefaultCameraController 1.0 DefaultCameraController.qml +Locator3D 1.0 Locator3D.qml +Grid3D 1.0 Grid3D.qml diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index b19e50f0b9..1d3e3bc4be 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -4,6 +4,7 @@ import QtQuick.Controls 1.4 as Controls1 // For SplitView import QtQuick.Layouts 1.3 import Qt.labs.platform 1.0 as Platform import Viewer 1.0 +import Viewer3D 1.0 import MaterialIcons 2.2 import Utils 1.0 From 7a8e1c689f3ded1765aa83986bdd6a02c1740655 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 20:20:08 +0100 Subject: [PATCH 025/293] [ui] Reconstruction: expose end node instead of mesh filepath In preparation for massive Viewer3D refactoring. --- meshroom/ui/qml/WorkspaceView.qml | 3 -- meshroom/ui/reconstruction.py | 75 +++++++++++++++++++------------ 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 1d3e3bc4be..9e343d3b5a 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -19,7 +19,6 @@ Item { property variant reconstruction: _reconstruction readonly property variant cameraInits: _reconstruction.cameraInits - readonly property url meshFile: Filepath.stringToUrl(_reconstruction.meshFile) property bool readOnly: false implicitWidth: 300 @@ -144,8 +143,6 @@ Item { anchors.bottom: parent.bottom anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter - visible: meshFile != "" && (viewer3D.source != meshFile) - onClicked: load3DMedia(meshFile) } } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3c9d7b2d3e..115ee32972 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -166,8 +166,7 @@ def __init__(self, graphFilepath='', parent=None): self._buildingIntrinsics = False self._cameraInit = None self._cameraInits = QObjectListModel(parent=self) - self._endChunk = None - self._meshFile = '' + self._endNode = None self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) self.graphChanged.connect(self.onGraphChanged) self._liveSfmManager = LiveSfmManager(self) @@ -215,21 +214,13 @@ def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() self.sfm = None - self._endChunk = None - self.setMeshFile('') + self.endNode = None self.updateCameraInits() if not self._graph: return self.setSfm(self.lastSfmNode()) - try: - endNode = self._graph.findNode("Texturing") - self._endChunk = endNode.getChunks()[0] # type: graph.NodeChunk - endNode.outputMesh.valueChanged.connect(self.updateMeshFile) - self._endChunk.statusChanged.connect(self.updateMeshFile) - self.updateMeshFile() - except KeyError: - self._endChunk = None + # TODO: listen specifically for cameraInit creation/deletion self._graph.nodes.countChanged.connect(self.updateCameraInits) @@ -265,24 +256,35 @@ def getCameraInitIndex(self): return self._cameraInits.indexOf(self._cameraInit) def setCameraInitIndex(self, idx): - self.setCameraInit(self._cameraInits[idx]) - - def updateMeshFile(self): - if self._endChunk and self._endChunk.status.status == Status.SUCCESS: - self.setMeshFile(self._endChunk.node.outputMesh.value) - else: - self.setMeshFile('') - - def setMeshFile(self, mf): - if self._meshFile == mf: - return - self._meshFile = mf - self.meshFileChanged.emit() + camInit = self._cameraInits[idx] if self._cameraInits else None + self.setCameraInit(camInit) def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ - sfmNodes = self._graph.nodesFromNode(self._cameraInits[0], 'StructureFromMotion')[0] - return sfmNodes[-1] if sfmNodes else None + return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS) + + def lastNodeOfType(self, nodeType, 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 + startNode (Node): the node to start from + preferredStatus (Status): (optional) the node status to prioritize + + Returns: + Node: the node matching the input parameters or None + """ + if not startNode: + return None + nodes = self._graph.nodesFromNode(startNode, nodeType)[0] + if not nodes: + return None + node = nodes[-1] + if preferredStatus: + node = next((n for n in reversed(nodes) if n.getGlobalStatus() == preferredStatus), node) + return node def addSfmAugmentation(self, withMVS=False): """ @@ -435,8 +437,6 @@ def setBuildingIntrinsics(self, value): intrinsicsBuilt = Signal(QObject, list, list) buildingIntrinsicsChanged = Signal() buildingIntrinsics = Property(bool, lambda self: self._buildingIntrinsics, notify=buildingIntrinsicsChanged) - meshFileChanged = Signal() - meshFile = Property(str, lambda self: self._meshFile, notify=meshFileChanged) liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) def updateViewsAndPoses(self): @@ -484,6 +484,21 @@ def setSfm(self, node): self._sfm.chunks[0].statusChanged.disconnect(self.updateViewsAndPoses) self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) + self.setEndNode(self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS)) + + def setEndNode(self, node=None): + if self._endNode == node: + return + if self._endNode: + try: + self._endNode.destroyed.disconnect(self.setEndNode) + except RuntimeError: + # self._endNode might have been destroyed at this point, causing PySide2 to throw a RuntimeError + pass + self._endNode = node + if self._endNode: + self._endNode.destroyed.connect(self.setEndNode) + self.endNodeChanged.emit() @Slot(QObject, result=bool) def isInViews(self, viewpoint): @@ -521,6 +536,8 @@ def setSelectedViewId(self, viewId): # 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) + endNodeChanged = Signal() + endNode = Property(QObject, lambda self: self._endNode, setEndNode, notify=endNodeChanged) nbCameras = Property(int, lambda self: len(self._poses), notify=sfmReportChanged) From ceae856fdf20368310be1cd2ec2fd6b52ee25839 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 28 Nov 2018 11:32:45 +0100 Subject: [PATCH 026/293] [README] contact and faq --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fe9203c3a..333cf627ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ![Meshroom - 3D Reconstruction Software](/docs/logo/banner-meshroom.png) -Meshroom is a free, open-source 3D Reconstruction Software based on the [AliceVision](https://github.com/alicevision/AliceVision) framework. +Meshroom is a free, open-source 3D Reconstruction Software based on the [AliceVision](https://github.com/alicevision/AliceVision) Photogrammetric Computer Vision framework. Learn more details about the pipeline on [AliceVision website](http://alicevision.github.io). @@ -9,6 +9,8 @@ See [results of the pipeline on sketchfab](http://sketchfab.com/AliceVision). Continuous integration: * Windows: [![Build status](https://ci.appveyor.com/api/projects/status/25sd7lfr3v0rnvni/branch/develop?svg=true)](https://ci.appveyor.com/project/AliceVision/meshroom/branch/develop) * Linux: [![Build Status](https://travis-ci.org/alicevision/meshroom.svg?branch=develop)](https://travis-ci.org/alicevision/meshroom) + + ## Photogrammetry Photogrammetry is the science of making measurements from photographs. @@ -59,3 +61,17 @@ You may need to adjust the folder `/usr/lib/nvidia-340` with the correct driver # Linux/macOS: PYTHONPATH=$PWD python bin/meshroom_photogrammetry --input INPUT_IMAGES_FOLDER --output OUTPUT_FOLDER ``` + + +## FAQ + +See the [Meshroom wiki](https://github.com/alicevision/meshroom/wiki) for more information. + + +## Contact + +Use the public mailing-list to ask questions or request features. It is also a good place for informal discussions like sharing results, interesting related technologies or publications: +> [alicevision@googlegroups.com](mailto:alicevision@googlegroups.com) +> [http://groups.google.com/group/alicevision](http://groups.google.com/group/alicevision) + +You can also contact the core team privately on: [alicevision-team@googlegroups.com](mailto:alicevision-team@googlegroups.com). From 910c7e68bfad07a9fa63c9aa9a4a4bdde5671678 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 30 Nov 2018 11:26:31 +0100 Subject: [PATCH 027/293] [nodes] FeatureExtraction: don't force CPU extraction by default use GPU features extraction if available by default --- meshroom/nodes/aliceVision/FeatureExtraction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index 1881719d20..94363dd602 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "1.1" from meshroom.core import desc @@ -40,7 +40,7 @@ class FeatureExtraction(desc.CommandLineNode): name='forceCpuExtraction', label='Force CPU Extraction', description='Use only CPU feature extraction.', - value=True, + value=False, uid=[], ), desc.ChoiceParam( From 50044faf9a1087c0c933356f74a754e8350d1848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 6 Dec 2018 16:07:16 +0100 Subject: [PATCH 028/293] [nodes] `DepthMap` add `minViewAngle` and `maxViewAngle` options --- meshroom/nodes/aliceVision/DepthMap.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index b3a578a930..cf830ec7ed 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -34,6 +34,22 @@ class DepthMap(desc.CommandLineNode): exclusive=True, uid=[0], ), + desc.FloatParam( + name='minViewAngle', + label='Min View Angle', + description='Minimum angle between two views.', + value=2.0, + range=(0.0, 10.0, 0.1), + uid=[0], + ), + desc.FloatParam( + name='maxViewAngle', + label='Max View Angle', + description='Maximum angle between two views.', + value=70.0, + range=(10.0, 120.0, 1), + uid=[0], + ), desc.IntParam( name='sgmMaxTCams', label='SGM: Nb Neighbour Cameras', From 062a9b1de7772bb3d1feab8e091a53e39e75f94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 6 Dec 2018 16:08:00 +0100 Subject: [PATCH 029/293] [nodes] `DepthMapFilter` add `minViewAngle` and `maxViewAngle` options --- meshroom/nodes/aliceVision/DepthMapFilter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index e00ce63f0b..5dd7b60c02 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -25,6 +25,22 @@ class DepthMapFilter(desc.CommandLineNode): value="", uid=[0], ), + desc.FloatParam( + name='minViewAngle', + label='Min View Angle', + description='Minimum angle between two views.', + value=2.0, + range=(0.0, 10.0, 0.1), + uid=[0], + ), + desc.FloatParam( + name='maxViewAngle', + label='Max View Angle', + description='Maximum angle between two views.', + value=70.0, + range=(10.0, 120.0, 1), + uid=[0], + ), desc.IntParam( name="nNearestCams", label="Number of Nearest Cameras", From b49053427703ad41d5026f3da1455f00a192f33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 6 Dec 2018 16:08:45 +0100 Subject: [PATCH 030/293] [nodes] `Meshing` add `estimateSpaceMinObservationAngle` option --- meshroom/nodes/aliceVision/Meshing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 7ff0e7f0e9..daf5197b3b 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -53,6 +53,14 @@ class Meshing(desc.CommandLineNode): range=(0, 100, 1), uid=[0], ), + desc.FloatParam( + name='estimateSpaceMinObservationAngle', + label='Min Observations Angle For SfM Space Estimation', + description='Minimum angle between two observations for SfM space estimation.', + value=0.2, + range=(0, 10, 0.1), + uid=[0], + ), desc.IntParam( name='maxInputPoints', label='Max Input Points', From 312e21c03ad0005c1a1672a89908b12c09c22652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Fri, 7 Dec 2018 11:25:25 +0100 Subject: [PATCH 031/293] [core] `BoolParam` add int cast to handle string values ('0', '1') --- meshroom/core/desc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index ca15232446..02ac9e2e9c 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -106,7 +106,7 @@ def __init__(self, name, label, description, value, uid, group='allParams'): def validateValue(self, value): try: - return bool(value) + return bool(int(value)) # int cast is useful to handle string values ('0', '1') except: raise ValueError('BoolParam only supports bool value (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) From 98dec1ffd78746a780623c4baaac20483f329b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Fri, 7 Dec 2018 11:28:17 +0100 Subject: [PATCH 032/293] [node] `CameraInit` add per intrinsic param `locked` --- meshroom/nodes/aliceVision/CameraInit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 1b87ef72c7..63c30c328c 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" import os import json @@ -40,6 +40,7 @@ label="Distortion Params", description="Distortion Parameters", ), + desc.BoolParam(name='locked', label='Locked', description='Whether Intrinsic is locked.', value=False, uid=[0]), ] From f6365c5607dee9a2d001a7b398986ceae97a1471 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 20:41:06 +0100 Subject: [PATCH 033/293] [ui] Viewer3D: fix wrong keyboard modifier state Keyboard Actions are never deactivated when focus is lost, leading to picking being enabled on a single click when Control was pressed in the 3D viewer and focus was lost to another Item. * keep track of focus loss inside the DefaultCameraController * use it to disable picking while Keyboard has not been pressed back in the 3D viewer * do the same for 'moving' state with Alt modifier --- .../qml/Viewer3D/DefaultCameraController.qml | 19 +++++++++++++++++-- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml index 12bdf45409..7f12596ece 100644 --- a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml +++ b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml @@ -12,8 +12,9 @@ Entity { property real translateSpeed: 75.0 property real tiltSpeed: 500.0 property real panSpeed: 500.0 - property bool moving: pressed || actionAlt.active - readonly property alias controlPressed: actionControl.active + property bool moving: pressed || (actionAlt.active && keyboardHandler._pressed) + property alias focus: keyboardHandler.focus + readonly property bool pickingActive: actionControl.active && keyboardHandler._pressed readonly property alias pressed: mouseHandler._pressed signal mousePressed(var mouse) @@ -40,6 +41,20 @@ Entity { } } + KeyboardHandler { + id: keyboardHandler + sourceDevice: keyboardSourceDevice + property bool _pressed + + // When focus is lost while pressing a key, the corresponding action + // stays active, even when it's released. + // Handle this issue manually by keeping an additional _pressed state + // which is cleared when focus changes (used for 'pickingActive' property). + onFocusChanged: if(!focus) _pressed = false + onPressed: _pressed = true + onReleased: _pressed = false + } + LogicalDevice { id: cameraControlDevice actions: [ diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index 9c9a5280be..fe17679121 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -193,6 +193,7 @@ FocusScope { DefaultCameraController { id: cameraController camera: mainCamera + focus: scene3D.activeFocus onMousePressed: { scene3D.forceActiveFocus() if(mouse.button == Qt.LeftButton) @@ -273,7 +274,7 @@ FocusScope { id: picker // Triangle picking is expensive // Only activate it when a double click may happen or when the 'Control' key is pressed - enabled: cameraController.controlPressed || doubleClickTimer.running + enabled: cameraController.pickingActive || doubleClickTimer.running hoverEnabled: false onPressed: { if(pick.button == Qt.LeftButton) From 2f5058790447d65aa9a486c924fe1072fdca8421 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 21:36:43 +0100 Subject: [PATCH 034/293] [ui] introduce Scene3DHelper object Expose missing QEntity methods and helper functions to QML via this Python class. --- meshroom/ui/app.py | 3 ++ meshroom/ui/components/__init__.py | 3 +- meshroom/ui/components/scene3D.py | 42 +++++++++++++++++++++++++ meshroom/ui/qml/Utils/Scene3DHelper.qml | 6 ++++ meshroom/ui/qml/Utils/qmldir | 1 + 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 meshroom/ui/components/scene3D.py create mode 100644 meshroom/ui/qml/Utils/Scene3DHelper.qml diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index dba4c68fa3..6d64a6d484 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -9,6 +9,7 @@ from meshroom.core import nodesDesc from meshroom.ui import components from meshroom.ui.components.filepath import FilepathHelper +from meshroom.ui.components.scene3D import Scene3DHelper from meshroom.ui.palette import PaletteManager from meshroom.ui.reconstruction import Reconstruction from meshroom.ui.utils import QmlInstantEngine @@ -51,6 +52,8 @@ def __init__(self, args): self.engine.rootContext().setContextProperty("_PaletteManager", pm) fpHelper = FilepathHelper(parent=self) self.engine.rootContext().setContextProperty("Filepath", fpHelper) + scene3DHelper = Scene3DHelper(parent=self) + self.engine.rootContext().setContextProperty("Scene3DHelper", scene3DHelper) self.engine.rootContext().setContextProperty("MeshroomApp", self) # Request any potential computation to stop on exit self.aboutToQuit.connect(r.stopExecution) diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index ab40a3d501..23b4e9072b 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -3,7 +3,8 @@ def registerTypes(): from PySide2.QtQml import qmlRegisterType from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.filepath import FilepathHelper + from meshroom.ui.components.scene3D import Scene3DHelper qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable - + qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable diff --git a/meshroom/ui/components/scene3D.py b/meshroom/ui/components/scene3D.py new file mode 100644 index 0000000000..8a46abc44b --- /dev/null +++ b/meshroom/ui/components/scene3D.py @@ -0,0 +1,42 @@ +from PySide2.QtCore import QObject, Slot +from PySide2.Qt3DCore import Qt3DCore +from PySide2.Qt3DRender import Qt3DRender + + +class Scene3DHelper(QObject): + + @Slot(Qt3DCore.QEntity, str, result="QVariantList") + def findChildrenByProperty(self, entity, propertyName): + """ Recursively get all children of an entity that have a property named 'propertyName'. """ + children = [] + for child in entity.childNodes(): + try: + if child.metaObject().indexOfProperty(propertyName) != -1: + children.append(child) + except RuntimeError: + continue + children += self.findChildrenByProperty(child, propertyName) + return children + + @Slot(Qt3DCore.QEntity, Qt3DCore.QComponent) + def addComponent(self, entity, component): + """ Adds a component to an entity. """ + entity.addComponent(component) + + @Slot(Qt3DCore.QEntity, Qt3DCore.QComponent) + def removeComponent(self, entity, component): + """ Removes a component from an entity. """ + entity.removeComponent(component) + + @Slot(Qt3DCore.QEntity, result=int) + def vertexCount(self, entity): + """ Return vertex count based on children QGeometryRenderer 'vertexCount'.""" + return sum([renderer.vertexCount() for renderer in entity.findChildren(Qt3DRender.QGeometryRenderer)]) + + @Slot(Qt3DCore.QEntity, result=int) + def faceCount(self, entity): + """ Returns face count based on children QGeometry buffers size.""" + count = 0 + for geo in entity.findChildren(Qt3DRender.QGeometry): + count += sum([attr.count() for attr in geo.attributes() if attr.name() == "vertexPosition"]) + return count / 3 diff --git a/meshroom/ui/qml/Utils/Scene3DHelper.qml b/meshroom/ui/qml/Utils/Scene3DHelper.qml new file mode 100644 index 0000000000..8941425b98 --- /dev/null +++ b/meshroom/ui/qml/Utils/Scene3DHelper.qml @@ -0,0 +1,6 @@ +pragma Singleton +import Meshroom.Helpers 1.0 + +Scene3DHelper { + +} diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index 0480ee67df..e2e04ca8a6 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -4,3 +4,4 @@ SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml Request 1.0 request.js # causes random crash at application exit # singleton Filepath 1.0 Filepath.qml +# singleton Scene3DHelper 1.0 Scene3DHelper.qml From 109b980ae56accd0cdc33a9c72ea0f87411ab205 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 21:58:30 +0100 Subject: [PATCH 035/293] [ui] Viewer3D: introduce new 3D media loading backend This commit adds several components to centralize and extend 3D media loading. They are not yet integrated into Viewer3D. The entry point to this system is the MediaLibrary component that: * can load N medias based on a filepath or load-and-watch node attributes * provides a cache mechanism to instant-reload medias that were unloaded under certain conditions * gives access to statistics (vertex/face/camera/textureCount) through a unified interface --- meshroom/ui/qml/Viewer3D/AlembicLoader.qml | 54 ++++ meshroom/ui/qml/Viewer3D/MediaCache.qml | 76 ++++++ meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 241 ++++++++++++++++++ meshroom/ui/qml/Viewer3D/MediaLoader.qml | 179 +++++++++++++ .../ui/qml/Viewer3D/MediaLoaderEntity.qml | 20 ++ meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 16 ++ meshroom/ui/qml/Viewer3D/qmldir | 1 + 7 files changed, 587 insertions(+) create mode 100644 meshroom/ui/qml/Viewer3D/MediaCache.qml create mode 100644 meshroom/ui/qml/Viewer3D/MediaLibrary.qml create mode 100644 meshroom/ui/qml/Viewer3D/MediaLoader.qml create mode 100644 meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml create mode 100644 meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml diff --git a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml index 85c635dd15..31a02ff1c6 100644 --- a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml +++ b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml @@ -1,4 +1,8 @@ import AlembicEntity 1.0 +import QtQuick 2.9 +import Qt3D.Core 2.1 +import Qt3D.Render 2.1 +import Qt3D.Extras 2.1 /** * Support for Alembic files in Qt3d. @@ -6,4 +10,54 @@ import AlembicEntity 1.0 */ AlembicEntity { id: root + + signal cameraSelected(var viewId) + + function spawnCameraSelectors() { + var validCameras = 0; + // spawn camera selector for each camera + for(var i = 0; i < root.cameras.length; ++i) + { + var cam = root.cameras[i]; + // retrieve view id + var viewId = cam.userProperties["mvg_viewId"]; + if(viewId === undefined) + continue; + // filter out non-reconstructed cameras + if(cam.parent.parent.objectName === "mvgCamerasUndefined") { + cam.enabled = false; + continue; + } + camSelectionComponent.createObject(cam, {"viewId": viewId}); + validCameras++; + } + return validCameras; + } + + SystemPalette { + id: activePalette + } + + // Camera selection picking and display + Component { + id: camSelectionComponent + Entity { + id: camSelector + property string viewId + + components: [ + CuboidMesh { xExtent: 0.2; yExtent: 0.2; zExtent: 0.2;}, + PhongMaterial{ + id: mat + ambient: viewId === _reconstruction.selectedViewId ? activePalette.highlight : "#CCC" + diffuse: cameraPicker.containsMouse ? Qt.lighter(activePalette.highlight, 1.2) : ambient + }, + ObjectPicker { + id: cameraPicker + enabled: root.enabled + onClicked: _reconstruction.selectedViewId = camSelector.viewId + } + ] + } + } } diff --git a/meshroom/ui/qml/Viewer3D/MediaCache.qml b/meshroom/ui/qml/Viewer3D/MediaCache.qml new file mode 100644 index 0000000000..5531b52575 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/MediaCache.qml @@ -0,0 +1,76 @@ +import Qt3D.Core 2.1 +import Qt3D.Render 2.1 + +import Utils 1.0 + +Entity { + id: root + + enabled: false // disabled entity + + property int cacheSize: 2 + property var mediaCache: {[]} + + /// The current number of managed entities + function currentSize() { + return Object.keys(mediaCache).length; + } + + /// Whether the cache contains an entity for the given source + function contains(source) { + return mediaCache[source] !== undefined; + } + + /// Add an entity to the cache + function add(source, object){ + if(!Filepath.exists(source)) + return false; + if(contains(source)) + return true; + // console.debug("[cache] add: " + source) + mediaCache[source] = object; + object.parent = root; + // remove oldest entry in cache + if(currentSize() > cacheSize) + shrink(); + return true; + } + + /// Pop an entity from the cache based on its source + function pop(source){ + if(!contains(source)) + return undefined; + + var obj = mediaCache[source]; + delete mediaCache[source]; + // console.debug("[cache] pop: " + source) + // delete cached obj if file does not exist on disk anymore + if(!Filepath.exists(source)) + { + obj.destroy(); + obj = undefined; + } + return obj; + } + + /// Remove and destroy an entity from cache + function destroyEntity(source) { + var obj = pop(source); + if(obj) + obj.destroy(); + } + + + // Shrink cache to fit max size + function shrink() { + while(currentSize() > cacheSize) + destroyEntity(Object.keys(mediaCache)[0]); + } + + // Clear cache and destroy all managed entities + function clear() { + Object.keys(mediaCache).forEach(function(key){ + destroyEntity(key); + }); + } +} diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml new file mode 100644 index 0000000000..d8c6baa65f --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -0,0 +1,241 @@ +import QtQuick 2.9 +import Qt3D.Core 2.1 +import Qt3D.Render 2.1 +import Utils 1.0 + + +/** + * MediaLibrary is an Entity that loads and manages a list of 3D media. + * It also uses an internal cache to instantly reload media. + */ +Entity { + id: root + + readonly property alias model: m.mediaModel + property int renderMode + property bool pickingEnabled: false + readonly property alias count: instantiator.count // number of instantiated media delegates + + signal pressed(var pick) + signal loadRequest(var idx) + + QtObject { + id: m + property ListModel mediaModel: ListModel {} + property var sourceToEntity: ({}) + + readonly property var mediaElement: ({ + "source": "", + "valid": true, + "label": "", + "visible": true, + "section": "", + "attribute": null, + "entity": null, + "requested": true, + "vertexCount": 0, + "faceCount": 0, + "cameraCount": 0, + "textureCount": 0, + "status": SceneLoader.None + }) + } + + function makeElement(values) { + return Object.assign({}, JSON.parse(JSON.stringify(m.mediaElement)), values); + } + + function ensureVisible(source) { + var idx = find(source); + if(idx === -1) + return + // load / make media visible + m.mediaModel.get(idx).requested = true; + m.mediaModel.get(idx).visible = true; + loadRequest(idx); + } + + function find(source) { + for(var i=0; i 'entity' reference + m.sourceToEntity[modelSource] = mediaLoader; + // always request media loading when delegate has been created + updateModelAndCache(true); + // if external media failed to open, remove element from model + if(!attribute && !object) + remove(index) + } + + onCurrentSourceChanged: updateModelAndCache() + + onFinalSourceChanged: { + var cachedObject = cache.pop(rawSource); + cached = cachedObject !== undefined; + if(cached) { + object = cachedObject; + object.parent = mediaLoader; + } + mediaLoader.source = Filepath.stringToUrl(finalSource); + if(object) { + // bind media info to corresponding model roles + // (test for object validity to avoid error messages right after object has been deleted) + var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount"]; + boundProperties.forEach( function(prop){ + model[prop] = Qt.binding(function() { return object ? object[prop] : 0; }); + }) + } + } + + onStatusChanged: { + model.status = status + // remove model entry for external media that failed to load + if(status === SceneLoader.Error && !model.attribute) + remove(index); + } + + components: [ + ObjectPicker { + enabled: parent.enabled && pickingEnabled + hoverEnabled: false + onPressed: root.pressed(pick) + } + ] + } + onObjectRemoved: { + delete m.sourceToEntity[object.modelSource]; + } + } +} diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml new file mode 100644 index 0000000000..051b7f1dcd --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -0,0 +1,179 @@ +import QtQuick 2.9 +import Qt3D.Core 2.1 +import Qt3D.Render 2.1 +import Qt3D.Extras 2.1 +import QtQuick.Scene3D 2.0 +import "Materials" +import Utils 1.0 + + +/** + * MediaLoader provides a single entry point for 3D media loading. + * It encapsulates all available plugins/loaders. + */ + Entity { + id: root + + property url source + property bool loading: false + property int status: SceneLoader.None + property var object: null + property int renderMode + + property bool cached: false + + onSourceChanged: { + if(cached) { + root.status = SceneLoader.Ready; + return; + } + + // clear previously created objet if any + if(object) { + object.destroy(); + object = null; + } + + var component = undefined; + status = SceneLoader.Loading; + + if(!Filepath.exists(source)) { + status = SceneLoader.None; + return; + } + + switch(Filepath.extension(source)) { + case ".abc": if(Viewer3DSettings.supportAlembic) component = abcLoaderEntityComponent; break; + case ".exr": if(Viewer3DSettings.supportDepthMap) component = depthMapLoaderComponent; break; + case ".obj": + default: component = sceneLoaderEntityComponent; break; + } + + // Media loader available + if(component) { + object = component.createObject(root, {"source": source}); + } + } + + Component { + id: sceneLoaderEntityComponent + MediaLoaderEntity { + id: sceneLoaderEntity + objectName: "SceneLoader" + + components: [ + SceneLoader { + source: parent.source + onStatusChanged: { + if(status == SceneLoader.Ready) { + textureCount = sceneLoaderPostProcess(sceneLoaderEntity); + faceCount = Scene3DHelper.faceCount(sceneLoaderEntity) + } + root.status = status; + } + } + ] + } + } + + Component { + id: abcLoaderEntityComponent + MediaLoaderEntity { + id: abcLoaderEntity + Component.onCompleted: { + + var obj = Viewer3DSettings.abcLoaderComp.createObject(abcLoaderEntity, { + 'source': source, + 'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }), + 'locatorScale': 0.3 + }); + + obj.statusChanged.connect(function() { + if(obj.status === SceneLoader.Ready) { + for(var i = 0; i < obj.pointClouds.length; ++i) { + vertexCount += Scene3DHelper.vertexCount(obj.pointClouds[i]); + } + cameraCount = obj.spawnCameraSelectors(); + } + root.status = obj.status; + }) + } + } + } + + Component { + id: depthMapLoaderComponent + MediaLoaderEntity { + id: depthMapLoaderEntity + Component.onCompleted: { + var obj = Viewer3DSettings.depthMapLoaderComp.createObject(depthMapLoaderEntity, { + 'source': source + }); + faceCount = Scene3DHelper.faceCount(obj); + root.status = SceneLoader.Ready; + } + } + } + + Component { + id: materialSwitcherComponent + MaterialSwitcher { } + } + + // Remove automatically created DiffuseMapMaterial and + // instantiate a MaterialSwitcher instead. Returns the faceCount + function sceneLoaderPostProcess(rootEntity) + { + var materials = Scene3DHelper.findChildrenByProperty(rootEntity, "diffuse"); + var entities = []; + var texCount = 0; + materials.forEach(function(mat){ + entities.push(mat.parent); + }) + + entities.forEach(function(entity) { + var mats = []; + var componentsToRemove = []; + // Create as many MaterialSwitcher as individual materials for this entity + // NOTE: we let each MaterialSwitcher modify the components of the entity + // and therefore remove the default material spawned by the sceneLoader + for(var i = 0; i < entity.components.length; ++i) + { + var comp = entity.components[i] + + // handle DiffuseMapMaterials created by SceneLoader + if(comp.toString().indexOf("QDiffuseMapMaterial") > -1) { + // store material definition + var m = { + "diffuseMap": comp.diffuse.data[0].source, + "shininess": comp.shininess, + "specular": comp.specular, + "ambient": comp.ambient, + "mode": root.renderMode + } + texCount++; + mats.push(m) + componentsToRemove.push(comp); + } + + if(comp.toString().indexOf("QPhongMaterial") > -1) { + // create MaterialSwitcher with default colors + mats.push({}) + componentsToRemove.push(comp); + } + } + + mats.forEach(function(m){ + // create a material switcher for each material definition + var matSwitcher = materialSwitcherComponent.createObject(entity, m) + matSwitcher.mode = Qt.binding(function(){ return root.renderMode }) + }) + + // remove replaced components + componentsToRemove.forEach(function(comp){ + Scene3DHelper.removeComponent(entity, comp); + }); + }) + return texCount; + } +} diff --git a/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml b/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml new file mode 100644 index 0000000000..657404d6f9 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml @@ -0,0 +1,20 @@ +import QtQuick 2.9 +import Qt3D.Core 2.1 + + +/** + * MediaLoaderEntity provides a unified interface for accessing statistics + * of a 3D media independently from the way it was loaded. + */ +Entity { + property url source + + /// Number of vertices + property int vertexCount + /// Number of faces + property int faceCount + /// Number of cameras + property int cameraCount + /// Number of textures + property int textureCount +} diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml new file mode 100644 index 0000000000..91c1ccf594 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -0,0 +1,16 @@ +pragma Singleton +import QtQuick 2.9 + +/** + * Viewer3DSettings singleton gathers properties related to the 3D Viewer capabilities, state and display options. + */ +Item { + readonly property Component abcLoaderComp: Qt.createComponent("AlembicLoader.qml") + readonly property bool supportAlembic: abcLoaderComp.status == Component.Ready + readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml") + readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready + + // Rasterized point size + property real pointSize: 4 + +} diff --git a/meshroom/ui/qml/Viewer3D/qmldir b/meshroom/ui/qml/Viewer3D/qmldir index 81a62925e2..b7cfa49e51 100644 --- a/meshroom/ui/qml/Viewer3D/qmldir +++ b/meshroom/ui/qml/Viewer3D/qmldir @@ -1,6 +1,7 @@ module Viewer3D Viewer3D 1.0 Viewer3D.qml +singleton Viewer3DSettings 1.0 Viewer3DSettings.qml DefaultCameraController 1.0 DefaultCameraController.qml Locator3D 1.0 Locator3D.qml Grid3D 1.0 Grid3D.qml From e5fa9f10871f06b90c1812e17adc08637f6318ad Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:06:40 +0100 Subject: [PATCH 036/293] [ui] Viewer3D: centralize render modes model in Viewer3DSettings --- meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml | 5 ++--- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml index 8972a257ee..7f0b85a2a9 100644 --- a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml +++ b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml @@ -14,7 +14,6 @@ Entity { objectName: "MaterialSwitcher" property int mode: 2 - readonly property var modes: ["Solid", "Wireframe", "Textured"] property string diffuseMap: "" property color ambient: "#AAA" property real shininess @@ -89,7 +88,7 @@ Entity { StateGroup { id: modeState - state: modes[mode] + state: Viewer3DSettings.renderModes[mode].name states: [ State { @@ -124,7 +123,7 @@ Entity { DiffuseSpecularMaterial { id: textured parent: root.parent - objectName: "SolidMaterial" + objectName: "TexturedMaterial" ambient: root.ambient shininess: root.shininess specular: root.specular diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 91c1ccf594..befc9f8893 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -1,5 +1,6 @@ pragma Singleton import QtQuick 2.9 +import MaterialIcons 2.2 /** * Viewer3DSettings singleton gathers properties related to the 3D Viewer capabilities, state and display options. @@ -10,6 +11,15 @@ Item { readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml") readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready + // Available render modes + readonly property var renderModes: [ // Can't use ListModel because of MaterialIcons expressions + {"name": "Solid", "icon": MaterialIcons.crop_din }, + {"name": "Wireframe", "icon": MaterialIcons.grid_on }, + {"name": "Textured", "icon": MaterialIcons.texture }, + ] + // Current render mode + property int renderMode: 2 + // Rasterized point size property real pointSize: 4 From e35076ef97d50a887c6fcaf2d039d2a15a64e1fa Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:18:16 +0100 Subject: [PATCH 037/293] [ui] Viewer3D: improved frame graph * enable PrimitivePicking to enable picking on point clouds * use OnDemand render policy * activate FrustrumCulling + increase far plane * add management of rasterized OpenGL points via a PointSize render --- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 37 ++++++++++++------- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 3 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index fe17679121..89ba61870e 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -167,7 +167,7 @@ FocusScope { projectionType: CameraLens.PerspectiveProjection fieldOfView: 45 nearPlane : 0.01 - farPlane : 1000.0 + farPlane : 10000.0 position: Qt.vector3d(28.0, 21.0, 28.0) upVector: Qt.vector3d(0.0, 1.0, 0.0) viewCenter: Qt.vector3d(0.0, 0.0, 0.0) @@ -224,28 +224,37 @@ FocusScope { components: [ RenderSettings { - // To avoid performance drops, picking is only enabled under certain circumstances (see ObjectPicker below) - pickingSettings.pickMethod: PickingSettings.TrianglePicking + pickingSettings.pickMethod: PickingSettings.PrimitivePicking // enables point/edge/triangle picking pickingSettings.pickResultMode: PickingSettings.NearestPick - renderPolicy: RenderSettings.Always - activeFrameGraph: Viewport { - normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) - RenderSurfaceSelector { + renderPolicy: RenderSettings.OnDemand + + activeFrameGraph: RenderSurfaceSelector { + // Use the whole viewport + Viewport { + normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) CameraSelector { id: cameraSelector camera: mainCamera - ClearBuffers { - buffers : ClearBuffers.ColorDepthBuffer - clearColor: Qt.rgba(0, 0, 0, 0.1) + FrustumCulling { + ClearBuffers { + clearColor: "transparent" + buffers : ClearBuffers.ColorDepthBuffer + RenderStateSet { + renderStates: [ + PointSize { + sizeMode: Viewer3DSettings.fixedPointSize ? PointSize.Fixed : PointSize.Programmable + value: Viewer3DSettings.pointSize + }, + DepthTest { depthFunction: DepthTest.Less } + ] + } + } } } } } }, - Qt3DInput.InputSettings { - eventSource: _window - enabled: true - } + Qt3DInput.InputSettings { } ] Entity { diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index befc9f8893..dcc190ab40 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -22,5 +22,6 @@ Item { // Rasterized point size property real pointSize: 4 - + // Whether point size is fixed or view dependent + property bool fixedPointSize: false } From 97fcdf67bf3697b7b53fece669cb3f70cd612894 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:20:29 +0100 Subject: [PATCH 038/293] [ui] Viewer3D: multi 3D media support * use new media loading backend through MediaLibrary * Inspector3D: new overlay UI that displays and allows to manipulate MediaLibrary content --- meshroom/ui/qml/Utils/format.js | 9 + meshroom/ui/qml/Utils/qmldir | 1 + meshroom/ui/qml/Viewer3D/Inspector3D.qml | 268 +++++++++++ meshroom/ui/qml/Viewer3D/Viewer3D.qml | 432 +++--------------- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 5 +- meshroom/ui/qml/WorkspaceView.qml | 54 +-- 6 files changed, 374 insertions(+), 395 deletions(-) create mode 100644 meshroom/ui/qml/Utils/format.js create mode 100644 meshroom/ui/qml/Viewer3D/Inspector3D.qml diff --git a/meshroom/ui/qml/Utils/format.js b/meshroom/ui/qml/Utils/format.js new file mode 100644 index 0000000000..5827ea3b69 --- /dev/null +++ b/meshroom/ui/qml/Utils/format.js @@ -0,0 +1,9 @@ +.pragma library + + +function intToString(v) { + // use EN locale to get comma separated thousands + // + remove automatically added trailing decimals + // (this 'toLocaleString' does not take any option) + return v.toLocaleString(Qt.locale('en-US')).split('.')[0] +} diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index e2e04ca8a6..9ab0a412a9 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -2,6 +2,7 @@ module Utils SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml Request 1.0 request.js +Format 1.0 format.js # causes random crash at application exit # singleton Filepath 1.0 Filepath.qml # singleton Scene3DHelper 1.0 Scene3DHelper.qml diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml new file mode 100644 index 0000000000..98bd7a38c4 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -0,0 +1,268 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import Qt3D.Core 2.0 +import Qt3D.Render 2.1 +import QtQuick.Controls.Material 2.4 +import Controls 1.0 +import Utils 1.0 + +FloatingPane { + id: root + + implicitWidth: 200 + + property int renderMode: 2 + property Transform targetTransform + property Locator3D origin: null + property Grid3D grid: null + property MediaLibrary mediaLibrary + property Camera camera + + signal mediaFocusRequest(var index) + signal mediaRemoveRequest(var index) + + MouseArea { anchors.fill: parent; onWheel: wheel.accepted = true } + + ColumnLayout { + width: parent.width + height: parent.height + spacing: 10 + + Label { text: "RENDER"; font.bold: true; font.pointSize: 8 } + Flow { + Layout.fillWidth: true + Repeater { + model: Viewer3DSettings.renderModes + + delegate: MaterialToolButton { + text: modelData["icon"] + ToolTip.text: modelData["name"] + " (" + (index+1) + ")" + onClicked: Viewer3DSettings.renderMode = index + checked: Viewer3DSettings.renderMode === index + } + } + } + + Label { text: "SCENE"; font.bold: true; font.pointSize: 8 } + + GridLayout { + id: controlsLayout + Layout.fillWidth: true + columns: 3 + columnSpacing: 6 + Flow { + Layout.columnSpan: 3 + Layout.fillWidth: true + spacing: 0 + CheckBox { + text: "Grid" + checked: Viewer3DSettings.displayGrid + onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid + } + CheckBox { + text: "Locator" + checked: Viewer3DSettings.displayLocator + onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator + } + } + + // Rotation Controls + Label { + font.family: MaterialIcons.fontFamily + text: MaterialIcons.rotation3D + font.pointSize: 14 + Layout.rowSpan: 3 + } + + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value} + Label { text: "X" } + + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value} + Label { text: "Y" } + + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value } + Label { text: "Z" } + + Label { text: "Points" } + RowLayout { + Layout.columnSpan: 2 + Slider { + Layout.fillWidth: true; from: 1; to: 20;stepSize: 0.1 + value: Viewer3DSettings.pointSize + onValueChanged: Viewer3DSettings.pointSize = value + } + CheckBox { + text: "Fixed"; + checked: Viewer3DSettings.fixedPointSize + onClicked: Viewer3DSettings.fixedPointSize = !Viewer3DSettings.fixedPointSize + } + } + + } + + Label { text: "MEDIA"; font.bold: true; font.pointSize: 8 } + + ListView { + id: mediaListView + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + model: mediaLibrary.model + spacing: 2 + //section.property: "section" + + ScrollBar.vertical: ScrollBar { id: scrollBar } + + section.delegate: Pane { + width: parent.width + padding: 1 + background: null + + Label { + width: parent.width + padding: 4 + background: Rectangle { color: Qt.darker(parent.palette.base, 1.15) } + text: section + } + } + + Connections { + target: mediaLibrary + onLoadRequest: { + mediaListView.positionViewAtIndex(idx, ListView.Visible); + } + } + + delegate: RowLayout { + // add mediaLibrary.count in the binding to ensure 'entity' + // is re-evaluated when mediaLibrary delegates are modified + property bool loading: model.status === SceneLoader.Loading + spacing: 2 + width: parent.width - scrollBar.width / 2 + + property string src: model.source + onSrcChanged: focusAnim.restart() + + RowLayout { + Layout.alignment: Qt.AlignTop + enabled: model.status === SceneLoader.Ready + spacing: 0 + + MaterialToolButton { + text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off + font.pointSize: 10 + ToolTip.text: model.visible ? "Hide" : "Show" + flat: true + opacity: model.visible ? 1.0 : 0.6 + onClicked: { + if(hoverArea.modifiers & Qt.ControlModifier) + mediaLibrary.solo(index); + else + model.visible = !model.visible + } + // Handle modifiers on button click + MouseArea { + id: hoverArea + property int modifiers + anchors.fill: parent + hoverEnabled: true + onPositionChanged: modifiers = mouse.modifiers + onExited: modifiers = Qt.NoModifier + onPressed: { + modifiers = mouse.modifiers; + mouse.accepted = false; + } + } + } + MaterialToolButton { + text: MaterialIcons.filter_center_focus + font.pointSize: 10 + ToolTip.text: "Frame" + onClicked: camera.viewEntity(mediaLibrary.entityAt(index)) + flat: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 1 + Layout.alignment: Qt.AlignTop + + Label { + id: label + leftPadding: 0 + rightPadding: 0 + topPadding: 3 + bottomPadding: topPadding + Layout.fillWidth: true + text: model.label + elide: Text.ElideMiddle + background: Rectangle { + Connections { + target: mediaLibrary + onLoadRequest: if(idx == index) focusAnim.restart() + } + ColorAnimation on color { + id: focusAnim + from: label.palette.highlight + to: "transparent" + duration: 2000 + } + MouseArea { + anchors.fill: parent + onDoubleClicked: camera.viewEntity(mediaLibrary.entityAt(index)) + } + } + } + Item { + Layout.fillWidth: true + implicitHeight: childrenRect.height + RowLayout { + visible: model.status === SceneLoader.Ready + MaterialLabel { visible: model.vertexCount; text: MaterialIcons.grain } + Label { visible: model.vertexCount; text: Format.intToString(model.vertexCount) } + MaterialLabel { visible: model.faceCount; text: MaterialIcons.details; rotation: -180 } + Label { visible: model.faceCount; text: Format.intToString(model.faceCount) } + MaterialLabel { visible: model.cameraCount; text: MaterialIcons.videocam } + Label { visible: model.cameraCount; text: model.cameraCount } + MaterialLabel { visible: model.textureCount; text: MaterialIcons.texture } + Label { visible: model.textureCount; text: model.textureCount } + } + } + } + + MaterialToolButton { + id: requestMediaButton + Layout.alignment: Qt.AlignTop + + enabled: !loading + text: loading || !model.requested ? MaterialIcons.radio_button_unchecked : MaterialIcons.radio_button_checked + font.pointSize: 10 + palette.buttonText: model.valid ? "#4CAF50" : label.palette.buttonText + ToolTip.text: "" + onClicked: model.requested = !model.requested + } + + MaterialToolButton { + Layout.alignment: Qt.AlignTop + + visible: !loading + text: MaterialIcons.clear + font.pointSize: 10 + ToolTip.text: "Remove" + onClicked: mediaLibrary.remove(index) + } + + BusyIndicator { + visible: loading + running: visible + padding: 0 + implicitHeight: 14 + implicitWidth: requestMediaButton.width + } + } + } + } +} diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index 89ba61870e..fcbd5e3d84 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -1,6 +1,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 +import QtQuick.Controls 1.4 as Controls1 import QtQuick.Layouts 1.3 +import QtQml.Models 2.2 import QtQuick.Scene3D 2.0 import Qt3D.Core 2.1 import Qt3D.Render 2.1 @@ -11,22 +13,12 @@ import MaterialIcons 2.2 import Controls 1.0 + FocusScope { id: root - property alias source: modelLoader.source - property alias abcSource: modelLoader.abcSource - property alias depthMapSource: modelLoader.depthMapSource - property int renderMode: 2 - readonly property alias loading: modelLoader.loading - readonly property alias polyCount: modelLoader.polyCount - - // Alembic optional support => won't be available if AlembicEntity plugin is not available - readonly property Component abcLoaderComp: Qt.createComponent("AlembicLoader.qml") - readonly property bool supportAlembic: abcLoaderComp.status == Component.Ready - readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml") - readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready + property alias library: mediaLibrary // functions function resetCameraCenter() { @@ -40,122 +32,41 @@ FocusScope { mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0); } - function findChildrenByProperty(node, propertyName, container) - { - if(!node || !node.childNodes) - return; - for(var i=0; i < node.childNodes.length; ++i) - { - var childNode = node.childNodes[i]; - if(!childNode) - continue; - if(childNode[propertyName] !== undefined) - container.push(childNode); - else - findChildrenByProperty(childNode, propertyName, container) - } + function load(filepath) { + mediaLibrary.load(filepath); } - // Remove automatically created DiffuseMapMaterial and - // instantiate a MaterialSwitcher instead - function setupMaterialSwitchers(rootEntity) - { - var materials = []; - findChildrenByProperty(rootEntity, "diffuse", materials); - var entities = [] - materials.forEach(function(mat){ - entities.push(mat.parent) - }) - - entities.forEach(function(entity) { - var mats = [] - var hasTextures = false - // Create as many MaterialSwitcher as individual materials for this entity - // NOTE: we let each MaterialSwitcher modify the components of the entity - // and therefore remove the default material spawned by the sceneLoader - for(var i=0; i < entity.components.length; ++i) - { - var comp = entity.components[i] - // handle DiffuseMapMaterials created by SceneLoader - if(comp.toString().indexOf("QDiffuseMapMaterial") > -1) - { - // store material definition - var m = { - "diffuseMap": comp.diffuse.data[0].source, - "shininess": comp.shininess, - "specular": comp.specular, - "ambient": comp.ambient, - "mode": root.renderMode - } - mats.push(m) - hasTextures = true - } - - if(comp.toString().indexOf("QPhongMaterial") > -1) { - // create MaterialSwitcher with default colors - mats.push({}) - } - // Retrieve polycount using vertexPosition buffer - if(comp.toString().indexOf("Geometry") > -1) { - for(var k = 0; k < comp.geometry.attributes.length; ++k) - { - if(comp.geometry.attributes[k].name == "vertexPosition") - modelLoader.polyCount += comp.geometry.attributes[k].count / 3 - } - } - } - - modelLoader.meshHasTexture = mats.length > 0 - mats.forEach(function(m){ - // create a material switcher for each material definition - var matSwitcher = materialSwitcherComponent.createObject(entity, m) - matSwitcher.mode = Qt.binding(function(){ return root.renderMode }) - }) - }) - } - - Component { - id: materialSwitcherComponent - MaterialSwitcher {} - } - - function clear() - { - clearScene() - clearAbc() + function view(attribute) { + mediaLibrary.view(attribute) } - function clearScene() - { - source = '' - } - - function clearAbc() - { - abcSource = '' - } - - function clearDepthMap() - { - depthMapSource = 'no_file' - depthMapSource = '' + function clear() { + mediaLibrary.clear() } SystemPalette { id: activePalette } + Scene3D { id: scene3D anchors.fill: parent cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // vs. UserAspectRatio - hoverEnabled: false // if true, will trigger positionChanged events in attached MouseHandler + hoverEnabled: true // if true, will trigger positionChanged events in attached MouseHandler aspects: ["logic", "input"] focus: true + Keys.onPressed: { if (event.key == Qt.Key_F) { resetCameraCenter(); resetCameraPosition(); - event.accepted = true; + } + else if(Qt.Key_1 <= event.key && event.key <= Qt.Key_3) + { + Viewer3DSettings.renderMode = event.key - Qt.Key_1; + } + else { + event.accepted = false } } @@ -176,16 +87,30 @@ FocusScope { Behavior on viewCenter { Vector3dAnimation { duration: 250 } } + // Scene light, attached to the camera + Entity { + components: [ + PointLight { + color: "white" + } + ] + } } - // Scene light, attached to the camera Entity { components: [ - PointLight { - color: "white" + SphereMesh { }, Transform { - translation: mainCamera.position + id: viewCenterTransform + translation: mainCamera.viewCenter + scale: 0.005 * mainCamera.viewCenter.minus(mainCamera.position).length() + }, + PhongMaterial { + ambient: "#FFF" + shininess: 0.2 + diffuse: activePalette.highlight + specular: activePalette.highlight } ] } @@ -257,272 +182,49 @@ FocusScope { Qt3DInput.InputSettings { } ] - Entity { - id: modelLoader - property string source - property string abcSource - property string depthMapSource - property int polyCount - property bool meshHasTexture: false - // SceneLoader status is not reliable when loading a 3D file - property bool loading: false - onSourceChanged: { - polyCount = 0 - meshHasTexture = false - loading = true - } - onAbcSourceChanged: { - if(root.supportAlembic) - loading = true - } - - components: [sceneLoaderEntity, transform, picker] - - // ObjectPicker used for view re-centering - ObjectPicker { - id: picker - // Triangle picking is expensive - // Only activate it when a double click may happen or when the 'Control' key is pressed - enabled: cameraController.pickingActive || doubleClickTimer.running - hoverEnabled: false - onPressed: { - if(pick.button == Qt.LeftButton) - mainCamera.viewCenter = pick.worldIntersection - doubleClickTimer.stop() - } - } - - Transform { - id: transform - } - - Entity { - id: sceneLoaderEntity - enabled: showMeshCheckBox.checked - - components: [ - SceneLoader { - id: scene - source: modelLoader.source - onStatusChanged: { - if(scene.status != SceneLoader.Loading) - modelLoader.loading = false; - if(scene.status == SceneLoader.Ready) - { - setupMaterialSwitchers(modelLoader) - } - } - } - ] - } - - Entity { - id: abcLoaderEntity - // Instantiate the AlembicEntity dynamically - // to avoid import errors if the plugin is not available - property Entity abcLoader: null - enabled: showSfMCheckBox.checked - - Component.onCompleted: { - if(!root.supportAlembic) // Alembic plugin not available - return - - // destroy previously created entity - if(abcLoader != undefined) - abcLoader.destroy() - - abcLoader = abcLoaderComp.createObject(abcLoaderEntity, { - 'url': Qt.binding(function() { return modelLoader.abcSource } ), - 'particleSize': Qt.binding(function() { return 0.01 * transform.scale }), - 'locatorScale': Qt.binding(function() { return 0.2}) - }); - // urlChanged signal is emitted once the Alembic file is loaded - // set the 'loading' property to false when it's emitted - // TODO: AlembicEntity should expose a status - abcLoader.onUrlChanged.connect(function(){ - modelLoader.loading = false - spawnCameraSelectors() - }) - modelLoader.loading = false - } - function spawnCameraSelectors() { - // spawn camera selector for each camera - for(var i = 0; i < abcLoader.cameras.length; ++i) - { - var cam = abcLoader.cameras[i] - // retrieve view id - var viewId = cam.userProperties["mvg_viewId"] - if(viewId == undefined) - continue - var obj = camSelectionComponent.createObject(cam) - obj.viewId = viewId - } - } + MediaLibrary { + id: mediaLibrary + renderMode: Viewer3DSettings.renderMode + // Picking to set focus point (camera view center) + // Only activate it when a double click may happen or when the 'Control' key is pressed + pickingEnabled: cameraController.pickingActive || doubleClickTimer.running - // Camera selection picking and display - property Component camSelectionComponent: Component { - id: camSelectionComponent - Entity { - property string viewId - property alias ambient: mat.ambient - components: [ - CuboidMesh { xExtent: 0.2; yExtent: 0.2; zExtent: 0.2}, - PhongMaterial{ - id: mat - ambient: viewId == _reconstruction.selectedViewId ? activePalette.highlight : "#CCC" - diffuse: ambient - }, - ObjectPicker { - onClicked: _reconstruction.selectedViewId = viewId - } - ] - } + components: [ + Transform { + id: transform } - } + ] - Entity { - id: depthMapLoaderEntity - // Instantiate the DepthMapEntity dynamically - // to avoid import errors if the plugin is not available - property Entity depthMapLoader: null - enabled: showDepthMapCheckBox.checked - - Component.onCompleted: { - if(!root.supportDepthMap) // DepthMap plugin not available - return - - // destroy previously created entity - if(depthMapLoader != undefined) - depthMapLoader.destroy() - - depthMapLoader = depthMapLoaderComp.createObject(depthMapLoaderEntity, { - 'source': Qt.binding(function() { return modelLoader.depthMapSource } ) - }); - // 'sourceChanged' signal is emitted once the depthMap file is loaded - // set the 'loading' property to false when it's emitted - // TODO: DepthMapEntity should expose a status - depthMapLoader.onSourceChanged.connect(function(){ modelLoader.loading = false }) - modelLoader.loading = false + onPressed: { + if(pick.button == Qt.LeftButton) + { + mainCamera.viewCenter = pick.worldIntersection; } + doubleClickTimer.stop(); } - Locator3D { enabled: locatorCheckBox.checked } - } - Grid3D { enabled: gridCheckBox.checked } - } - } - - // - // UI Overlay - // - - // Rotation/Scale - FloatingPane { - anchors { top: parent.top; left: parent.left } - - GridLayout { - id: controlsLayout - columns: 3 - columnSpacing: 6 - - property int sliderWidth: 70 - - // Rotation Controls - Label { - font.family: MaterialIcons.fontFamily - text: MaterialIcons.rotation3D - font.pointSize: 14 - Layout.rowSpan: 3 + Locator3D { enabled: Viewer3DSettings.displayLocator } } - Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationX = value } - Label { text: "X" } - - Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationY = value } - Label { text: "Y" } - - Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationZ = value } - Label { text: "Z" } - - // Scale Control - Label { text: "Scale" } - Slider { Layout.columnSpan: 2; implicitWidth: controlsLayout.sliderWidth; from: 1; to: 10; onPositionChanged: transform.scale = value } + Grid3D { enabled: Viewer3DSettings.displayGrid } } } - // Outliner - FloatingPane { - anchors { top: parent.top; right: parent.right } - - Column { - Row { - visible: root.supportAlembic - CheckBox { id: showSfMCheckBox; text: "SfM"; checked: true; visible: root.supportAlembic; opacity: root.abcSource ? 1.0 : 0.6 } - ToolButton { - text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.abcSource != ''; - onClicked: clearAbc() - ToolTip.text: "Unload" - ToolTip.visible: hovered - } - } - Row { - visible: root.depthMapSource != '' - CheckBox { id: showDepthMapCheckBox; text: "DepthMap"; checked: true; } - ToolButton { - text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; - onClicked: clearDepthMap() - ToolTip.text: "Unload" - ToolTip.visible: hovered - } - } - Row { - CheckBox { id: showMeshCheckBox; text: "Mesh"; checked: true; opacity: root.source ? 1.0 : 0.6 } - ToolButton { - text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.source != ''; - onClicked: clearScene() - ToolTip.text: "Unload" - ToolTip.visible: hovered - } - } - CheckBox { id: gridCheckBox; text: "Grid"; checked: true } - CheckBox { id: locatorCheckBox; text: "Locator"; checked: true } - } - } - - // Render Mode - FloatingPane { - anchors { bottom: parent.bottom; left: parent.left } + // UI Overlay + Controls1.SplitView { + id: overlaySplitView + anchors.fill: parent + Item { Layout.fillWidth: true; Layout.minimumWidth: parent.width * 0.5 } - Row { - anchors.verticalCenter: parent.verticalCenter - Repeater { - model: [ // Can't use ListModel because of MaterialIcons expressions - {"name": "Solid", "icon": MaterialIcons.crop_din}, - {"name": "Wireframe", "icon": MaterialIcons.grid_on}, - {"name": "Textured", "icon": MaterialIcons.texture }, - ] - delegate: ToolButton { - text: modelData["icon"] - ToolTip.text: modelData["name"] - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - font.pointSize: 11 - padding: 4 - onClicked: root.renderMode = index - checkable: !checked // hack to disable check toggle on click - checked: renderMode === index - } - } - } - } + Inspector3D { + id: inspector + width: 220 + Layout.minimumWidth: 5 - FloatingPane { - anchors.right: parent.right - anchors.bottom: parent.bottom - visible: modelLoader.polyCount > 0 - Label { - text: modelLoader.polyCount + " faces" + camera: mainCamera + targetTransform: transform + mediaLibrary: mediaLibrary } } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index dcc190ab40..95031e5fc0 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -21,7 +21,10 @@ Item { property int renderMode: 2 // Rasterized point size - property real pointSize: 4 + property real pointSize: 1.5 // Whether point size is fixed or view dependent property bool fixedPointSize: false + // Helpers display + property bool displayGrid: true + property bool displayLocator: true } diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 9e343d3b5a..c50b3bcb87 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -21,35 +21,33 @@ Item { readonly property variant cameraInits: _reconstruction.cameraInits property bool readOnly: false + implicitWidth: 300 implicitHeight: 400 // Load a 3D media file in the 3D viewer - function load3DMedia(filepath) - { - if(!Filepath.exists(Filepath.urlToString(filepath))) - return - switch(Filepath.extension(filepath)) - { - case ".abc": viewer3D.abcSource = filepath; break; - case ".exr": viewer3D.depthMapSource = filepath; break; - case ".obj": viewer3D.source = filepath; break; - } + function load3DMedia(filepath) { + viewer3D.load(filepath); + } + + function viewAttribute(attr) { + viewer3D.view(attr); } Connections { target: reconstruction - onGraphChanged: { - viewer3D.clear() - viewer2D.clear() - } - onSfmReportChanged: { - viewer3D.abcSource = '' - if(!reconstruction.sfm) - return - load3DMedia(Filepath.stringToUrl(reconstruction.sfm.attribute('output').value)) - } + onGraphChanged: viewer3D.clear() + onSfmChanged: viewSfM() + onSfmReportChanged: viewSfM() + } + Component.onCompleted: viewSfM() + + // Load reconstruction's current SfM file + function viewSfM() { + if(!reconstruction.sfm) + return; + viewAttribute(reconstruction.sfm.attribute('output')); } SystemPalette { id: activePalette } @@ -115,12 +113,16 @@ Item { Panel { title: "3D Viewer" - implicitWidth: Math.round(parent.width * 0.33) + implicitWidth: Math.round(parent.width * 0.45) Layout.minimumWidth: 20 Layout.minimumHeight: 80 Viewer3D { id: viewer3D + readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null + readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS" + readonly property int outputMediaIndex: library.find(outputAttribute) + anchors.fill: parent DropArea { anchors.fill: parent @@ -129,20 +131,14 @@ Item { } } - Label { - anchors.centerIn: parent - text: "Loading..." - visible: viewer3D.loading - padding: 6 - background: Rectangle { color: parent.palette.base; opacity: 0.5 } - } - // Load reconstructed model Button { text: "Load Model" anchors.bottom: parent.bottom anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter + visible: viewer3D.outputReady && viewer3D.outputMediaIndex == -1 + onClicked: viewAttribute(_reconstruction.endNode.attribute("outputMesh")) } } } From e9682a8ce9c0dcfa820fd4558807a7fc87170914 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:27:19 +0100 Subject: [PATCH 039/293] [ui] Viewer3D: load all urls from an external file drop --- meshroom/ui/qml/WorkspaceView.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index c50b3bcb87..63e2a3a61b 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -127,7 +127,9 @@ Item { DropArea { anchors.fill: parent keys: ["text/uri-list"] - onDropped: load3DMedia(drop.urls[0]) + onDropped: { + drop.urls.forEach(function(url){ load3DMedia(url); }); + } } } From 717b4f8b37284c2703de4df180a71cf85c5a7140 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:32:10 +0100 Subject: [PATCH 040/293] [ui] GraphEditor: display compatible attributes in Viewer3D on double click --- .../ui/qml/GraphEditor/AttributeEditor.qml | 3 +++ .../qml/GraphEditor/AttributeItemDelegate.qml | 5 ++++ meshroom/ui/qml/main.qml | 26 ++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index cd075ecf06..2858121211 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -16,6 +16,8 @@ ColumnLayout { signal upgradeRequest() + signal attributeDoubleClicked(var attribute) + spacing: 0 Pane { @@ -98,6 +100,7 @@ ColumnLayout { labelWidth: 180 width: attributesListView.width attribute: object + onDoubleClicked: root.attributeDoubleClicked(attr) } // Helper MouseArea to lose edit/activeFocus // when clicking on the background diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index e96bc8ad65..0965114912 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -18,6 +18,8 @@ RowLayout { readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly + signal doubleClicked(var attr) + spacing: 4 Label { @@ -50,6 +52,7 @@ RowLayout { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.AllButtons + onDoubleClicked: root.doubleClicked(root.attribute) property Component menuComp: Menu { id: paramMenu @@ -307,6 +310,7 @@ RowLayout { obj.label.text = index obj.label.horizontalAlignment = Text.AlignHCenter obj.label.verticalAlignment = Text.AlignVCenter + obj.doubleClicked.connect(function(attr) {root.doubleClicked(attr)}) } ToolButton { enabled: root.editable @@ -346,6 +350,7 @@ RowLayout { }) obj.Layout.fillWidth = true obj.labelWidth = 100 // reduce label width for children (space gain) + obj.doubleClicked.connect(function(attr) {root.doubleClicked(attr)}) } } } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index d0b1ce84e7..89684b545c 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -508,6 +508,7 @@ ApplicationWindow { } Panel { + id: graphEditorPanel Layout.fillWidth: true Layout.fillHeight: false padding: 0 @@ -515,6 +516,16 @@ ApplicationWindow { title: "Graph Editor" visible: settings_UILayout.showGraphEditor + function displayAttribute(attr) { + if( attr.desc.type === "File" + && _3dFileExtensions.indexOf(Filepath.extension(attr.value)) > - 1 ) + { + workspaceView.viewAttribute(attr); + return true; + } + return false; + } + Controls1.SplitView { orientation: Qt.Horizontal anchors.fill: parent @@ -533,7 +544,7 @@ ApplicationWindow { readOnly: _reconstruction.computing onNodeDoubleClicked: { - if(node.nodeType == "StructureFromMotion") + if(node.nodeType === "StructureFromMotion") { _reconstruction.sfm = node return @@ -542,12 +553,10 @@ ApplicationWindow { { var attr = node.attributes.at(i) if(attr.isOutput - && attr.desc.type === "File" - && _3dFileExtensions.indexOf(Filepath.extension(attr.value)) > - 1 ) - { - workspaceView.load3DMedia(Filepath.stringToUrl(attr.value)) - break // only load first model found - } + && graphEditorPanel.displayAttribute(attr)) + { + break; + } } } } @@ -565,6 +574,9 @@ ApplicationWindow { node: graphEditor.selectedNode // Make AttributeEditor readOnly when computing readOnly: _reconstruction.computing + onAttributeDoubleClicked: { + graphEditorPanel.displayAttribute(attribute) + } onUpgradeRequest: { var n = _reconstruction.upgradeNode(node) From 2e06ad1b839cfcec1a7cbc1c96805cb231483a29 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Dec 2018 16:32:46 +0100 Subject: [PATCH 041/293] [ui] 3DViewer: make 'visible' property drive media load request * remove the notion of manual media (un)loading from high-level UI * visibility button now drives media loading: * if media is not available (not yet computed), it will be loaded once available if visibility is on * once loaded, media can't be explicitly unloaded * use an icon to indicate that the media is not available instead of colors --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 21 ++++++++++----------- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 98bd7a38c4..cd28b7bdb3 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -147,7 +147,6 @@ FloatingPane { RowLayout { Layout.alignment: Qt.AlignTop - enabled: model.status === SceneLoader.Ready spacing: 0 MaterialToolButton { @@ -233,19 +232,18 @@ FloatingPane { } } + // Media unavailability indicator MaterialToolButton { - id: requestMediaButton Layout.alignment: Qt.AlignTop - - enabled: !loading - text: loading || !model.requested ? MaterialIcons.radio_button_unchecked : MaterialIcons.radio_button_checked + enabled: false + visible: !model.valid + text: MaterialIcons.no_sim font.pointSize: 10 - palette.buttonText: model.valid ? "#4CAF50" : label.palette.buttonText - ToolTip.text: "" - onClicked: model.requested = !model.requested } + // Remove media from library button MaterialToolButton { + id: removeButton Layout.alignment: Qt.AlignTop visible: !loading @@ -255,12 +253,13 @@ FloatingPane { onClicked: mediaLibrary.remove(index) } + // Media loading indicator BusyIndicator { visible: loading running: visible - padding: 0 - implicitHeight: 14 - implicitWidth: requestMediaButton.width + padding: removeButton.padding + implicitHeight: implicitWidth + implicitWidth: removeButton.width } } } diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index d8c6baa65f..b06381497c 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -49,8 +49,6 @@ Entity { var idx = find(source); if(idx === -1) return - // load / make media visible - m.mediaModel.get(idx).requested = true; m.mediaModel.get(idx).visible = true; loadRequest(idx); } @@ -143,6 +141,7 @@ Entity { readonly property var attribute: model.attribute readonly property int idx: index readonly property var modelSource: attribute || model.source + readonly property bool visible: model.visible // multi-step binding to ensure MediaLoader source is properly // updated when needed, whether raw source is valid or not @@ -163,7 +162,7 @@ Entity { readonly property string finalSource: model.requested ? currentSource : "" renderMode: root.renderMode - enabled: model.visible + enabled: visible // QObject.destroyed signal is not accessible // Use the object as NodeInstantiator model to be notified of its deletion @@ -173,6 +172,17 @@ Entity { onObjectRemoved: remove(idx) } + // 'visible' property drives media loading request + onVisibleChanged: { + // always request media loading if visible + if(model.visible) + model.requested = true; + // only cancel loading request if media is not valid + // (a media won't be unloaded if already loaded, only hidden) + else if(!model.valid) + model.requested = false; + } + function updateModelAndCache(forceRequest) { // don't cache explicitely unloaded media if(model.requested && object && dependencyReady) { @@ -184,8 +194,10 @@ Entity { if(attribute) { model.source = rawSource; } - // auto-restore entity if raw source is in cache + // auto-restore entity if raw source is in cache ... model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource); + // ... and update media visibility (useful if media was hidden but loaded back from cache) + model.visible = model.requested; model.valid = Filepath.exists(rawSource) && dependencyReady; } From 9534e75552cf3be713ecff31950704bce1d449b5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Dec 2018 15:28:17 +0100 Subject: [PATCH 042/293] [ui.utils] makeProperty: destroyCallback mechanism for QObject-type properties allow to be warned when the underlying value of a QObject-type property has been destroyed and react to this --- meshroom/ui/utils.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/utils.py b/meshroom/ui/utils.py index 860419337a..b660d7cc61 100755 --- a/meshroom/ui/utils.py +++ b/meshroom/ui/utils.py @@ -1,8 +1,9 @@ import os import time -from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property +from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property, QObject from PySide2.QtQml import QQmlApplicationEngine +from PySide2 import shiboken2 class QmlInstantEngine(QQmlApplicationEngine): @@ -193,7 +194,7 @@ def reload(self): self.load(self._sourceFile) -def makeProperty(T, attributeName, notify=None): +def makeProperty(T, attributeName, notify=None, destroyCallback=None): """ Shortcut function to create a Qt Property with generic getter and setter. @@ -204,6 +205,8 @@ def makeProperty(T, attributeName, notify=None): T (type): the type of the property attributeName (str): the name of underlying instance attribute to get/set notify (Signal): the notify signal; if None, property will be constant + destroyCallback (function): (optional) Function to call when value gets destroyed. + Only applicable for QObject-type properties. Examples: class Foo(QObject): @@ -217,12 +220,17 @@ class Foo(QObject): Returns: Property: the created Property """ - def setter(instance, value, notifyName): + def setter(instance, value): """ Generic setter. """ - if getattr(instance, attributeName) == value: + currentValue = getattr(instance, attributeName) + if currentValue == value: return + if destroyCallback and currentValue and shiboken2.isValid(currentValue): + currentValue.destroyed.disconnect(destroyCallback) setattr(instance, attributeName, value) - getattr(instance, notifyName).emit() + if destroyCallback and value: + value.destroyed.connect(destroyCallback) + getattr(instance, signalName(notify)).emit() def getter(instance): """ Generic getter. """ @@ -233,7 +241,9 @@ def signalName(signalInstance): # string representation contains trailing '()', remove it return str(signalInstance)[:-2] + if destroyCallback and not issubclass(T, QObject): + raise RuntimeError("destroyCallback can only be used with QObject-type properties.") if notify: - return Property(T, getter, lambda self, value: setter(self, value, signalName(notify)), notify=notify) + return Property(T, getter, setter, notify=notify) else: return Property(T, getter, constant=True) From 5b991053a8078820dc15443e556f30c0c9da5311 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Dec 2018 15:42:38 +0100 Subject: [PATCH 043/293] [ui] UIGraph: add selectedNode property move node selection management to the Python side --- meshroom/ui/graph.py | 10 ++++++++++ meshroom/ui/qml/GraphEditor/GraphEditor.qml | 5 ++--- meshroom/ui/qml/main.qml | 6 +++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 49d03eb8f6..8f41020683 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -201,6 +201,7 @@ def __init__(self, filepath='', parent=None): self._running = self._submitted = False self._sortedDFSChunks = QObjectListModel(parent=self) self._layout = GraphLayout(self) + self._selectedNode = None if filepath: self.load(filepath) @@ -235,6 +236,7 @@ def updateChunks(self): def clear(self): if self._graph: + self.clearNodeSelection() self._graph.deleteLater() self._graph = None self._sortedDFSChunks.clear() @@ -466,6 +468,10 @@ def appendAttribute(self, attribute, value=QJsonValue()): def removeAttribute(self, attribute): self.push(commands.ListAttributeRemoveCommand(self._graph, attribute)) + def clearNodeSelection(self): + """ Clear node selection. """ + self.selectedNode = None + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graphChanged = Signal() graph = Property(Graph, lambda self: self._graph, notify=graphChanged) @@ -480,3 +486,7 @@ def removeAttribute(self, attribute): sortedDFSChunks = Property(QObject, lambda self: self._sortedDFSChunks, constant=True) lockedChanged = Signal() + + selectedNodeChanged = Signal() + # Currently selected node + selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, clearNodeSelection) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index d1f5e50bc4..238bba1d1c 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -14,7 +14,6 @@ Item { readonly property variant graph: uigraph ? uigraph.graph : null /// core graph contained in ui graph property variant nodeTypesModel: null /// the list of node types that can be instantiated property bool readOnly: false - property variant selectedNode: null property var _attributeToDelegate: ({}) @@ -52,7 +51,7 @@ Item { /// Select node delegate function selectNode(node) { - root.selectedNode = node + uigraph.selectedNode = node } /// Duplicate a node and optionnally all the following ones @@ -308,7 +307,7 @@ Item { node: object width: uigraph.layout.nodeWidth readOnly: root.readOnly - selected: root.selectedNode == node + selected: uigraph.selectedNode === node onSelectedChanged: if(selected) forceActiveFocus() onAttributePinCreated: registerAttributePin(attribute, pin) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 89684b545c..5502cb56df 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -568,10 +568,10 @@ ApplicationWindow { Loader { anchors.fill: parent anchors.margins: 2 - active: graphEditor.selectedNode != null + active: _reconstruction.selectedNode !== null sourceComponent: Component { AttributeEditor { - node: graphEditor.selectedNode + node: _reconstruction.selectedNode // Make AttributeEditor readOnly when computing readOnly: _reconstruction.computing onAttributeDoubleClicked: { @@ -580,7 +580,7 @@ ApplicationWindow { onUpgradeRequest: { var n = _reconstruction.upgradeNode(node) - graphEditor.selectNode(n) + _reconstruction.selectedNode = n; } } } From 05854ed897f18e8363a070ba6bfc7fe86dcfa142 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Dec 2018 19:13:17 +0100 Subject: [PATCH 044/293] [ui] UIGraph: add hoveredNode property + node hovering visual feedback * keep track of currently hovered node in UIGraph on Python side * Node: show border on hover + make MouseArea contain everything else to always get hover events, even when cursor is over children attribute pins --- meshroom/ui/graph.py | 10 + meshroom/ui/qml/GraphEditor/GraphEditor.qml | 4 + meshroom/ui/qml/GraphEditor/Node.qml | 214 ++++++++++---------- 3 files changed, 124 insertions(+), 104 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 8f41020683..de975832be 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -202,6 +202,7 @@ def __init__(self, filepath='', parent=None): self._sortedDFSChunks = QObjectListModel(parent=self) self._layout = GraphLayout(self) self._selectedNode = None + self._hoveredNode = None if filepath: self.load(filepath) @@ -236,6 +237,7 @@ def updateChunks(self): def clear(self): if self._graph: + self.clearNodeHover() self.clearNodeSelection() self._graph.deleteLater() self._graph = None @@ -472,6 +474,10 @@ def clearNodeSelection(self): """ Clear node selection. """ self.selectedNode = None + def clearNodeHover(self): + """ Reset currently hovered node to None. """ + self.hoveredNode = None + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graphChanged = Signal() graph = Property(Graph, lambda self: self._graph, notify=graphChanged) @@ -490,3 +496,7 @@ def clearNodeSelection(self): selectedNodeChanged = Signal() # Currently selected node selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, clearNodeSelection) + + hoveredNodeChanged = Signal() + # Currently hovered node + hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, clearNodeHover) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 238bba1d1c..3548b52997 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -308,6 +308,7 @@ Item { width: uigraph.layout.nodeWidth readOnly: root.readOnly selected: uigraph.selectedNode === node + hovered: uigraph.hoveredNode === node onSelectedChanged: if(selected) forceActiveFocus() onAttributePinCreated: registerAttributePin(attribute, pin) @@ -331,6 +332,9 @@ Item { onMoved: uigraph.moveNode(node, position) + onEntered: uigraph.hoveredNode = node + onExited: uigraph.hoveredNode = null + Keys.onDeletePressed: uigraph.removeNode(node) Behavior on x { diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 6f02192992..babf307823 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -13,14 +13,17 @@ Item { readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue") readonly property color defaultColor: isCompatibilityNode ? "#444" : "#607D8B" property bool selected: false + property bool hovered: false signal pressed(var mouse) signal doubleClicked(var mouse) signal moved(var position) + signal entered() + signal exited() signal attributePinCreated(var attribute, var pin) signal attributePinDeleted(var attribute, var pin) - implicitHeight: body.height + implicitHeight: childrenRect.height objectName: node.name SystemPalette { id: activePalette } @@ -39,7 +42,8 @@ Item { } MouseArea { - anchors.fill: parent + width: parent.width + height: body.height drag.target: parent // small drag threshold to avoid moving the node by mistake drag.threshold: 2 @@ -47,128 +51,130 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: root.pressed(mouse) onDoubleClicked: root.doubleClicked(mouse) + onEntered: root.entered() + onExited: root.exited() drag.onActiveChanged: { if(!drag.active) root.moved(Qt.point(root.x, root.y)) } - } - // Selection border - Rectangle { - anchors.fill: parent - anchors.margins: -border.width - visible: root.selected - border.width: 2.5 - border.color: activePalette.highlight - opacity: 0.9 - color: "transparent" - } - - Rectangle { - id: background - anchors.fill: parent - color: activePalette.base - layer.enabled: true - layer.effect: DropShadow { radius: 2; color: shadowColor } - } + // Selection border + Rectangle { + anchors.fill: parent + anchors.margins: -border.width + visible: root.selected || root.hovered + border.width: 2.5 + border.color: root.selected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5) + opacity: 0.9 + color: "transparent" + } - Column { - id: body - width: parent.width + Rectangle { + id: background + anchors.fill: parent + color: activePalette.base + layer.enabled: true + layer.effect: DropShadow { radius: 2; color: shadowColor } + } - Label { + Column { + id: body width: parent.width - horizontalAlignment: Text.AlignHCenter - padding: 4 - text: node.label - color: "#EEE" - font.pointSize: 8 - background: Rectangle { - color: root.baseColor + + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + padding: 4 + text: node.label + color: "#EEE" + font.pointSize: 8 + background: Rectangle { + color: root.baseColor + } } - } - // Node Chunks - NodeChunks { - defaultColor: Qt.darker(baseColor, 1.3) - implicitHeight: 3 - width: parent.width - model: node.chunks - } + // Node Chunks + NodeChunks { + defaultColor: Qt.darker(baseColor, 1.3) + implicitHeight: 3 + width: parent.width + model: node.chunks + } - Item { width: 1; height: 2} - - Item { - width: parent.width + 6 - height: childrenRect.height - anchors.horizontalCenter: parent.horizontalCenter - - Column { - id: inputs - width: parent.width / 2 - spacing: 1 - Repeater { - model: node.attributes - delegate: Loader { - active: !object.isOutput && object.type == "File" - || (object.type == "ListAttribute" && object.desc.elementDesc.type == "File") // TODO: review this - width: inputs.width - - sourceComponent: AttributePin { - id: inPin - nodeItem: root - attribute: object - readOnly: root.readOnly - Component.onCompleted: attributePinCreated(attribute, inPin) - Component.onDestruction: attributePinDeleted(attribute, inPin) - onPressed: root.pressed(mouse) - onChildPinCreated: attributePinCreated(childAttribute, inPin) - onChildPinDeleted: attributePinDeleted(childAttribute, inPin) + Item { width: 1; height: 2} + + Item { + width: parent.width + 6 + height: childrenRect.height + anchors.horizontalCenter: parent.horizontalCenter + + Column { + id: inputs + width: parent.width / 2 + spacing: 1 + Repeater { + model: node.attributes + delegate: Loader { + active: !object.isOutput && object.type == "File" + || (object.type == "ListAttribute" && object.desc.elementDesc.type == "File") // TODO: review this + width: inputs.width + + sourceComponent: AttributePin { + id: inPin + nodeItem: root + attribute: object + readOnly: root.readOnly + Component.onCompleted: attributePinCreated(attribute, inPin) + Component.onDestruction: attributePinDeleted(attribute, inPin) + onPressed: root.pressed(mouse) + onChildPinCreated: attributePinCreated(childAttribute, inPin) + onChildPinDeleted: attributePinDeleted(childAttribute, inPin) + } } } } - } - Column { - id: outputs - width: parent.width / 2 - anchors.right: parent.right - spacing: 1 - Repeater { - model: node.attributes - - delegate: Loader { - active: object.isOutput - anchors.right: parent.right - width: outputs.width - - sourceComponent: AttributePin { - id: outPin - nodeItem: root - attribute: object - readOnly: root.readOnly - onPressed: root.pressed(mouse) - Component.onCompleted: attributePinCreated(object, outPin) - Component.onDestruction: attributePinDeleted(attribute, outPin) + Column { + id: outputs + width: parent.width / 2 + anchors.right: parent.right + spacing: 1 + Repeater { + model: node.attributes + + delegate: Loader { + active: object.isOutput + anchors.right: parent.right + width: outputs.width + + sourceComponent: AttributePin { + id: outPin + nodeItem: root + attribute: object + readOnly: root.readOnly + onPressed: root.pressed(mouse) + Component.onCompleted: attributePinCreated(object, outPin) + Component.onDestruction: attributePinDeleted(attribute, outPin) + } } } } } + Item { width: 1; height: 2} } - Item { width: 1; height: 2} - } - // CompatibilityBadge icon for CompatibilityNodes - Loader { - active: root.isCompatibilityNode - anchors { - right: parent.right - top: parent.top - margins: -4 - } - sourceComponent: CompatibilityBadge { - sourceComponent: iconDelegate - canUpgrade: root.node.canUpgrade - issueDetails: root.node.issueDetails + // CompatibilityBadge icon for CompatibilityNodes + Loader { + active: root.isCompatibilityNode + anchors { + right: parent.right + top: parent.top + margins: -4 + } + sourceComponent: CompatibilityBadge { + sourceComponent: iconDelegate + canUpgrade: root.node.canUpgrade + issueDetails: root.node.issueDetails + } } } } From 2f307c16fb6b23c4cb69db6a65588143e33a0ace Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 6 Dec 2018 19:35:26 +0100 Subject: [PATCH 045/293] [ui] Viewer3D: synchronize media list and graph hover/selection * ease bidirectional navigation between 3D media list and graph * use same mechanism to indicate selected/hovered elements * remove 'frame' button (space gain + action available on double click) --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 128 ++++++++++++++++------- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 3 +- meshroom/ui/qml/WorkspaceView.qml | 2 + 3 files changed, 92 insertions(+), 41 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index cd28b7bdb3..93cb1bad52 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -19,6 +19,7 @@ FloatingPane { property Grid3D grid: null property MediaLibrary mediaLibrary property Camera camera + property var uigraph: null signal mediaFocusRequest(var index) signal mediaRemoveRequest(var index) @@ -128,6 +129,13 @@ FloatingPane { } } + currentIndex: -1 + + Connections { + target: uigraph + onSelectedNodeChanged: mediaListView.currentIndex = -1 + } + Connections { target: mediaLibrary onLoadRequest: { @@ -136,6 +144,7 @@ FloatingPane { } delegate: RowLayout { + id: mediaDelegate // add mediaLibrary.count in the binding to ensure 'entity' // is re-evaluated when mediaLibrary delegates are modified property bool loading: model.status === SceneLoader.Loading @@ -145,12 +154,37 @@ FloatingPane { property string src: model.source onSrcChanged: focusAnim.restart() - RowLayout { - Layout.alignment: Qt.AlignTop - spacing: 0 + property bool hovered: model.attribute ? uigraph.hoveredNode === model.attribute.node : mouseArea.containsMouse + property bool isSelectedNode: model.attribute ? uigraph.selectedNode === model.attribute.node : false + + function updateCurrentIndex() { + if(isSelectedNode) { mediaListView.currentIndex = index } + } + + onIsSelectedNodeChanged: updateCurrentIndex() - MaterialToolButton { - text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off + Connections { + target: mediaListView + onCountChanged: mediaDelegate.updateCurrentIndex() + } + + // Current/selected element indicator + Rectangle { + Layout.fillHeight: true + width: 2 + color: { + if(mediaListView.currentIndex == index || mediaDelegate.isSelectedNode) + return label.palette.highlight; + if(mediaDelegate.hovered) + return Qt.darker(label.palette.highlight, 1.5); + return "transparent"; + } + } + + // Media visibility/loading control + MaterialToolButton { + Layout.alignment: Qt.AlignTop + text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off font.pointSize: 10 ToolTip.text: model.visible ? "Hide" : "Show" flat: true @@ -173,47 +207,42 @@ FloatingPane { modifiers = mouse.modifiers; mouse.accepted = false; } - } - } - MaterialToolButton { - text: MaterialIcons.filter_center_focus - font.pointSize: 10 - ToolTip.text: "Frame" - onClicked: camera.viewEntity(mediaLibrary.entityAt(index)) - flat: true } } - ColumnLayout { + // Media label and info + Item { + implicitHeight: childrenRect.height Layout.fillWidth: true - spacing: 1 Layout.alignment: Qt.AlignTop - - Label { - id: label - leftPadding: 0 - rightPadding: 0 - topPadding: 3 - bottomPadding: topPadding - Layout.fillWidth: true - text: model.label - elide: Text.ElideMiddle - background: Rectangle { - Connections { - target: mediaLibrary - onLoadRequest: if(idx == index) focusAnim.restart() - } - ColorAnimation on color { - id: focusAnim - from: label.palette.highlight - to: "transparent" - duration: 2000 - } - MouseArea { - anchors.fill: parent - onDoubleClicked: camera.viewEntity(mediaLibrary.entityAt(index)) + ColumnLayout { + id: centralLayout + width: parent.width + spacing: 1 + + Label { + id: label + Layout.fillWidth: true + leftPadding: 0 + rightPadding: 0 + topPadding: 3 + bottomPadding: topPadding + text: model.label + opacity: model.valid ? 1.0 : 0.6 + elide: Text.ElideMiddle + font.weight: mediaListView.currentIndex == index ? Font.DemiBold : Font.Normal + background: Rectangle { + Connections { + target: mediaLibrary + onLoadRequest: if(idx == index) focusAnim.restart() + } + ColorAnimation on color { + id: focusAnim + from: label.palette.highlight + to: "transparent" + duration: 2000 + } } - } } Item { Layout.fillWidth: true @@ -230,6 +259,25 @@ FloatingPane { Label { visible: model.textureCount; text: model.textureCount } } } + } + MouseArea { + id: mouseArea + anchors.fill: centralLayout + hoverEnabled: true + onEntered: { if(model.attribute) uigraph.hoveredNode = model.attribute.node } + onExited: { if(model.attribute) uigraph.hoveredNode = null } + onClicked: { + if(model.attribute) + uigraph.selectedNode = model.attribute.node; + else + uigraph.selectedNode = null; + mediaListView.currentIndex = index; + } + onDoubleClicked: { + model.visible = true; + camera.viewEntity(mediaLibrary.entityAt(index)); + } + } } // Media unavailability indicator diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index fcbd5e3d84..9a1c47636a 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -19,6 +19,7 @@ FocusScope { property int renderMode: 2 property alias library: mediaLibrary + property alias inspector: inspector3d // functions function resetCameraCenter() { @@ -218,7 +219,7 @@ FocusScope { Item { Layout.fillWidth: true; Layout.minimumWidth: parent.width * 0.5 } Inspector3D { - id: inspector + id: inspector3d width: 220 Layout.minimumWidth: 5 diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 63e2a3a61b..692f12efd7 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -124,6 +124,8 @@ Item { readonly property int outputMediaIndex: library.find(outputAttribute) anchors.fill: parent + inspector.uigraph: reconstruction + DropArea { anchors.fill: parent keys: ["text/uri-list"] From 1c070c7fdb035d852709f69f821709b16342f99d Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 14:53:45 +0100 Subject: [PATCH 046/293] [ui] Controls: add Group custom control --- meshroom/ui/qml/Controls/Group.qml | 47 ++++++++++++++++++++++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + 2 files changed, 48 insertions(+) create mode 100644 meshroom/ui/qml/Controls/Group.qml diff --git a/meshroom/ui/qml/Controls/Group.qml b/meshroom/ui/qml/Controls/Group.qml new file mode 100644 index 0000000000..452c9cddf2 --- /dev/null +++ b/meshroom/ui/qml/Controls/Group.qml @@ -0,0 +1,47 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 + +/** + * A custom GroupBox with predefined header. + */ +GroupBox { + id: root + + title: "" + property int sidePadding: 6 + property alias labelBackground: labelBg + property alias toolBarContent: toolBar.data + + padding: 2 + leftPadding: sidePadding + rightPadding: sidePadding + topPadding: label.height + padding + background: Item {} + + label: Pane { + background: Rectangle { + id: labelBg + color: palette.base + opacity: 0.8 + } + padding: 2 + width: root.width + RowLayout { + width: parent.width + Label { + text: root.title + Layout.fillWidth: true + elide: Text.ElideRight + padding: 3 + font.bold: true + font.pointSize: 8 + } + RowLayout { + id: toolBar + height: parent.height + } + } + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 17d5225525..354a1604e9 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -1,4 +1,5 @@ module Controls FloatingPane 1.0 FloatingPane.qml +Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml From fc857d5dc8475855ae508d4ac09276bc0b5b69d7 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 14:55:12 +0100 Subject: [PATCH 047/293] [ui] Viewer3D: add 'cameraScale' parameter to drive camera locator size --- meshroom/ui/qml/Viewer3D/MediaLoader.qml | 3 ++- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index 051b7f1dcd..59e049aba1 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -80,12 +80,13 @@ import Utils 1.0 id: abcLoaderEntityComponent MediaLoaderEntity { id: abcLoaderEntity + enabled: root.enabled Component.onCompleted: { var obj = Viewer3DSettings.abcLoaderComp.createObject(abcLoaderEntity, { 'source': source, 'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }), - 'locatorScale': 0.3 + 'locatorScale': Qt.binding(function() { return Viewer3DSettings.cameraScale }) }); obj.statusChanged.connect(function() { diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 95031e5fc0..55e487582b 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -24,6 +24,7 @@ Item { property real pointSize: 1.5 // Whether point size is fixed or view dependent property bool fixedPointSize: false + property real cameraScale: 0.3 // Helpers display property bool displayGrid: true property bool displayLocator: true From 877a852296ee8ab6de0a9f4a1342131698595ae6 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 10 Dec 2018 12:08:45 +0100 Subject: [PATCH 048/293] [node] CameraInit: improve "locked" param description --- meshroom/nodes/aliceVision/CameraInit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 63c30c328c..48f59db445 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -40,7 +40,9 @@ label="Distortion Params", description="Distortion Parameters", ), - desc.BoolParam(name='locked', label='Locked', description='Whether Intrinsic is locked.', value=False, uid=[0]), + desc.BoolParam(name='locked', label='Locked', + description='If the camera has been calibrated, the internal camera parameters (intrinsics) can be locked. It should improve robustness and speedup the reconstruction.', + value=False, uid=[0]), ] From 272cd24fb9933b57dcaee0908eb0f0acc953268a Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 15:24:08 +0100 Subject: [PATCH 049/293] [ui] Viewer3D: Inspector3D cleanup * Define 2 groups: "Settings" and "Scene" * Settings * add camera scale slider control * move rendering modes back to an independent overlay outside the inspector * Scene * add button to control visibility of media info --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 439 ++++++++++++----------- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 22 +- 2 files changed, 245 insertions(+), 216 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 93cb1bad52..c518f85d61 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -24,167 +24,174 @@ FloatingPane { signal mediaFocusRequest(var index) signal mediaRemoveRequest(var index) + padding: 0 + MouseArea { anchors.fill: parent; onWheel: wheel.accepted = true } ColumnLayout { - width: parent.width - height: parent.height - spacing: 10 + anchors.fill: parent + spacing: 4 - Label { text: "RENDER"; font.bold: true; font.pointSize: 8 } - Flow { + Group { Layout.fillWidth: true - Repeater { - model: Viewer3DSettings.renderModes - - delegate: MaterialToolButton { - text: modelData["icon"] - ToolTip.text: modelData["name"] + " (" + (index+1) + ")" - onClicked: Viewer3DSettings.renderMode = index - checked: Viewer3DSettings.renderMode === index - } - } - } + title: "SETTINGS" - Label { text: "SCENE"; font.bold: true; font.pointSize: 8 } + GridLayout { + width: parent.width + columns: 3 + columnSpacing: 6 + rowSpacing: 3 - GridLayout { - id: controlsLayout - Layout.fillWidth: true - columns: 3 - columnSpacing: 6 - Flow { - Layout.columnSpan: 3 - Layout.fillWidth: true - spacing: 0 - CheckBox { - text: "Grid" - checked: Viewer3DSettings.displayGrid - onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid - } - CheckBox { - text: "Locator" - checked: Viewer3DSettings.displayLocator - onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator + // Rotation Controls + Label { + font.family: MaterialIcons.fontFamily + text: MaterialIcons.rotation3D + font.pointSize: 14 + Layout.rowSpan: 3 + Layout.alignment: Qt.AlignHCenter } - } - // Rotation Controls - Label { - font.family: MaterialIcons.fontFamily - text: MaterialIcons.rotation3D - font.pointSize: 14 - Layout.rowSpan: 3 - } + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value} + Label { text: "X" } - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value} - Label { text: "X" } + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value} + Label { text: "Y" } - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value} - Label { text: "Y" } + Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value } + Label { text: "Z" } - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value } - Label { text: "Z" } + Label { text: "Points"; padding: 2 } + RowLayout { + Layout.columnSpan: 2 + Slider { + Layout.fillWidth: true; from: 0.1; to: 10; stepSize: 0.1 + value: Viewer3DSettings.pointSize + onValueChanged: Viewer3DSettings.pointSize = value + } + MaterialToolButton { + text: MaterialIcons.center_focus_strong + ToolTip.text: "Fixed Point Size" + font.pointSize: 10 + padding: 3 + checked: Viewer3DSettings.fixedPointSize + onClicked: Viewer3DSettings.fixedPointSize = !Viewer3DSettings.fixedPointSize + } - Label { text: "Points" } - RowLayout { - Layout.columnSpan: 2 + } + Label { text: "Cameras"; padding: 2 } Slider { - Layout.fillWidth: true; from: 1; to: 20;stepSize: 0.1 - value: Viewer3DSettings.pointSize - onValueChanged: Viewer3DSettings.pointSize = value + Layout.columnSpan: 2 + value: Viewer3DSettings.cameraScale + from: 0 + to: 2 + stepSize: 0.01 + Layout.fillWidth: true + padding: 0 + onMoved: Viewer3DSettings.cameraScale = value } - CheckBox { - text: "Fixed"; - checked: Viewer3DSettings.fixedPointSize - onClicked: Viewer3DSettings.fixedPointSize = !Viewer3DSettings.fixedPointSize + Flow { + Layout.columnSpan: 3 + Layout.fillWidth: true + spacing: 2 + CheckBox { + text: "Grid" + padding: 2 + checked: Viewer3DSettings.displayGrid + onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid + } + CheckBox { + text: "Locator" + padding: 2 + checked: Viewer3DSettings.displayLocator + onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator + } } } - } - Label { text: "MEDIA"; font.bold: true; font.pointSize: 8 } - - ListView { - id: mediaListView - Layout.fillHeight: true + // 3D Scene content + Group { + title: "SCENE" Layout.fillWidth: true - clip: true - model: mediaLibrary.model - spacing: 2 - //section.property: "section" + Layout.fillHeight: true + sidePadding: 0 + + toolBarContent: MaterialToolButton { + id: infoButton + ToolTip.text: "Media Info" + text: MaterialIcons.info_outline + font.pointSize: 10 + implicitHeight: parent.height + checkable: true + checked: true + padding: 0 + } - ScrollBar.vertical: ScrollBar { id: scrollBar } + ListView { + id: mediaListView + anchors.fill: parent + clip: true + model: mediaLibrary.model + spacing: 4 - section.delegate: Pane { - width: parent.width - padding: 1 - background: null + ScrollBar.vertical: ScrollBar { id: scrollBar } - Label { - width: parent.width - padding: 4 - background: Rectangle { color: Qt.darker(parent.palette.base, 1.15) } - text: section - } - } + currentIndex: -1 - currentIndex: -1 - - Connections { - target: uigraph - onSelectedNodeChanged: mediaListView.currentIndex = -1 - } + Connections { + target: uigraph + onSelectedNodeChanged: mediaListView.currentIndex = -1 + } - Connections { - target: mediaLibrary - onLoadRequest: { - mediaListView.positionViewAtIndex(idx, ListView.Visible); + Connections { + target: mediaLibrary + onLoadRequest: { + mediaListView.positionViewAtIndex(idx, ListView.Visible); + } } - } - delegate: RowLayout { - id: mediaDelegate - // add mediaLibrary.count in the binding to ensure 'entity' - // is re-evaluated when mediaLibrary delegates are modified - property bool loading: model.status === SceneLoader.Loading - spacing: 2 - width: parent.width - scrollBar.width / 2 + delegate: RowLayout { + id: mediaDelegate + // add mediaLibrary.count in the binding to ensure 'entity' + // is re-evaluated when mediaLibrary delegates are modified + property bool loading: model.status === SceneLoader.Loading + spacing: 2 + width: parent.width - scrollBar.width / 2 - property string src: model.source - onSrcChanged: focusAnim.restart() + property string src: model.source + onSrcChanged: focusAnim.restart() - property bool hovered: model.attribute ? uigraph.hoveredNode === model.attribute.node : mouseArea.containsMouse - property bool isSelectedNode: model.attribute ? uigraph.selectedNode === model.attribute.node : false + property bool hovered: model.attribute ? uigraph.hoveredNode === model.attribute.node : mouseArea.containsMouse + property bool isSelectedNode: model.attribute ? uigraph.selectedNode === model.attribute.node : false - function updateCurrentIndex() { - if(isSelectedNode) { mediaListView.currentIndex = index } - } + function updateCurrentIndex() { + if(isSelectedNode) { mediaListView.currentIndex = index } + } - onIsSelectedNodeChanged: updateCurrentIndex() + onIsSelectedNodeChanged: updateCurrentIndex() - Connections { - target: mediaListView - onCountChanged: mediaDelegate.updateCurrentIndex() - } + Connections { + target: mediaListView + onCountChanged: mediaDelegate.updateCurrentIndex() + } - // Current/selected element indicator - Rectangle { - Layout.fillHeight: true - width: 2 - color: { - if(mediaListView.currentIndex == index || mediaDelegate.isSelectedNode) - return label.palette.highlight; - if(mediaDelegate.hovered) - return Qt.darker(label.palette.highlight, 1.5); - return "transparent"; + // Current/selected element indicator + Rectangle { + Layout.fillHeight: true + width: 2 + color: { + if(mediaListView.currentIndex == index || mediaDelegate.isSelectedNode) + return label.palette.highlight; + if(mediaDelegate.hovered) + return Qt.darker(label.palette.highlight, 1.5); + return "transparent"; + } } - } - // Media visibility/loading control - MaterialToolButton { - Layout.alignment: Qt.AlignTop - text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off + // Media visibility/loading control + MaterialToolButton { + Layout.alignment: Qt.AlignTop + text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off font.pointSize: 10 ToolTip.text: model.visible ? "Hide" : "Show" flat: true @@ -207,107 +214,109 @@ FloatingPane { modifiers = mouse.modifiers; mouse.accepted = false; } + } } - } - // Media label and info - Item { - implicitHeight: childrenRect.height - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - ColumnLayout { - id: centralLayout - width: parent.width - spacing: 1 - - Label { - id: label - Layout.fillWidth: true - leftPadding: 0 - rightPadding: 0 - topPadding: 3 - bottomPadding: topPadding - text: model.label - opacity: model.valid ? 1.0 : 0.6 - elide: Text.ElideMiddle - font.weight: mediaListView.currentIndex == index ? Font.DemiBold : Font.Normal - background: Rectangle { - Connections { - target: mediaLibrary - onLoadRequest: if(idx == index) focusAnim.restart() + // Media label and info + Item { + implicitHeight: childrenRect.height + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + ColumnLayout { + id: centralLayout + width: parent.width + spacing: 1 + + Label { + id: label + Layout.fillWidth: true + leftPadding: 0 + rightPadding: 0 + topPadding: 3 + bottomPadding: topPadding + text: model.label + opacity: model.valid ? 1.0 : 0.6 + elide: Text.ElideMiddle + font.weight: mediaListView.currentIndex == index ? Font.DemiBold : Font.Normal + background: Rectangle { + Connections { + target: mediaLibrary + onLoadRequest: if(idx == index) focusAnim.restart() + } + ColorAnimation on color { + id: focusAnim + from: label.palette.highlight + to: "transparent" + duration: 2000 + } } - ColorAnimation on color { - id: focusAnim - from: label.palette.highlight - to: "transparent" - duration: 2000 + } + Item { + visible: infoButton.checked + Layout.fillWidth: true + implicitHeight: childrenRect.height + RowLayout { + visible: model.status === SceneLoader.Ready + MaterialLabel { visible: model.vertexCount; text: MaterialIcons.grain } + Label { visible: model.vertexCount; text: Format.intToString(model.vertexCount) } + MaterialLabel { visible: model.faceCount; text: MaterialIcons.details; rotation: -180 } + Label { visible: model.faceCount; text: Format.intToString(model.faceCount) } + MaterialLabel { visible: model.cameraCount; text: MaterialIcons.videocam } + Label { visible: model.cameraCount; text: model.cameraCount } + MaterialLabel { visible: model.textureCount; text: MaterialIcons.texture } + Label { visible: model.textureCount; text: model.textureCount } } } - } - Item { - Layout.fillWidth: true - implicitHeight: childrenRect.height - RowLayout { - visible: model.status === SceneLoader.Ready - MaterialLabel { visible: model.vertexCount; text: MaterialIcons.grain } - Label { visible: model.vertexCount; text: Format.intToString(model.vertexCount) } - MaterialLabel { visible: model.faceCount; text: MaterialIcons.details; rotation: -180 } - Label { visible: model.faceCount; text: Format.intToString(model.faceCount) } - MaterialLabel { visible: model.cameraCount; text: MaterialIcons.videocam } - Label { visible: model.cameraCount; text: model.cameraCount } - MaterialLabel { visible: model.textureCount; text: MaterialIcons.texture } - Label { visible: model.textureCount; text: model.textureCount } } - } - } - MouseArea { - id: mouseArea - anchors.fill: centralLayout - hoverEnabled: true - onEntered: { if(model.attribute) uigraph.hoveredNode = model.attribute.node } - onExited: { if(model.attribute) uigraph.hoveredNode = null } - onClicked: { - if(model.attribute) - uigraph.selectedNode = model.attribute.node; - else - uigraph.selectedNode = null; - mediaListView.currentIndex = index; - } - onDoubleClicked: { - model.visible = true; - camera.viewEntity(mediaLibrary.entityAt(index)); + MouseArea { + id: mouseArea + anchors.fill: centralLayout + hoverEnabled: true + onEntered: { if(model.attribute) uigraph.hoveredNode = model.attribute.node } + onExited: { if(model.attribute) uigraph.hoveredNode = null } + onClicked: { + if(model.attribute) + uigraph.selectedNode = model.attribute.node; + else + uigraph.selectedNode = null; + mediaListView.currentIndex = index; + } + onDoubleClicked: { + model.visible = true; + camera.viewEntity(mediaLibrary.entityAt(index)); + } } } - } - // Media unavailability indicator - MaterialToolButton { - Layout.alignment: Qt.AlignTop - enabled: false - visible: !model.valid - text: MaterialIcons.no_sim - font.pointSize: 10 - } + // Media unavailability indicator + MaterialToolButton { + Layout.alignment: Qt.AlignTop + enabled: false + visible: !model.valid + text: MaterialIcons.no_sim + font.pointSize: 10 + } - // Remove media from library button - MaterialToolButton { - id: removeButton - Layout.alignment: Qt.AlignTop + // Remove media from library button + MaterialToolButton { + id: removeButton + Layout.alignment: Qt.AlignTop - visible: !loading - text: MaterialIcons.clear - font.pointSize: 10 - ToolTip.text: "Remove" - onClicked: mediaLibrary.remove(index) - } + visible: !loading + text: MaterialIcons.clear + font.pointSize: 10 + ToolTip.text: "Remove" + onClicked: mediaLibrary.remove(index) + } - // Media loading indicator - BusyIndicator { - visible: loading - running: visible - padding: removeButton.padding - implicitHeight: implicitWidth - implicitWidth: removeButton.width + // Media loading indicator + BusyIndicator { + visible: loading + running: visible + padding: removeButton.padding + implicitHeight: implicitWidth + implicitWidth: removeButton.width + } } } } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index 9a1c47636a..cc9ac1e742 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -220,7 +220,7 @@ FocusScope { Inspector3D { id: inspector3d - width: 220 + width: 200 Layout.minimumWidth: 5 camera: mainCamera @@ -229,6 +229,26 @@ FocusScope { } } + // Rendering modes + FloatingPane { + anchors.bottom: parent.bottom + padding: 4 + Row { + Repeater { + model: Viewer3DSettings.renderModes + + delegate: MaterialToolButton { + text: modelData["icon"] + ToolTip.text: modelData["name"] + " (" + (index+1) + ")" + font.pointSize: 11 + onClicked: Viewer3DSettings.renderMode = index + checked: Viewer3DSettings.renderMode === index + checkable: !checked // hack to disabled check on toggle + } + } + } + } + // Menu Menu { id: contextMenu From c3750a33c303d09806b9fe3380a2e371a6900c16 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 10 Dec 2018 13:23:20 +0100 Subject: [PATCH 050/293] [ui] Inspector3D: add media contextual menu Contextual menu to expose additional actions: * open media containing folder * copy media path * advanced manual control over media (un)loading + fix MediaLibrary to avoid binding loop on 'visible' when directly modifying 'request' property --- meshroom/ui/components/filepath.py | 6 ++++++ meshroom/ui/qml/Viewer3D/Inspector3D.qml | 26 ++++++++++++++++++++++- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 10 +++++---- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/components/filepath.py b/meshroom/ui/components/filepath.py index cb6c8a6d08..9e9a047574 100644 --- a/meshroom/ui/components/filepath.py +++ b/meshroom/ui/components/filepath.py @@ -72,3 +72,9 @@ def urlToString(self, url): def stringToUrl(self, path): """ Convert a path (string) to a QUrl using 'QUrl.fromLocalFile' method """ return QUrl.fromLocalFile(path) + + @Slot(str, result=str) + @Slot(QUrl, result=str) + def normpath(self, path): + """ Returns native normalized path """ + return os.path.normpath(self.asStr(path)) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index c518f85d61..81184b5203 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -193,7 +193,7 @@ FloatingPane { Layout.alignment: Qt.AlignTop text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off font.pointSize: 10 - ToolTip.text: model.visible ? "Hide" : "Show" + ToolTip.text: model.visible ? "Hide" : model.requested ? "Show" : model.valid ? "Load and Show" : "Load and Show when Available" flat: true opacity: model.visible ? 1.0 : 0.6 onClicked: { @@ -272,6 +272,7 @@ FloatingPane { id: mouseArea anchors.fill: centralLayout hoverEnabled: true + acceptedButtons: Qt.AllButtons onEntered: { if(model.attribute) uigraph.hoveredNode = model.attribute.node } onExited: { if(model.attribute) uigraph.hoveredNode = null } onClicked: { @@ -279,6 +280,8 @@ FloatingPane { uigraph.selectedNode = model.attribute.node; else uigraph.selectedNode = null; + if(mouse.button == Qt.RightButton) + contextMenu.popup(); mediaListView.currentIndex = index; } onDoubleClicked: { @@ -286,6 +289,27 @@ FloatingPane { camera.viewEntity(mediaLibrary.entityAt(index)); } } + + Menu { + id: contextMenu + MenuItem { + text: "Open Containing Folder" + enabled: model.valid + onTriggered: Qt.openUrlExternally(Filepath.dirname(model.source)) + } + MenuItem { + text: "Copy Path" + // hidden TextEdit to copy to clipboard + TextEdit { id: fullpath; visible: false; text: Filepath.normpath(model.source) } + onTriggered: { fullpath.selectAll(); fullpath.copy(); } + } + MenuSeparator {} + MenuItem { + text: model.requested ? "Unload Media" : "Load Media" + enabled: model.valid + onTriggered: model.requested = !model.requested + } + } } // Media unavailability indicator diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index b06381497c..7f6960ea88 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -194,10 +194,8 @@ Entity { if(attribute) { model.source = rawSource; } - // auto-restore entity if raw source is in cache ... + // auto-restore entity if raw source is in cache model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource); - // ... and update media visibility (useful if media was hidden but loaded back from cache) - model.visible = model.requested; model.valid = Filepath.exists(rawSource) && dependencyReady; } @@ -211,9 +209,13 @@ Entity { remove(index) } - onCurrentSourceChanged: updateModelAndCache() + onCurrentSourceChanged: updateModelAndCache(false) onFinalSourceChanged: { + // update media visibility + // (useful if media was explicitly unloaded or hidden but loaded back from cache) + model.visible = model.requested; + var cachedObject = cache.pop(rawSource); cached = cachedObject !== undefined; if(cached) { From 72cd60fd0aa06c4397f6522d2ae82b3f672a9eef Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 15:31:46 +0100 Subject: [PATCH 051/293] [ui] AlembicLoader: use 'skipHidden' to avoid loading unreconstructed cams --- meshroom/ui/qml/Viewer3D/AlembicLoader.qml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml index 31a02ff1c6..dba8096188 100644 --- a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml +++ b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml @@ -1,4 +1,4 @@ -import AlembicEntity 1.0 +import AlembicEntity 2.0 import QtQuick 2.9 import Qt3D.Core 2.1 import Qt3D.Render 2.1 @@ -11,6 +11,9 @@ import Qt3D.Extras 2.1 AlembicEntity { id: root + // filter out non-reconstructed cameras + skipHidden: true + signal cameraSelected(var viewId) function spawnCameraSelectors() { @@ -23,11 +26,6 @@ AlembicEntity { var viewId = cam.userProperties["mvg_viewId"]; if(viewId === undefined) continue; - // filter out non-reconstructed cameras - if(cam.parent.parent.objectName === "mvgCamerasUndefined") { - cam.enabled = false; - continue; - } camSelectionComponent.createObject(cam, {"viewId": viewId}); validCameras++; } From b56cf9e7cf24609922ab10352fd9772b6a5db5e9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 23 Nov 2018 22:39:46 +0100 Subject: [PATCH 052/293] [ui] Viewer3D: tweak initial camera position and Grid3D material * centralize initial camera parameters + move camera closer to the scene center * add motion smoothing when camera position is set * improve grid material aspect --- meshroom/ui/qml/Viewer3D/Grid3D.qml | 5 ++++- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 26 ++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Grid3D.qml b/meshroom/ui/qml/Viewer3D/Grid3D.qml index 4108765f1a..bd852a8e0d 100644 --- a/meshroom/ui/qml/Viewer3D/Grid3D.qml +++ b/meshroom/ui/qml/Viewer3D/Grid3D.qml @@ -52,7 +52,10 @@ Entity { } }, PhongMaterial { - ambient: Qt.rgba(0.4, 0.4, 0.4, 1) + ambient: "#FFF" + diffuse: "#222" + specular: diffuse + shininess: 0 } ] } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index cc9ac1e742..179575e126 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -21,16 +21,15 @@ FocusScope { property alias library: mediaLibrary property alias inspector: inspector3d - // functions - function resetCameraCenter() { - mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0); - mainCamera.upVector = Qt.vector3d(0.0, 1.0, 0.0); - } + readonly property vector3d defaultCamPosition: Qt.vector3d(12.0, 10.0, -12.0) + readonly property vector3d defaultCamUpVector: Qt.vector3d(0.0, 1.0, 0.0) + readonly property vector3d defaultCamViewCenter: Qt.vector3d(0.0, 0.0, 0.0) + // functions function resetCameraPosition() { - mainCamera.position = Qt.vector3d(28.0, 21.0, 28.0); - mainCamera.upVector = Qt.vector3d(0.0, 1.0, 0.0); - mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0); + mainCamera.position = defaultCamPosition; + mainCamera.upVector = defaultCamUpVector; + mainCamera.viewCenter = defaultCamViewCenter; } function load(filepath) { @@ -59,7 +58,6 @@ FocusScope { Keys.onPressed: { if (event.key == Qt.Key_F) { - resetCameraCenter(); resetCameraPosition(); } else if(Qt.Key_1 <= event.key && event.key <= Qt.Key_3) @@ -80,14 +78,18 @@ FocusScope { fieldOfView: 45 nearPlane : 0.01 farPlane : 10000.0 - position: Qt.vector3d(28.0, 21.0, 28.0) - upVector: Qt.vector3d(0.0, 1.0, 0.0) - viewCenter: Qt.vector3d(0.0, 0.0, 0.0) + position: defaultCamPosition + upVector: defaultCamUpVector + viewCenter: defaultCamViewCenter aspectRatio: width/height Behavior on viewCenter { Vector3dAnimation { duration: 250 } } + Behavior on position { + Vector3dAnimation { duration: 250 } + } + // Scene light, attached to the camera Entity { components: [ From 66943922d82562cce68946ded84de8a0135bc287 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 15:50:02 +0100 Subject: [PATCH 053/293] [ui] Viewer3D: clean "Load Model" handling move related properties to the button responsible for loading the end node when a result is available --- meshroom/ui/qml/WorkspaceView.qml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 692f12efd7..056165fd95 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -119,9 +119,6 @@ Item { Viewer3D { id: viewer3D - readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null - readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS" - readonly property int outputMediaIndex: library.find(outputAttribute) anchors.fill: parent inspector.uigraph: reconstruction @@ -137,11 +134,15 @@ Item { // Load reconstructed model Button { + readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null + readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS" + readonly property int outputMediaIndex: viewer3D.library.find(outputAttribute) + text: "Load Model" anchors.bottom: parent.bottom anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter - visible: viewer3D.outputReady && viewer3D.outputMediaIndex == -1 + visible: outputReady && outputMediaIndex == -1 onClicked: viewAttribute(_reconstruction.endNode.attribute("outputMesh")) } } From 6836f7fd9397c1690df592b03f54b11cf6098405 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 18:10:21 +0100 Subject: [PATCH 054/293] [ui] MaterialSwitcher: use Scene3DHelper to add/remove entity components --- meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml | 50 ++----------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml index 7f0b85a2a9..a93abeba40 100644 --- a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml +++ b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml @@ -28,63 +28,24 @@ Entity { onMaterialChanged: { // remove previous material(s) removeComponentsByType(parent, "Material") - addComponent(root.parent, material) + Scene3DHelper.addComponent(root.parent, material) } } - function printComponents(entity) - { - console.log("Components of Entity '" + entity + "'") - for(var i=0; i < entity.components.length; ++i) - { - console.log(" -- [" + i + "]: " + entity.components[i]) - } - } - - function addComponent(entity, component) - { - if(!entity) - return - var comps = []; - comps.push(component); - - for(var i=0; i < entity.components.length; ++i) - { - comps.push(entity.components[i]); - } - entity.components = comps; - } - function removeComponentsByType(entity, type) { if(!entity) return - var comps = []; for(var i=0; i < entity.components.length; ++i) { - if(entity.components[i].toString().indexOf(type) == -1) + if(entity.components[i].toString().indexOf(type) != -1) { - comps.push(entity.components[i]); + //entity.components[i].enabled = false; + Scene3DHelper.removeComponent(entity, entity.components[i]); } } - entity.components = comps; } - function removeComponent(entity, component) - { - if(!entity) - return - var comps = []; - - for(var i=0; i < entity.components.length; ++i) - { - if(entity.components[i] == component) - { - comps.push(entity.components[i]); - } - } - entity.components = comps; - } StateGroup { id: modeState @@ -112,7 +73,6 @@ Entity { DiffuseSpecularMaterial { id: solid - parent: root.parent objectName: "SolidMaterial" ambient: root.ambient shininess: root.shininess @@ -122,7 +82,6 @@ Entity { DiffuseSpecularMaterial { id: textured - parent: root.parent objectName: "TexturedMaterial" ambient: root.ambient shininess: root.shininess @@ -136,7 +95,6 @@ Entity { WireframeMaterial { id: wireframe - parent: root.parent objectName: "WireframeMaterial" effect: WireframeEffect {} ambient: root.ambient From 87d5a62846eb5b857835507a0937db2b3de38eca Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 18:22:57 +0100 Subject: [PATCH 055/293] [ui] Viewer3D: new Trackball camera manipulator --- meshroom/ui/components/scene3D.py | 6 ++ .../qml/Viewer3D/DefaultCameraController.qml | 60 ++++++++++++++-- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 6 ++ meshroom/ui/qml/Viewer3D/TrackballGizmo.qml | 70 +++++++++++++++++++ meshroom/ui/qml/Viewer3D/Viewer3D.qml | 27 +++---- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 3 +- 6 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 meshroom/ui/qml/Viewer3D/TrackballGizmo.qml diff --git a/meshroom/ui/components/scene3D.py b/meshroom/ui/components/scene3D.py index 8a46abc44b..cb4b2a28b7 100644 --- a/meshroom/ui/components/scene3D.py +++ b/meshroom/ui/components/scene3D.py @@ -1,6 +1,7 @@ from PySide2.QtCore import QObject, Slot from PySide2.Qt3DCore import Qt3DCore from PySide2.Qt3DRender import Qt3DRender +from PySide2.QtGui import QVector3D, QQuaternion class Scene3DHelper(QObject): @@ -40,3 +41,8 @@ def faceCount(self, entity): for geo in entity.findChildren(Qt3DRender.QGeometry): count += sum([attr.count() for attr in geo.attributes() if attr.name() == "vertexPosition"]) return count / 3 + + @Slot(QQuaternion, QVector3D, result=QVector3D) + def rotatedVector(self, quaternion, vector): + """ Returns the rotation of 'vector' with 'quaternion'. """ + return quaternion.rotatedVector(vector) diff --git a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml index 7f12596ece..792b901cdc 100644 --- a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml +++ b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml @@ -15,6 +15,9 @@ Entity { property bool moving: pressed || (actionAlt.active && keyboardHandler._pressed) property alias focus: keyboardHandler.focus readonly property bool pickingActive: actionControl.active && keyboardHandler._pressed + property real rotationSpeed: 2.0 + property size windowSize + property real trackballSize: 1.0 readonly property alias pressed: mouseHandler._pressed signal mousePressed(var mouse) @@ -29,10 +32,20 @@ Entity { MouseHandler { id: mouseHandler property bool _pressed + property point lastPosition + property point currentPosition sourceDevice: mouseSourceDevice - onPressed: { _pressed = true; mousePressed(mouse) } - onReleased: { _pressed = false; mouseReleased(mouse) } + onPressed: { + _pressed = true; + currentPosition = lastPosition = Qt.point(mouse.x, mouse.y); + mousePressed(mouse); + } + onReleased: { + _pressed = false; + mouseReleased(mouse); + } onClicked: mouseClicked(mouse) + onPositionChanged: { currentPosition = Qt.point(mouse.x, mouse.y) } onDoubleClicked: mouseDoubleClicked(mouse) onWheel: { var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.2; @@ -135,6 +148,36 @@ Entity { ] } + // Based on the C++ version from https://github.com/cjmdaixi/Qt3DTrackball + function projectToTrackball(screenCoords) + { + var sx = screenCoords.x, sy = windowSize.height - screenCoords.y; + var p2d = Qt.vector2d(sx / windowSize.width - 0.5, sy / windowSize.height - 0.5); + var z = 0.0; + var r2 = trackballSize * trackballSize; + var lengthSquared = p2d.length() * p2d.length(); + if(lengthSquared <= r2 * 0.5) + z = Math.sqrt(r2 - lengthSquared); + else + z = r2 * 0.5 / p2d.length(); + return Qt.vector3d(p2d.x, p2d.y, z); + } + + function clamp(x) + { + return Math.max(-1, Math.min(x, 1)); + } + + function createRotation(firstPoint, nextPoint) + { + var lastPos3D = projectToTrackball(firstPoint).normalized(); + var currentPos3D = projectToTrackball(nextPoint).normalized(); + var obj = {}; + obj.angle = Math.acos(clamp(currentPos3D.dotProduct(lastPos3D))); + obj.dir = currentPos3D.crossProduct(lastPos3D); + return obj; + } + components: [ FrameAction { onTriggered: { @@ -145,11 +188,14 @@ Entity { root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt)) return; } - if(actionLMB.active) { // rotate - var rx = -axisMX.value; - var ry = -axisMY.value; - root.camera.panAboutViewCenter(root.panSpeed * rx * dt, Qt.vector3d(0,1,0)) - root.camera.tiltAboutViewCenter(root.tiltSpeed * ry * dt) + if(actionLMB.active){ // trackball rotation + var res = createRotation(mouseHandler.lastPosition, mouseHandler.currentPosition); + var transform = root.camera.components[1]; // transform is camera first component + var currentRotation = transform.rotation; + var rotatedAxis = Scene3DHelper.rotatedVector(transform.rotation, res.dir); + res.angle *= rotationSpeed * dt; + root.camera.rotateAboutViewCenter(transform.fromAxisAndAngle(rotatedAxis, res.angle * Math.PI * 180)); + mouseHandler.lastPosition = mouseHandler.currentPosition; return; } if(actionAlt.active && actionRMB.active) { // zoom with alt + RMD diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 81184b5203..1c03b64155 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -99,6 +99,12 @@ FloatingPane { checked: Viewer3DSettings.displayGrid onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid } + CheckBox { + text: "Gizmo" + padding: 2 + checked: Viewer3DSettings.displayGizmo + onClicked: Viewer3DSettings.displayGizmo = !Viewer3DSettings.displayGizmo + } CheckBox { text: "Locator" padding: 2 diff --git a/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml b/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml new file mode 100644 index 0000000000..16ccefa754 --- /dev/null +++ b/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml @@ -0,0 +1,70 @@ +import Qt3D.Core 2.0 +import Qt3D.Render 2.9 +import Qt3D.Input 2.0 +import Qt3D.Extras 2.10 +import QtQuick 2.9 + +Entity { + id: root + property real beamRadius: 0.0075 + property real beamLength: 1 + property int slices: 10 + property int rings: 50 + property color colorX: "#F44336" + property color colorY: "#8BC34A" + property color colorZ: "#03A9F4" + property real alpha: 1.0 + property Transform transform: Transform {} + + components: [transform] + + Behavior on alpha { + PropertyAnimation { duration: 100 } + } + + SystemPalette { id: sysPalette } + + // Gizmo center + Entity { + components: [ + SphereMesh { radius: beamRadius * 4}, + PhongMaterial { + ambient: "#FFF" + shininess: 0.2 + diffuse: sysPalette.highlight + specular: sysPalette.highlight + } + ] + } + + // X, Y, Z rings + NodeInstantiator { + model: 3 + Entity { + components: [ + TorusMesh { + radius: root.beamLength + minorRadius: root.beamRadius + slices: root.slices + rings: root.rings + }, + DiffuseSpecularMaterial { + ambient: { + switch(index) { + case 0: return colorX; + case 1: return colorY; + case 2: return colorZ; + } + } + shininess: 0 + diffuse: Qt.rgba(0.6, 0.6, 0.6, root.alpha) + }, + + Transform { + rotationY: index == 0 ? 90 : 0 + rotationX: index == 1 ? 90 : 0 + } + ] + } + } +} diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index 179575e126..da0c452e13 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -100,26 +100,21 @@ FocusScope { } } - Entity { - components: [ - SphereMesh { - }, - Transform { - id: viewCenterTransform - translation: mainCamera.viewCenter - scale: 0.005 * mainCamera.viewCenter.minus(mainCamera.position).length() - }, - PhongMaterial { - ambient: "#FFF" - shininess: 0.2 - diffuse: activePalette.highlight - specular: activePalette.highlight - } - ] + TrackballGizmo { + beamRadius: 4.0/root.height + alpha: cameraController.moving ? 1.0 : 0.7 + transform: Transform { + translation: mainCamera.viewCenter + scale: 0.15 * mainCamera.viewCenter.minus(mainCamera.position).length() + } } DefaultCameraController { id: cameraController + windowSize: Qt.size(root.width, root.height) + rotationSpeed: 10 + trackballSize: 0.4 + camera: mainCamera focus: scene3D.activeFocus onMousePressed: { diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 55e487582b..3cbe3738e6 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -27,5 +27,6 @@ Item { property real cameraScale: 0.3 // Helpers display property bool displayGrid: true - property bool displayLocator: true + property bool displayGizmo: true + property bool displayLocator: false } From 4d73013784e7479594769ee9eec1bcd5dd3c24bc Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 10 Dec 2018 13:40:53 +0100 Subject: [PATCH 056/293] [ui] Add Colors singleton to start centralizing useful colors + use in 3D gizmos --- meshroom/ui/qml/Utils/Colors.qml | 16 ++++++++++++++++ meshroom/ui/qml/Utils/qmldir | 1 + meshroom/ui/qml/Viewer3D/Locator3D.qml | 13 +++++++------ meshroom/ui/qml/Viewer3D/TrackballGizmo.qml | 19 +++++++++---------- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 6 ++++++ 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 meshroom/ui/qml/Utils/Colors.qml diff --git a/meshroom/ui/qml/Utils/Colors.qml b/meshroom/ui/qml/Utils/Colors.qml new file mode 100644 index 0000000000..15e79b9f65 --- /dev/null +++ b/meshroom/ui/qml/Utils/Colors.qml @@ -0,0 +1,16 @@ +pragma Singleton +import QtQuick 2.9 +import QtQuick.Controls 2.4 + +/** + * Singleton that gathers useful colors, shades and system palettes. + */ +QtObject { + property SystemPalette sysPalette: SystemPalette {} + property SystemPalette disabledSysPalette: SystemPalette { colorGroup: SystemPalette.Disabled } + + readonly property color green: "#4CAF50" + readonly property color orange: "#FF9800" + readonly property color red: "#F44336" + readonly property color blue: "#03A9F4" +} diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index 9ab0a412a9..a81d6c9639 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -1,5 +1,6 @@ module Utils +singleton Colors 1.0 Colors.qml SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml Request 1.0 request.js Format 1.0 format.js diff --git a/meshroom/ui/qml/Viewer3D/Locator3D.qml b/meshroom/ui/qml/Viewer3D/Locator3D.qml index 32b31446a2..20ad95cf30 100644 --- a/meshroom/ui/qml/Viewer3D/Locator3D.qml +++ b/meshroom/ui/qml/Viewer3D/Locator3D.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 +import Utils 1.0 // Locator Entity { @@ -38,12 +39,12 @@ Entity { buffer: Buffer { type: Buffer.VertexBuffer data: Float32Array([ - 1.0, 0.0, 0.0, - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0, - 0.0, 0.0, 1.0 + Colors.red.r, Colors.red.g, Colors.red.b, + Colors.red.r, Colors.red.g, Colors.red.b, + Colors.green.r, Colors.green.g, Colors.green.b, + Colors.green.r, Colors.green.g, Colors.green.b, + Colors.blue.r, Colors.blue.g, Colors.blue.b, + Colors.blue.r, Colors.blue.g, Colors.blue.b ]) } } diff --git a/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml b/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml index 16ccefa754..38efa9617b 100644 --- a/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml +++ b/meshroom/ui/qml/Viewer3D/TrackballGizmo.qml @@ -10,9 +10,10 @@ Entity { property real beamLength: 1 property int slices: 10 property int rings: 50 - property color colorX: "#F44336" - property color colorY: "#8BC34A" - property color colorZ: "#03A9F4" + property color centerColor: "white" + property color xColor: "red" + property color yColor: "green" + property color zColor: "blue" property real alpha: 1.0 property Transform transform: Transform {} @@ -22,8 +23,6 @@ Entity { PropertyAnimation { duration: 100 } } - SystemPalette { id: sysPalette } - // Gizmo center Entity { components: [ @@ -31,8 +30,8 @@ Entity { PhongMaterial { ambient: "#FFF" shininess: 0.2 - diffuse: sysPalette.highlight - specular: sysPalette.highlight + diffuse: centerColor + specular: centerColor } ] } @@ -51,9 +50,9 @@ Entity { DiffuseSpecularMaterial { ambient: { switch(index) { - case 0: return colorX; - case 1: return colorY; - case 2: return colorZ; + case 0: return xColor; + case 1: return yColor; + case 2: return zColor; } } shininess: 0 diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index da0c452e13..c6c8e1fcc5 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -12,6 +12,7 @@ import Qt3D.Input 2.1 as Qt3DInput // to avoid clash with Controls2 Action import MaterialIcons 2.2 import Controls 1.0 +import Utils 1.0 FocusScope { @@ -103,6 +104,11 @@ FocusScope { TrackballGizmo { beamRadius: 4.0/root.height alpha: cameraController.moving ? 1.0 : 0.7 + enabled: Viewer3DSettings.displayGizmo + xColor: Colors.red + yColor: Colors.green + zColor: Colors.blue + centerColor: Colors.sysPalette.highlight transform: Transform { translation: mainCamera.viewCenter scale: 0.15 * mainCamera.viewCenter.minus(mainCamera.position).length() From b2e1743a6f3dd50dc4d8843a8b9bad914d127083 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 11 Dec 2018 16:38:37 +0100 Subject: [PATCH 057/293] [ui] Viewer3D: move TrackballController code to Python side Port JS trackball camera controller code to Python for improved performance and stability. --- meshroom/ui/components/__init__.py | 3 +- meshroom/ui/components/scene3D.py | 62 +++++++++++++++++-- .../qml/Viewer3D/DefaultCameraController.qml | 50 ++++----------- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index 23b4e9072b..ae258aa6d8 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -3,8 +3,9 @@ def registerTypes(): from PySide2.QtQml import qmlRegisterType from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.filepath import FilepathHelper - from meshroom.ui.components.scene3D import Scene3DHelper + from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable + qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController") diff --git a/meshroom/ui/components/scene3D.py b/meshroom/ui/components/scene3D.py index cb4b2a28b7..971db7935a 100644 --- a/meshroom/ui/components/scene3D.py +++ b/meshroom/ui/components/scene3D.py @@ -1,8 +1,11 @@ -from PySide2.QtCore import QObject, Slot +from math import acos, pi, sqrt + +from PySide2.QtCore import QObject, Slot, QSize, Signal, QPointF from PySide2.Qt3DCore import Qt3DCore from PySide2.Qt3DRender import Qt3DRender -from PySide2.QtGui import QVector3D, QQuaternion +from PySide2.QtGui import QVector3D, QQuaternion, QVector2D +from meshroom.ui.utils import makeProperty class Scene3DHelper(QObject): @@ -42,7 +45,54 @@ def faceCount(self, entity): count += sum([attr.count() for attr in geo.attributes() if attr.name() == "vertexPosition"]) return count / 3 - @Slot(QQuaternion, QVector3D, result=QVector3D) - def rotatedVector(self, quaternion, vector): - """ Returns the rotation of 'vector' with 'quaternion'. """ - return quaternion.rotatedVector(vector) + +class TrackballController(QObject): + """ + Trackball-like camera controller. + Based on the C++ version from https://github.com/cjmdaixi/Qt3DTrackball + """ + + _windowSize = QSize() + _camera = None + _trackballSize = 1.0 + _rotationSpeed = 5.0 + + def projectToTrackball(self, screenCoords): + sx = screenCoords.x() + sy = self._windowSize.height() - screenCoords.y() + p2d = QVector2D(sx / self._windowSize.width() - 0.5, sy / self._windowSize.height() - 0.5) + z = 0.0 + r2 = pow(self._trackballSize, 2) + lengthSquared = p2d.lengthSquared() + if lengthSquared <= r2 * 0.5: + z = sqrt(r2 - lengthSquared) + else: + z = r2 * 0.5 / p2d.length() + return QVector3D(p2d.x(), p2d.y(), z) + + @staticmethod + def clamp(x): + return max(-1, min(x, 1)) + + def createRotation(self, firstPoint, nextPoint): + lastPos3D = self.projectToTrackball(firstPoint).normalized() + currentPos3D = self.projectToTrackball(nextPoint).normalized() + angle = acos(self.clamp(QVector3D.dotProduct(currentPos3D, lastPos3D))) + direction = QVector3D.crossProduct(currentPos3D, lastPos3D) + return angle, direction + + @Slot(QPointF, QPointF, float) + def rotate(self, lastPosition, currentPosition, dt): + angle, direction = self.createRotation(lastPosition, currentPosition) + rotatedAxis = self._camera.transform().rotation().rotatedVector(direction) + angle *= self._rotationSpeed * dt + self._camera.rotateAboutViewCenter(QQuaternion.fromAxisAndAngle(rotatedAxis, angle * pi * 180)) + + windowSizeChanged = Signal() + windowSize = makeProperty(QSize, '_windowSize', windowSizeChanged) + cameraChanged = Signal() + camera = makeProperty(Qt3DRender.QCamera, '_camera', cameraChanged) + trackballSizeChanged = Signal() + trackballSize = makeProperty(float, '_trackballSize', trackballSizeChanged) + rotationSpeedChanged = Signal() + rotationSpeed = makeProperty(float, '_rotationSpeed', rotationSpeedChanged) diff --git a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml index 792b901cdc..aad3942b92 100644 --- a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml +++ b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml @@ -5,6 +5,8 @@ import Qt3D.Input 2.1 import Qt3D.Logic 2.0 import QtQml 2.2 +import Meshroom.Helpers 1.0 + Entity { id: root @@ -15,9 +17,9 @@ Entity { property bool moving: pressed || (actionAlt.active && keyboardHandler._pressed) property alias focus: keyboardHandler.focus readonly property bool pickingActive: actionControl.active && keyboardHandler._pressed - property real rotationSpeed: 2.0 - property size windowSize - property real trackballSize: 1.0 + property alias rotationSpeed: trackball.rotationSpeed + property alias windowSize: trackball.windowSize + property alias trackballSize: trackball.trackballSize readonly property alias pressed: mouseHandler._pressed signal mousePressed(var mouse) @@ -29,6 +31,11 @@ Entity { KeyboardDevice { id: keyboardSourceDevice } MouseDevice { id: mouseSourceDevice } + TrackballController { + id: trackball + camera: root.camera + } + MouseHandler { id: mouseHandler property bool _pressed @@ -148,36 +155,6 @@ Entity { ] } - // Based on the C++ version from https://github.com/cjmdaixi/Qt3DTrackball - function projectToTrackball(screenCoords) - { - var sx = screenCoords.x, sy = windowSize.height - screenCoords.y; - var p2d = Qt.vector2d(sx / windowSize.width - 0.5, sy / windowSize.height - 0.5); - var z = 0.0; - var r2 = trackballSize * trackballSize; - var lengthSquared = p2d.length() * p2d.length(); - if(lengthSquared <= r2 * 0.5) - z = Math.sqrt(r2 - lengthSquared); - else - z = r2 * 0.5 / p2d.length(); - return Qt.vector3d(p2d.x, p2d.y, z); - } - - function clamp(x) - { - return Math.max(-1, Math.min(x, 1)); - } - - function createRotation(firstPoint, nextPoint) - { - var lastPos3D = projectToTrackball(firstPoint).normalized(); - var currentPos3D = projectToTrackball(nextPoint).normalized(); - var obj = {}; - obj.angle = Math.acos(clamp(currentPos3D.dotProduct(lastPos3D))); - obj.dir = currentPos3D.crossProduct(lastPos3D); - return obj; - } - components: [ FrameAction { onTriggered: { @@ -189,12 +166,7 @@ Entity { return; } if(actionLMB.active){ // trackball rotation - var res = createRotation(mouseHandler.lastPosition, mouseHandler.currentPosition); - var transform = root.camera.components[1]; // transform is camera first component - var currentRotation = transform.rotation; - var rotatedAxis = Scene3DHelper.rotatedVector(transform.rotation, res.dir); - res.angle *= rotationSpeed * dt; - root.camera.rotateAboutViewCenter(transform.fromAxisAndAngle(rotatedAxis, res.angle * Math.PI * 180)); + trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt); mouseHandler.lastPosition = mouseHandler.currentPosition; return; } From 139fe5ac5c86a2e3ce879e75a3c003c51fabaa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 11:43:59 +0100 Subject: [PATCH 058/293] [nodes] `Meshing` Remove unused param `imagesFolder` --- meshroom/nodes/aliceVision/Meshing.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index daf5197b3b..d45dbee284 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -17,13 +17,6 @@ class Meshing(desc.CommandLineNode): value='', uid=[0], ), - desc.File( - name='imagesFolder', - label='Images Folder', - description='Use images from a specific folder. Filename should be the image uid.', - value='', - uid=[0], - ), desc.File( name="depthMapFolder", label='Depth Maps Folder', From 3429f0cce2de93a456bb9b1870273942e85ecbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 11:44:55 +0100 Subject: [PATCH 059/293] [nodes] `StructureFromMotion` Rename param `useRigsCalibration` to `useRigConstraint` --- meshroom/nodes/aliceVision/StructureFromMotion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 283ac7f93c..a9c3cd56d1 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -162,9 +162,9 @@ class StructureFromMotion(desc.CommandLineNode): uid=[], ), desc.BoolParam( - name='useRigsCalibration', - label='Use Rigs Calibration', - description='Enable/Disable rigs calibration.', + name='useRigConstraint', + label='Use Rig Constraint', + description='Enable/Disable rig constraint.', value=True, uid=[0], ), From bc08343a31893a0c1df8a7141685309ef589b7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 15:42:46 +0100 Subject: [PATCH 060/293] [nodes] `PrepareDenseScene` Add option `imagesFolder` --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index e04bebebcd..439e8c9b8b 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -17,6 +17,13 @@ class PrepareDenseScene(desc.CommandLineNode): value='', uid=[0], ), + desc.File( + name='imagesFolder', + label='Images Folder', + description='Use images from a specific folder. Filename should be the same or the image uid.', + value='', + uid=[0], + ), desc.ChoiceParam( name='outputFileType', label='Output File Type', From 1f2baf43c33a95784b5511714d26c63067867052 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 15:51:45 +0100 Subject: [PATCH 061/293] [ui.utils] makeProperty: generate and store reset calllback on instance * change 'destroyCallback' function to 'resetOnDestroy' boolean parameter * create a dedicated lambda to reset value to None when object is destroyed and attach it to the target instance * only way to keep a reference to this function that can be used for destroyed signal (dis)connection with instance accessible --- meshroom/ui/graph.py | 4 ++-- meshroom/ui/utils.py | 25 +++++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index de975832be..60fbacf0c0 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -495,8 +495,8 @@ def clearNodeHover(self): selectedNodeChanged = Signal() # Currently selected node - selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, clearNodeSelection) + selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True) hoveredNodeChanged = Signal() # Currently hovered node - hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, clearNodeHover) + hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True) diff --git a/meshroom/ui/utils.py b/meshroom/ui/utils.py index b660d7cc61..f697f89dfe 100755 --- a/meshroom/ui/utils.py +++ b/meshroom/ui/utils.py @@ -194,7 +194,7 @@ def reload(self): self.load(self._sourceFile) -def makeProperty(T, attributeName, notify=None, destroyCallback=None): +def makeProperty(T, attributeName, notify=None, resetOnDestroy=False): """ Shortcut function to create a Qt Property with generic getter and setter. @@ -205,8 +205,9 @@ def makeProperty(T, attributeName, notify=None, destroyCallback=None): T (type): the type of the property attributeName (str): the name of underlying instance attribute to get/set notify (Signal): the notify signal; if None, property will be constant - destroyCallback (function): (optional) Function to call when value gets destroyed. - Only applicable for QObject-type properties. + resetOnDestroy (bool): Only applicable for QObject-type properties. + Whether to reset property to None when current value gets destroyed. + Examples: class Foo(QObject): @@ -225,11 +226,19 @@ def setter(instance, value): currentValue = getattr(instance, attributeName) if currentValue == value: return - if destroyCallback and currentValue and shiboken2.isValid(currentValue): - currentValue.destroyed.disconnect(destroyCallback) + + resetCallbackName = '__reset__' + attributeName + if resetOnDestroy and not hasattr(instance, resetCallbackName): + # store reset callback on instance, only way to keep a reference to this function + # that can be used for destroyed signal (dis)connection + setattr(instance, resetCallbackName, lambda self=instance, *args: setter(self, None)) + resetCallback = getattr(instance, resetCallbackName, None) + + if resetCallback and currentValue and shiboken2.isValid(currentValue): + currentValue.destroyed.disconnect(resetCallback) setattr(instance, attributeName, value) - if destroyCallback and value: - value.destroyed.connect(destroyCallback) + if resetCallback and value: + value.destroyed.connect(resetCallback) getattr(instance, signalName(notify)).emit() def getter(instance): @@ -241,7 +250,7 @@ def signalName(signalInstance): # string representation contains trailing '()', remove it return str(signalInstance)[:-2] - if destroyCallback and not issubclass(T, QObject): + if resetOnDestroy and not issubclass(T, QObject): raise RuntimeError("destroyCallback can only be used with QObject-type properties.") if notify: return Property(T, getter, setter, notify=notify) From 53d9fa160afb905c095e71dd8ca67896625cd184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 16:40:04 +0100 Subject: [PATCH 062/293] [nodes] `PrepareDenseScene` option `imagesFolder` is now a list `imagesFolders` --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index 439e8c9b8b..c264fc9618 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -17,12 +17,17 @@ class PrepareDenseScene(desc.CommandLineNode): value='', uid=[0], ), - desc.File( - name='imagesFolder', - label='Images Folder', - description='Use images from a specific folder. Filename should be the same or the image uid.', - value='', - uid=[0], + desc.ListAttribute( + elementDesc=desc.File( + name="imagesFolder", + label="Images Folder", + description="", + value="", + uid=[0], + ), + name="imagesFolders", + label="Images Folders", + description='Use images from specific folder(s). Filename should be the same or the image uid.', ), desc.ChoiceParam( name='outputFileType', From edfc34fbd1ef162e228782d3474505814ed80b34 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 17:13:18 +0100 Subject: [PATCH 063/293] [ui] Viewer3D: remove media if no loader was found for a valid source --- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index 7f6960ea88..5430d57837 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -231,6 +231,10 @@ Entity { model[prop] = Qt.binding(function() { return object ? object[prop] : 0; }); }) } + else if(finalSource) { + // source was valid but no loader was created, remove element + remove(index); + } } onStatusChanged: { From 5814a39e3e6d7992b6c46da6a2e90d4eb189a5e5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 17:15:24 +0100 Subject: [PATCH 064/293] [ui] Viewer3D: add loading overlay with BusyIndicator --- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 9 +++++++++ meshroom/ui/qml/Viewer3D/Viewer3D.qml | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index 5430d57837..1decce67fd 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -16,6 +16,15 @@ Entity { property bool pickingEnabled: false readonly property alias count: instantiator.count // number of instantiated media delegates + /// True while at least one media is being loaded + readonly property bool loading: { + for(var i=0; i Date: Thu, 13 Dec 2018 17:35:05 +0100 Subject: [PATCH 065/293] [nodes] `PrepareDenseScene` Spelling correction --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index e04bebebcd..7a3bb5a6b1 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -29,14 +29,14 @@ class PrepareDenseScene(desc.CommandLineNode): desc.BoolParam( name='saveMetadata', label='Save Metadata', - description='Save projections and intrinsics informations in images metadata (only for .exr images).', + description='Save projections and intrinsics information in images metadata (only for .exr images).', value=True, uid=[0], ), desc.BoolParam( name='saveMatricesTxtFiles', label='Save Matrices Text Files', - description='Save projections and intrinsics informations in text files.', + description='Save projections and intrinsics information in text files.', value=False, uid=[0], ), From a0522c7c1457ee8e86f1a46d3f24ce7cecdd165b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 17:36:43 +0100 Subject: [PATCH 066/293] [nodes] `MeshFiltering` option `keepLargestMeshOnly` to `False` --- meshroom/nodes/aliceVision/MeshFiltering.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/nodes/aliceVision/MeshFiltering.py b/meshroom/nodes/aliceVision/MeshFiltering.py index 460f618680..3734b45b80 100644 --- a/meshroom/nodes/aliceVision/MeshFiltering.py +++ b/meshroom/nodes/aliceVision/MeshFiltering.py @@ -26,13 +26,13 @@ class MeshFiltering(desc.CommandLineNode): name='keepLargestMeshOnly', label='Keep Only the Largest Mesh', description='Keep only the largest connected triangles group.', - value=True, + value=False, uid=[0], ), desc.IntParam( name='iterations', - label='Nb Iterations', - description='', + label='Smoothing Iterations', + description='Number of smoothing iterations', value=5, range=(0, 50, 1), uid=[0], From 05c2305df79a3d4ab2c3f9bec8179cb095f531f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 17:37:28 +0100 Subject: [PATCH 067/293] [nodes] `Meshing` option `estimateSpaceMinObservationAngle` to 10 --- meshroom/nodes/aliceVision/Meshing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index d45dbee284..2b0bc0b86f 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -50,8 +50,8 @@ class Meshing(desc.CommandLineNode): name='estimateSpaceMinObservationAngle', label='Min Observations Angle For SfM Space Estimation', description='Minimum angle between two observations for SfM space estimation.', - value=0.2, - range=(0, 10, 0.1), + value=10, + range=(0, 120, 1), uid=[0], ), desc.IntParam( From d0da1b1c902aff734596d9e2161ee33d26a90a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 17:38:38 +0100 Subject: [PATCH 068/293] [nodes] `DepthMapFilter` Spelling correction --- meshroom/nodes/aliceVision/DepthMapFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index 5dd7b60c02..1dddcc5014 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -50,7 +50,7 @@ class DepthMapFilter(desc.CommandLineNode): uid=[0], ), desc.IntParam( - name="minNumOfConsistensCams", + name="minNumOfConsistentCams", label="Min Consistent Cameras", description="Min Number of Consistent Cameras", value=3, @@ -58,7 +58,7 @@ class DepthMapFilter(desc.CommandLineNode): uid=[0], ), desc.IntParam( - name="minNumOfConsistensCamsWithLowSimilarity", + name="minNumOfConsistentCamsWithLowSimilarity", label="Min Consistent Cameras Bad Similarity", description="Min Number of Consistent Cameras for pixels with weak similarity value", value=4, From 05f0ae50fc55ae4da3af75f8f60816d5e54b04d7 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 17:39:28 +0100 Subject: [PATCH 069/293] [ui] Viewer3D: remove scene orientation controls new trackball manipulator offers more intuitive 3D navigation regardless of the camera orientation --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 26 ++---------------------- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 1 - 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 1c03b64155..4f42ec4080 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -14,8 +14,6 @@ FloatingPane { implicitWidth: 200 property int renderMode: 2 - property Transform targetTransform - property Locator3D origin: null property Grid3D grid: null property MediaLibrary mediaLibrary property Camera camera @@ -38,31 +36,12 @@ FloatingPane { GridLayout { width: parent.width - columns: 3 + columns: 2 columnSpacing: 6 rowSpacing: 3 - // Rotation Controls - Label { - font.family: MaterialIcons.fontFamily - text: MaterialIcons.rotation3D - font.pointSize: 14 - Layout.rowSpan: 3 - Layout.alignment: Qt.AlignHCenter - } - - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value} - Label { text: "X" } - - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value} - Label { text: "Y" } - - Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value } - Label { text: "Z" } - Label { text: "Points"; padding: 2 } RowLayout { - Layout.columnSpan: 2 Slider { Layout.fillWidth: true; from: 0.1; to: 10; stepSize: 0.1 value: Viewer3DSettings.pointSize @@ -80,7 +59,6 @@ FloatingPane { } Label { text: "Cameras"; padding: 2 } Slider { - Layout.columnSpan: 2 value: Viewer3DSettings.cameraScale from: 0 to: 2 @@ -90,7 +68,7 @@ FloatingPane { onMoved: Viewer3DSettings.cameraScale = value } Flow { - Layout.columnSpan: 3 + Layout.columnSpan: 2 Layout.fillWidth: true spacing: 2 CheckBox { diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index e9f204252c..12d2da7dbf 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -240,7 +240,6 @@ FocusScope { Layout.minimumWidth: 5 camera: mainCamera - targetTransform: transform mediaLibrary: mediaLibrary } } From e1f30eb3205d9b3d1ab8c67f978117cecc08fac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 13 Dec 2018 17:40:31 +0100 Subject: [PATCH 070/293] [nodes] `DepthMapFilter` & `Meshing` rename option `depthMapFolder` to `depthMapsFolder` --- meshroom/multiview.py | 6 +++--- meshroom/nodes/aliceVision/DepthMapFilter.py | 6 +++--- meshroom/nodes/aliceVision/Meshing.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 0c46a74203..766040334a 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -126,11 +126,11 @@ def mvsPipeline(graph, sfm=None): imagesFolder=prepareDenseScene.output) depthMapFilter = graph.addNewNode('DepthMapFilter', input=depthMap.input, - depthMapFolder=depthMap.output) + depthMapsFolder=depthMap.output) meshing = graph.addNewNode('Meshing', input=depthMapFilter.input, - depthMapFolder=depthMapFilter.depthMapFolder, - depthMapFilterFolder=depthMapFilter.output) + depthMapsFolder=depthMapFilter.depthMapsFolder, + depthMapsFilterFolder=depthMapFilter.output) meshFiltering = graph.addNewNode('MeshFiltering', input=meshing.output) texturing = graph.addNewNode('Texturing', diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index 1dddcc5014..94bfbe93cf 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -19,9 +19,9 @@ class DepthMapFilter(desc.CommandLineNode): uid=[0], ), desc.File( - name="depthMapFolder", - label="Depth Map Folder", - description="Input depth map folder", + name="depthMapsFolder", + label="Depth Maps Folder", + description="Input depth maps folder", value="", uid=[0], ), diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 2b0bc0b86f..decda83360 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -18,14 +18,14 @@ class Meshing(desc.CommandLineNode): uid=[0], ), desc.File( - name="depthMapFolder", + name="depthMapsFolder", label='Depth Maps Folder', description='Input depth maps folder', value='', uid=[0], ), desc.File( - name="depthMapFilterFolder", + name="depthMapsFilterFolder", label='Filtered Depth Maps Folder', description='Input filtered depth maps folder', value='', From 0db42e5acc0f873d79e1e6768d79a657b4c5234e Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 17:39:54 +0100 Subject: [PATCH 071/293] [ui] Viewer3D: rename "Locator" to "Origin" --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 6 +++--- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 3 +-- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 4f42ec4080..f4af53c49d 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -84,10 +84,10 @@ FloatingPane { onClicked: Viewer3DSettings.displayGizmo = !Viewer3DSettings.displayGizmo } CheckBox { - text: "Locator" + text: "Origin" padding: 2 - checked: Viewer3DSettings.displayLocator - onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator + checked: Viewer3DSettings.displayOrigin + onClicked: Viewer3DSettings.displayOrigin = !Viewer3DSettings.displayOrigin } } } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index 12d2da7dbf..eca8a239af 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -207,9 +207,8 @@ FocusScope { doubleClickTimer.stop(); } - Locator3D { enabled: Viewer3DSettings.displayLocator } } - + Locator3D { enabled: Viewer3DSettings.displayOrigin } Grid3D { enabled: Viewer3DSettings.displayGrid } } } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 3cbe3738e6..202b6c216b 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -28,5 +28,5 @@ Item { // Helpers display property bool displayGrid: true property bool displayGizmo: true - property bool displayLocator: false + property bool displayOrigin: false } From d9a0fe13e0d0854c065b28ac6f8236a00624325b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 17 Dec 2018 11:46:34 +0100 Subject: [PATCH 072/293] [nodes] `DepthMap` & `DepthMapFilter` Fix: node version is `2.0` --- meshroom/nodes/aliceVision/DepthMap.py | 2 +- meshroom/nodes/aliceVision/DepthMapFilter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index cf830ec7ed..97f0ce0867 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index 94bfbe93cf..458ac29cf6 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc From f1fff821f45661a98bacbdb464f9f85b8f04f042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 17 Dec 2018 11:48:01 +0100 Subject: [PATCH 073/293] [nodes] Remove `ConvertAnimatedCamera` The corresponding software no longer exists --- .../aliceVision/ConvertAnimatedCamera.py | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 meshroom/nodes/aliceVision/ConvertAnimatedCamera.py diff --git a/meshroom/nodes/aliceVision/ConvertAnimatedCamera.py b/meshroom/nodes/aliceVision/ConvertAnimatedCamera.py deleted file mode 100644 index 3d834ed444..0000000000 --- a/meshroom/nodes/aliceVision/ConvertAnimatedCamera.py +++ /dev/null @@ -1,27 +0,0 @@ -__version__ = "1.0" - -from meshroom.core import desc - - -class ConvertAnimatedCamera(desc.CommandLineNode): - commandLine = 'aliceVision_convertAnimatedCamera {allParams}' - - inputs = [ - desc.File( - name='input', - label='Input', - description='''SfMData file.''', - value='', - uid=[0], - ), - ] - - outputs = [ - desc.File( - name='output', - label='Output', - description='Path to the output Alembic file.', - value=desc.Node.internalFolder + 'animatedCamera.abc', - uid=[], - ), - ] From e0e10c45b7fa901160c0b434299064024e0ad548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 17 Dec 2018 11:53:34 +0100 Subject: [PATCH 074/293] [nodes] `Texturing` Fix: node bersion is `3.0` --- meshroom/nodes/aliceVision/Texturing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 923339cb3d..f56543ac59 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -1,4 +1,4 @@ -__version__ = "2.0" +__version__ = "3.0" from meshroom.core import desc From 71ebc0cf1ba46c51e2ec295936d350e95005a908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 19 Dec 2018 15:25:11 +0100 Subject: [PATCH 075/293] [nodes] `convertSfMFormat` users can now specify a view white list to filter views --- meshroom/nodes/aliceVision/ConvertSfMFormat.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/ConvertSfMFormat.py b/meshroom/nodes/aliceVision/ConvertSfMFormat.py index b4cc4c7e9f..4d009da209 100644 --- a/meshroom/nodes/aliceVision/ConvertSfMFormat.py +++ b/meshroom/nodes/aliceVision/ConvertSfMFormat.py @@ -5,7 +5,8 @@ class ConvertSfMFormat(desc.CommandLineNode): commandLine = 'aliceVision_convertSfMFormat {allParams}' - + size = desc.DynamicNodeSize('input') + inputs = [ desc.File( name='input', @@ -34,6 +35,18 @@ class ConvertSfMFormat(desc.CommandLineNode): uid=[0], joinChar=',', ), + desc.ListAttribute( + elementDesc=desc.File( + name="imageId", + label="Image id", + description="", + value="", + uid=[0], + ), + name="imageWhiteList", + label="Image White List", + description='image white list (uids or image paths).', + ), desc.BoolParam( name='views', label='Views', From cf5b835272a1a1941c0e62988ac6daffc4590d5b Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Wed, 19 Dec 2018 17:34:45 +0100 Subject: [PATCH 076/293] Enable SfMTransform with selection of one view as reference for the coordinate system. --- meshroom/nodes/aliceVision/SfMTransform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index 1d44cc4c0c..49ed61117e 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -20,7 +20,7 @@ class SfMTransform(desc.CommandLineNode): label='Transformation Method', description='''Transformation method (transformation, auto_from_cameras, auto_from_landmarks).''', value='auto_from_landmarks', - values=['transformation', 'auto_from_cameras', 'auto_from_landmarks'], + values=['transformation', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera'], exclusive=True, uid=[0], ), From 65ec2d32455769706d5d3d553f1034c78d5efbd3 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 19 Dec 2018 17:30:33 +0100 Subject: [PATCH 077/293] [ui] Viewer3D: disable pointclouds/cameras when scaled down to 0 --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 2 +- meshroom/ui/qml/Viewer3D/MediaLoader.qml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index f4af53c49d..984474dfbf 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -43,7 +43,7 @@ FloatingPane { Label { text: "Points"; padding: 2 } RowLayout { Slider { - Layout.fillWidth: true; from: 0.1; to: 10; stepSize: 0.1 + Layout.fillWidth: true; from: 0; to: 10; stepSize: 0.1 value: Viewer3DSettings.pointSize onValueChanged: Viewer3DSettings.pointSize = value } diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index 59e049aba1..c24e8eb79f 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -93,8 +93,12 @@ import Utils 1.0 if(obj.status === SceneLoader.Ready) { for(var i = 0; i < obj.pointClouds.length; ++i) { vertexCount += Scene3DHelper.vertexCount(obj.pointClouds[i]); + obj.pointClouds[i].enabled = Qt.binding(function() { return Viewer3DSettings.pointSize > 0; }); } cameraCount = obj.spawnCameraSelectors(); + for(var i = 0; i < obj.cameras.length; ++i) { + obj.cameras[i].enabled = Qt.binding(function() { return Viewer3DSettings.cameraScale > 0; }); + } } root.status = obj.status; }) From d1d93d337dec4c2d534754e28cc4254da6f54812 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Wed, 19 Dec 2018 17:55:31 +0100 Subject: [PATCH 078/293] [cli] add possibility to give a .sfm as input with (optional) intrinsics --- bin/meshroom_photogrammetry | 34 +++++++++++++++++++++++++++++++--- meshroom/multiview.py | 4 +++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 40dba6556c..5539c9810f 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -1,5 +1,8 @@ #!/usr/bin/env python import argparse +import json +import os +import io import meshroom meshroom.setupEnvironment() @@ -43,8 +46,34 @@ if not args.input and not args.inputImages: print('Nothing to compute. You need to set --input or --inputImages.') exit(1) -graph = multiview.photogrammetry(inputFolder=args.input, inputImages=args.inputImages, output=args.output) -graph.findNode('DepthMap_1').downscale.value = args.scale +if args.input and os.path.isfile(args.input): + # with open(args.input) as jsonFile: + with io.open(args.input, 'r', encoding='utf-8', errors='ignore') as jsonFile: + fileData = json.load(jsonFile) + intrinsics = fileData.get("intrinsics", []) + print(intrinsics) + intrinsics = [{k: v for k, v in item.items()} for item in fileData.get("intrinsics", [])] + for intrinsic in intrinsics: + pp = intrinsic['principalPoint'] + intrinsic['principalPoint'] = {} + intrinsic['principalPoint']['x'] = pp[0] + intrinsic['principalPoint']['y'] = pp[1] + # convert empty string distortionParams (i.e: Pinhole model) to empty list + if intrinsic['distortionParams'] == '': + intrinsic['distortionParams'] = list() + print(intrinsics) + + # views = fileData.get("views", []) + views = [{k: v for k, v in item.items()} for item in fileData.get("views", [])] + for view in views: + view['metadata'] = json.dumps(view['metadata']) # convert metadata to string + # print(views) + + graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, inputImages=args.inputImages, output=args.output) + graph.findNode('DepthMap_1').downscale.value = args.scale +else: + graph = multiview.photogrammetry(inputFolder=args.input, inputImages=args.inputImages, output=args.output) + graph.findNode('DepthMap_1').downscale.value = args.scale if args.save: graph.save(args.save) @@ -62,4 +91,3 @@ if args.toNode: toNodes = graph.findNodes(args.toNode) meshroom.core.graph.executeGraph(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus) - diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 766040334a..9aa4a23f3a 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -20,7 +20,7 @@ def findFiles(folder, patterns): return outFiles -def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), output=''): +def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), inputIntrinsics=(), output=''): """ Create a new Graph with a complete photogrammetry pipeline. @@ -44,6 +44,8 @@ def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), output='' cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) if inputViewpoints: cameraInit.viewpoints.extend(inputViewpoints) + if inputIntrinsics: + cameraInit.intrinsics.extend(inputIntrinsics) if output: texturing = mvsNodes[-1] From e1c5a6c5e731bd81b0e69383999095cee2b94c78 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Wed, 19 Dec 2018 17:56:11 +0100 Subject: [PATCH 079/293] [nodes] exposed refineIntrinsics param to lock the camera intrinsics for all cameras --- meshroom/nodes/aliceVision/StructureFromMotion.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index a9c3cd56d1..8e426dd371 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -168,6 +168,16 @@ class StructureFromMotion(desc.CommandLineNode): value=True, uid=[0], ), + desc.BoolParam( + name='refineIntrinsics', + label='Refine intrinsic parameters.', + description='The intrinsics parameters of the cameras (focal length, \n' + 'principal point, distortion if any) are kept constant ' + 'during the reconstruction.\n' + 'This may be helpful if the input cameras are already fully calibrated.', + value=True, + uid=[0], + ), desc.File( name='initialPairA', label='Initial Pair A', From 8ada83444725907206886e6d153d39e10761a6dd Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 10 Dec 2018 16:53:16 +0100 Subject: [PATCH 080/293] [core] Attribute: new 'advanced' notion on parameters --- meshroom/core/desc.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 02ac9e2e9c..9ec5f4bbcf 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -11,7 +11,7 @@ class Attribute(BaseObject): """ """ - def __init__(self, name, label, description, value, uid, group): + def __init__(self, name, label, description, value, advanced, uid, group): super(Attribute, self).__init__() self._name = name self._label = label @@ -19,6 +19,7 @@ def __init__(self, name, label, description, value, uid, group): self._value = value self._uid = uid self._group = group + self._advanced = advanced name = Property(str, lambda self: self._name, constant=True) label = Property(str, lambda self: self._label, constant=True) @@ -26,6 +27,7 @@ def __init__(self, name, label, description, value, uid, group): value = Property(Variant, lambda self: self._value, constant=True) uid = Property(Variant, lambda self: self._uid, constant=True) group = Property(str, lambda self: self._group, constant=True) + advanced = Property(bool, lambda self: self._advanced, constant=True) type = Property(str, lambda self: self.__class__.__name__, constant=True) def validateValue(self, value): @@ -40,7 +42,7 @@ def __init__(self, elementDesc, name, label, description, group='allParams', joi """ self._elementDesc = elementDesc self._joinChar = joinChar - super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group) + super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group, advanced=False) elementDesc = Property(Attribute, lambda self: self._elementDesc, constant=True) uid = Property(Variant, lambda self: self.elementDesc.uid, constant=True) @@ -60,7 +62,7 @@ def __init__(self, groupDesc, name, label, description, group='allParams', joinC """ self._groupDesc = groupDesc self._joinChar = joinChar - super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group) + super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group, advanced=False) groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) @@ -82,15 +84,15 @@ def retrieveChildrenUids(self): class Param(Attribute): """ """ - def __init__(self, name, label, description, value, uid, group): - super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + def __init__(self, name, label, description, value, uid, group, advanced): + super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) class File(Attribute): """ """ - def __init__(self, name, label, description, value, uid, group='allParams'): - super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False): + super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def validateValue(self, value): if not isinstance(value, pyCompatibility.basestring): @@ -101,8 +103,8 @@ def validateValue(self, value): class BoolParam(Param): """ """ - def __init__(self, name, label, description, value, uid, group='allParams'): - super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False): + super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def validateValue(self, value): try: @@ -114,9 +116,9 @@ def validateValue(self, value): class IntParam(Param): """ """ - def __init__(self, name, label, description, value, range, uid, group='allParams'): + def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False): self._range = range - super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def validateValue(self, value): # handle unsigned int values that are translated to int by shiboken and may overflow @@ -133,9 +135,9 @@ def validateValue(self, value): class FloatParam(Param): """ """ - def __init__(self, name, label, description, value, range, uid, group='allParams'): + def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False): self._range = range - super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def validateValue(self, value): try: @@ -149,13 +151,13 @@ def validateValue(self, value): class ChoiceParam(Param): """ """ - def __init__(self, name, label, description, value, values, exclusive, uid, group='allParams', joinChar=' '): + def __init__(self, name, label, description, value, values, exclusive, uid, group='allParams', joinChar=' ', advanced=False): assert values self._values = values self._exclusive = exclusive self._joinChar = joinChar self._valueType = type(self._values[0]) # cast to value type - super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def conformValue(self, val): """ Conform 'val' to the correct type and check for its validity """ @@ -180,8 +182,8 @@ def validateValue(self, value): class StringParam(Param): """ """ - def __init__(self, name, label, description, value, uid, group='allParams'): - super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False): + super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced) def validateValue(self, value): if not isinstance(value, pyCompatibility.basestring): From 168b573e368b86543c504db6e129c4789dec91af Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 12 Dec 2018 12:29:19 +0100 Subject: [PATCH 081/293] [nodes][aliceVision] mark advanced parameters --- meshroom/nodes/aliceVision/DepthMap.py | 29 ++++++++++++++----- meshroom/nodes/aliceVision/DepthMapFilter.py | 5 ++++ .../nodes/aliceVision/FeatureExtraction.py | 1 + meshroom/nodes/aliceVision/FeatureMatching.py | 8 +++++ meshroom/nodes/aliceVision/ImageMatching.py | 4 +++ .../aliceVision/ImageMatchingMultiSfM.py | 4 +++ meshroom/nodes/aliceVision/MeshDecimate.py | 1 + meshroom/nodes/aliceVision/MeshFiltering.py | 1 + meshroom/nodes/aliceVision/Meshing.py | 16 ++++++++++ .../nodes/aliceVision/PrepareDenseScene.py | 3 ++ .../nodes/aliceVision/StructureFromMotion.py | 11 +++++++ meshroom/nodes/aliceVision/Texturing.py | 4 +++ 12 files changed, 79 insertions(+), 8 deletions(-) diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index 97f0ce0867..6ba1f32ad3 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -41,6 +41,7 @@ class DepthMap(desc.CommandLineNode): value=2.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='maxViewAngle', @@ -49,6 +50,7 @@ class DepthMap(desc.CommandLineNode): value=70.0, range=(10.0, 120.0, 1), uid=[0], + advanced=True, ), desc.IntParam( name='sgmMaxTCams', @@ -65,6 +67,7 @@ class DepthMap(desc.CommandLineNode): value=4, range=(1, 20, 1), uid=[0], + advanced=True, ), desc.FloatParam( name='sgmGammaC', @@ -73,6 +76,7 @@ class DepthMap(desc.CommandLineNode): value=5.5, range=(0.0, 30.0, 0.5), uid=[0], + advanced=True, ), desc.FloatParam( name='sgmGammaP', @@ -81,6 +85,7 @@ class DepthMap(desc.CommandLineNode): value=8.0, range=(0.0, 30.0, 0.5), uid=[0], + advanced=True, ), desc.IntParam( name='refineNSamplesHalf', @@ -89,6 +94,7 @@ class DepthMap(desc.CommandLineNode): value=150, range=(1, 500, 10), uid=[0], + advanced=True, ), desc.IntParam( name='refineNDepthsToRefine', @@ -97,6 +103,7 @@ class DepthMap(desc.CommandLineNode): value=31, range=(1, 100, 1), uid=[0], + advanced=True, ), desc.IntParam( name='refineNiters', @@ -105,14 +112,7 @@ class DepthMap(desc.CommandLineNode): value=100, range=(1, 500, 10), uid=[0], - ), - desc.IntParam( - name='refineWSH', - label='Refine: WSH', - description='Refine: Half-size of the patch used to compute the similarity.', - value=3, - range=(1, 20, 1), - uid=[0], + advanced=True, ), desc.IntParam( name='refineMaxTCams', @@ -122,6 +122,15 @@ class DepthMap(desc.CommandLineNode): range=(1, 20, 1), uid=[0], ), + desc.IntParam( + name='refineWSH', + label='Refine: WSH', + description='Refine: Half-size of the patch used to compute the similarity.', + value=3, + range=(1, 20, 1), + uid=[0], + advanced=True, + ), desc.FloatParam( name='refineSigma', label='Refine: Sigma', @@ -129,6 +138,7 @@ class DepthMap(desc.CommandLineNode): value=15, range=(0.0, 30.0, 0.5), uid=[0], + advanced=True, ), desc.FloatParam( name='refineGammaC', @@ -137,6 +147,7 @@ class DepthMap(desc.CommandLineNode): value=15.5, range=(0.0, 30.0, 0.5), uid=[0], + advanced=True, ), desc.FloatParam( name='refineGammaP', @@ -145,6 +156,7 @@ class DepthMap(desc.CommandLineNode): value=8.0, range=(0.0, 30.0, 0.5), uid=[0], + advanced=True, ), desc.BoolParam( name='refineUseTcOrRcPixSize', @@ -152,6 +164,7 @@ class DepthMap(desc.CommandLineNode): description='Refine: Use minimum pixel size of neighbour cameras (Tc) or current camera pixel size (Rc)', value=False, uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index 458ac29cf6..73c33fd0d9 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -32,6 +32,7 @@ class DepthMapFilter(desc.CommandLineNode): value=2.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='maxViewAngle', @@ -40,6 +41,7 @@ class DepthMapFilter(desc.CommandLineNode): value=70.0, range=(10.0, 120.0, 1), uid=[0], + advanced=True, ), desc.IntParam( name="nNearestCams", @@ -48,6 +50,7 @@ class DepthMapFilter(desc.CommandLineNode): value=10, range=(0, 20, 1), uid=[0], + advanced=True, ), desc.IntParam( name="minNumOfConsistentCams", @@ -72,6 +75,7 @@ class DepthMapFilter(desc.CommandLineNode): value=0, range=(0, 10, 1), uid=[0], + advanced=True, ), desc.IntParam( name="pixSizeBallWithLowSimilarity", @@ -80,6 +84,7 @@ class DepthMapFilter(desc.CommandLineNode): value=0, range=(0, 10, 1), uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index 94363dd602..9165add99b 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -42,6 +42,7 @@ class FeatureExtraction(desc.CommandLineNode): description='Use only CPU feature extraction.', value=False, uid=[], + advanced=True ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/FeatureMatching.py b/meshroom/nodes/aliceVision/FeatureMatching.py index 6fef1648fc..0c487150d6 100644 --- a/meshroom/nodes/aliceVision/FeatureMatching.py +++ b/meshroom/nodes/aliceVision/FeatureMatching.py @@ -60,6 +60,7 @@ class FeatureMatching(desc.CommandLineNode): values=('BRUTE_FORCE_L2', 'ANN_L2', 'CASCADE_HASHING_L2', 'FAST_CASCADE_HASHING_L2', 'BRUTE_FORCE_HAMMING'), exclusive=True, uid=[0], + advanced=True, ), desc.ChoiceParam( name='geometricEstimator', @@ -69,6 +70,7 @@ class FeatureMatching(desc.CommandLineNode): values=['acransac', 'loransac'], exclusive=True, uid=[0], + advanced=True, ), desc.ChoiceParam( name='geometricFilterType', @@ -83,6 +85,7 @@ class FeatureMatching(desc.CommandLineNode): values=['fundamental_matrix', 'essential_matrix', 'homography_matrix', 'homography_growing', 'no_filtering'], exclusive=True, uid=[0], + advanced=True, ), desc.FloatParam( name='distanceRatio', @@ -91,6 +94,7 @@ class FeatureMatching(desc.CommandLineNode): value=0.8, range=(0.0, 1.0, 0.01), uid=[0], + advanced=True, ), desc.IntParam( name='maxIteration', @@ -99,6 +103,7 @@ class FeatureMatching(desc.CommandLineNode): value=2048, range=(1, 20000, 1), uid=[0], + advanced=True, ), desc.IntParam( name='maxMatches', @@ -107,6 +112,7 @@ class FeatureMatching(desc.CommandLineNode): value=0, range=(0, 10000, 1), uid=[0], + advanced=True, ), desc.BoolParam( name='savePutativeMatches', @@ -114,6 +120,7 @@ class FeatureMatching(desc.CommandLineNode): description='putative matches.', value=False, uid=[0], + advanced=True, ), desc.BoolParam( name='guidedMatching', @@ -128,6 +135,7 @@ class FeatureMatching(desc.CommandLineNode): description='debug files (svg, dot).', value=False, uid=[], + advanced=True ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/ImageMatching.py b/meshroom/nodes/aliceVision/ImageMatching.py index a774fc3ab8..8d4ec51536 100644 --- a/meshroom/nodes/aliceVision/ImageMatching.py +++ b/meshroom/nodes/aliceVision/ImageMatching.py @@ -41,6 +41,7 @@ class ImageMatching(desc.CommandLineNode): description='Input name for the weight file, if not provided the weights will be computed on the database built with the provided set.', value='', uid=[0], + advanced=True, ), desc.IntParam( name='minNbImages', @@ -49,6 +50,7 @@ class ImageMatching(desc.CommandLineNode): value=200, range=(0, 500, 1), uid=[0], + advanced=True, ), desc.IntParam( name='maxDescriptors', @@ -57,6 +59,7 @@ class ImageMatching(desc.CommandLineNode): value=500, range=(0, 100000, 1), uid=[0], + advanced=True, ), desc.IntParam( name='nbMatches', @@ -65,6 +68,7 @@ class ImageMatching(desc.CommandLineNode): value=50, range=(0, 1000, 1), uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py b/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py index 09dca195b6..2a0b56a1e6 100644 --- a/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py +++ b/meshroom/nodes/aliceVision/ImageMatchingMultiSfM.py @@ -49,6 +49,7 @@ class ImageMatchingMultiSfM(desc.CommandLineNode): description='Input name for the weight file, if not provided the weights will be computed on the database built with the provided set.', value='', uid=[0], + advanced=True, ), desc.ChoiceParam( name='matchingMode', @@ -66,6 +67,7 @@ class ImageMatchingMultiSfM(desc.CommandLineNode): value=200, range=(0, 500, 1), uid=[0], + advanced=True, ), desc.IntParam( name='maxDescriptors', @@ -74,6 +76,7 @@ class ImageMatchingMultiSfM(desc.CommandLineNode): value=500, range=(0, 100000, 1), uid=[0], + advanced=True, ), desc.IntParam( name='nbMatches', @@ -82,6 +85,7 @@ class ImageMatchingMultiSfM(desc.CommandLineNode): value=50, range=(0, 1000, 1), uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/MeshDecimate.py b/meshroom/nodes/aliceVision/MeshDecimate.py index 6c066a1461..280e9319a4 100644 --- a/meshroom/nodes/aliceVision/MeshDecimate.py +++ b/meshroom/nodes/aliceVision/MeshDecimate.py @@ -57,6 +57,7 @@ class MeshDecimate(desc.CommandLineNode): 'and the convention change from one software to another.', value=False, uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/MeshFiltering.py b/meshroom/nodes/aliceVision/MeshFiltering.py index 3734b45b80..a413327742 100644 --- a/meshroom/nodes/aliceVision/MeshFiltering.py +++ b/meshroom/nodes/aliceVision/MeshFiltering.py @@ -44,6 +44,7 @@ class MeshFiltering(desc.CommandLineNode): value=1.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index decda83360..238cfd0901 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -37,6 +37,7 @@ class Meshing(desc.CommandLineNode): description='Estimate the 3d space from the SfM', value=True, uid=[0], + advanced=True, ), desc.IntParam( name='estimateSpaceMinObservations', @@ -45,6 +46,7 @@ class Meshing(desc.CommandLineNode): value=3, range=(0, 100, 1), uid=[0], + advanced=True, ), desc.FloatParam( name='estimateSpaceMinObservationAngle', @@ -77,6 +79,7 @@ class Meshing(desc.CommandLineNode): value=1000000, range=(500000, 30000000, 1000), uid=[0], + advanced=True, ), desc.IntParam( name='minStep', @@ -87,6 +90,7 @@ class Meshing(desc.CommandLineNode): value=2, range=(1, 20, 1), uid=[0], + advanced=True, ), desc.ChoiceParam( name='partitioning', @@ -96,6 +100,7 @@ class Meshing(desc.CommandLineNode): values=('singleBlock', 'auto'), exclusive=True, uid=[0], + advanced=True, ), desc.ChoiceParam( name='repartition', @@ -105,6 +110,7 @@ class Meshing(desc.CommandLineNode): values=('multiResolution', 'regularGrid'), exclusive=True, uid=[0], + advanced=True, ), desc.FloatParam( name='angleFactor', @@ -113,6 +119,7 @@ class Meshing(desc.CommandLineNode): value=15.0, range=(0.0, 200.0, 1.0), uid=[0], + advanced=True, ), desc.FloatParam( name='simFactor', @@ -121,6 +128,7 @@ class Meshing(desc.CommandLineNode): value=15.0, range=(0.0, 200.0, 1.0), uid=[0], + advanced=True, ), desc.FloatParam( name='pixSizeMarginInitCoef', @@ -129,6 +137,7 @@ class Meshing(desc.CommandLineNode): value=2.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='pixSizeMarginFinalCoef', @@ -137,6 +146,7 @@ class Meshing(desc.CommandLineNode): value=4.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='voteMarginFactor', @@ -145,6 +155,7 @@ class Meshing(desc.CommandLineNode): value=4.0, range=(0.1, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='contributeMarginFactor', @@ -153,6 +164,7 @@ class Meshing(desc.CommandLineNode): value=2.0, range=(0.0, 10.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='simGaussianSizeInit', @@ -161,6 +173,7 @@ class Meshing(desc.CommandLineNode): value=10.0, range=(0.0, 50.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='simGaussianSize', @@ -169,6 +182,7 @@ class Meshing(desc.CommandLineNode): value=10.0, range=(0.0, 50.0, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='minAngleThreshold', @@ -177,6 +191,7 @@ class Meshing(desc.CommandLineNode): value=1.0, range=(0.0, 10.0, 0.01), uid=[0], + advanced=True, ), desc.BoolParam( name='refineFuse', @@ -184,6 +199,7 @@ class Meshing(desc.CommandLineNode): description='Refine depth map fusion with the new pixels size defined by angle and similarity scores.', value=True, uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index d68a6c840c..75f92a4a7e 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -37,6 +37,7 @@ class PrepareDenseScene(desc.CommandLineNode): values=['jpg', 'png', 'tif', 'exr'], exclusive=True, uid=[0], + advanced=True ), desc.BoolParam( name='saveMetadata', @@ -44,6 +45,7 @@ class PrepareDenseScene(desc.CommandLineNode): description='Save projections and intrinsics information in images metadata (only for .exr images).', value=True, uid=[0], + advanced=True ), desc.BoolParam( name='saveMatricesTxtFiles', @@ -51,6 +53,7 @@ class PrepareDenseScene(desc.CommandLineNode): description='Save projections and intrinsics information in text files.', value=False, uid=[0], + advanced=True ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index a9c3cd56d1..387732e41c 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -60,6 +60,7 @@ class StructureFromMotion(desc.CommandLineNode): values=['acransac', 'ransac', 'lsmeds', 'loransac', 'maxconsensus'], exclusive=True, uid=[0], + advanced=True, ), desc.BoolParam( name='lockScenePreviouslyReconstructed', @@ -83,6 +84,7 @@ class StructureFromMotion(desc.CommandLineNode): value=1, range=(2, 10, 1), uid=[0], + advanced=True, ), desc.IntParam( name='maxNumberOfMatches', @@ -112,6 +114,7 @@ class StructureFromMotion(desc.CommandLineNode): value=2, range=(2, 10, 1), uid=[0], + advanced=True, ), desc.FloatParam( name='minAngleForTriangulation', @@ -120,6 +123,7 @@ class StructureFromMotion(desc.CommandLineNode): value=3.0, range=(0.1, 10, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='minAngleForLandmark', @@ -128,6 +132,7 @@ class StructureFromMotion(desc.CommandLineNode): value=2.0, range=(0.1, 10, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='maxReprojectionError', @@ -136,6 +141,7 @@ class StructureFromMotion(desc.CommandLineNode): value=4.0, range=(0.1, 10, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='minAngleInitialPair', @@ -144,6 +150,7 @@ class StructureFromMotion(desc.CommandLineNode): value=5.0, range=(0.1, 10, 0.1), uid=[0], + advanced=True, ), desc.FloatParam( name='maxAngleInitialPair', @@ -152,6 +159,7 @@ class StructureFromMotion(desc.CommandLineNode): value=40.0, range=(0.1, 60, 0.1), uid=[0], + advanced=True, ), desc.BoolParam( name='useOnlyMatchesFromInputFolder', @@ -160,6 +168,7 @@ class StructureFromMotion(desc.CommandLineNode): 'Matches folders previously added to the SfMData file will be ignored.', value=False, uid=[], + advanced=True, ), desc.BoolParam( name='useRigConstraint', @@ -167,6 +176,7 @@ class StructureFromMotion(desc.CommandLineNode): description='Enable/Disable rig constraint.', value=True, uid=[0], + advanced=True, ), desc.File( name='initialPairA', @@ -190,6 +200,7 @@ class StructureFromMotion(desc.CommandLineNode): values=('.abc', '.ply'), exclusive=True, uid=[], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index f56543ac59..fcc5fce57e 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -105,6 +105,7 @@ class Texturing(desc.CommandLineNode): value=0.0, range=(0.0, 1.0, 0.01), uid=[0], + advanced=True, ), desc.FloatParam( name='angleHardThreshold', @@ -113,6 +114,7 @@ class Texturing(desc.CommandLineNode): value=90.0, range=(0.0, 180.0, 0.01), uid=[0], + advanced=True, ), desc.BoolParam( name='forceVisibleByAllVertices', @@ -127,6 +129,7 @@ class Texturing(desc.CommandLineNode): description='''Option to flip face normals. It can be needed as it depends on the vertices order in triangles and the convention change from one software to another.''', value=False, uid=[0], + advanced=True, ), desc.ChoiceParam( name='visibilityRemappingMethod', @@ -136,6 +139,7 @@ class Texturing(desc.CommandLineNode): values=['Pull', 'Push', 'PullPush'], exclusive=True, uid=[0], + advanced=True, ), desc.ChoiceParam( name='verboseLevel', From 1c935b6b5a2ce8f0831683f679f504d72578fa38 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 21:05:50 +0100 Subject: [PATCH 082/293] [ui] GraphEditor: show/hide advanced Attributes * use SortFilterModels to filter out advanced attributes when hidden * add GraphEditorSettings with persistent settings related to the GraphEditor --- .../ui/qml/GraphEditor/AttributeEditor.qml | 38 +++++++++++++------ .../qml/GraphEditor/AttributeItemDelegate.qml | 38 +++++++++++-------- .../qml/GraphEditor/GraphEditorSettings.qml | 11 ++++++ meshroom/ui/qml/GraphEditor/qmldir | 1 + 4 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index 2858121211..59f23689ce 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -46,6 +46,11 @@ ColumnLayout { } Menu { id: settingsMenu + MenuItem { + text: "Advanced Attributes" + checked: GraphEditorSettings.showAdvancedAttributes + onClicked: GraphEditorSettings.showAdvancedAttributes = !GraphEditorSettings.showAdvancedAttributes + } MenuItem { text: "Open Cache Folder" onClicked: Qt.openUrlExternally(Filepath.stringToUrl(node.internalFolder)) @@ -87,21 +92,32 @@ ColumnLayout { id: attributesListView anchors.fill: parent - anchors.margins: 4 - + anchors.margins: 2 clip: true - spacing: 1 + spacing: 2 ScrollBar.vertical: ScrollBar { id: scrollBar } - model: node ? node.attributes : undefined - - delegate: AttributeItemDelegate { - readOnly: root.isCompatibilityNode - labelWidth: 180 - width: attributesListView.width - attribute: object - onDoubleClicked: root.attributeDoubleClicked(attr) + model: SortFilterDelegateModel { + + model: node ? node.attributes : null + filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" + filterValue: false + function modelData(item, roleName) { + return item.model.object.desc[roleName] + } + + Component { + id: delegateComponent + AttributeItemDelegate { + width: attributesListView.width - scrollBar.width + readOnly: root.isCompatibilityNode + labelWidth: 180 + attribute: object + onDoubleClicked: root.attributeDoubleClicked(attr) + } + } } + // Helper MouseArea to lose edit/activeFocus // when clicking on the background MouseArea { diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 0965114912..4168acff1d 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -331,26 +331,34 @@ RowLayout { id: groupAttribute_component ListView { id: chilrenListView - model: attribute.value implicitWidth: parent.width implicitHeight: childrenRect.height onCountChanged: forceLayout() spacing: 2 + model: SortFilterDelegateModel { + model: attribute.value + filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" + filterValue: false - delegate: RowLayout { - id: row - width: chilrenListView.width - property var childAttrib: object - - Component.onCompleted: { - var cpt = Qt.createComponent("AttributeItemDelegate.qml") - var obj = cpt.createObject(row, - {'attribute': Qt.binding(function() { return row.childAttrib }), - 'readOnly': Qt.binding(function() { return root.readOnly }) - }) - obj.Layout.fillWidth = true - obj.labelWidth = 100 // reduce label width for children (space gain) - obj.doubleClicked.connect(function(attr) {root.doubleClicked(attr)}) + function modelData(item, roleName) { + return item.model.object.desc[roleName] + } + + delegate: RowLayout { + id: row + width: chilrenListView.width + property var childAttrib: object + + Component.onCompleted: { + var cpt = Qt.createComponent("AttributeItemDelegate.qml") + var obj = cpt.createObject(row, + {'attribute': Qt.binding(function() { return row.childAttrib }), + 'readOnly': Qt.binding(function() { return root.readOnly }) + }) + obj.Layout.fillWidth = true + obj.labelWidth = 100 // reduce label width for children (space gain) + obj.doubleClicked.connect(function(attr) {root.doubleClicked(attr)}) + } } } } diff --git a/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml b/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml new file mode 100644 index 0000000000..4255ceb7e3 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml @@ -0,0 +1,11 @@ +pragma Singleton +import Qt.labs.settings 1.0 + + +/** + * Persistent Settings related to the GraphEditor module. + */ +Settings { + category: 'GraphEditor' + property bool showAdvancedAttributes: false +} diff --git a/meshroom/ui/qml/GraphEditor/qmldir b/meshroom/ui/qml/GraphEditor/qmldir index 615d463580..22cbf96a5a 100644 --- a/meshroom/ui/qml/GraphEditor/qmldir +++ b/meshroom/ui/qml/GraphEditor/qmldir @@ -9,3 +9,4 @@ AttributeEditor 1.0 AttributeEditor.qml AttributeItemDelegate 1.0 AttributeItemDelegate.qml CompatibilityBadge 1.0 CompatibilityBadge.qml CompatibilityManager 1.0 CompatibilityManager.qml +singleton GraphEditorSettings 1.0 GraphEditorSettings.qml From dc8be1efaef75c3012a631909c64f2c3878ff9e8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 14 Dec 2018 15:45:24 +0100 Subject: [PATCH 083/293] [ui] Attribute: display icon on advanced parameters --- .../qml/GraphEditor/AttributeItemDelegate.qml | 133 ++++++++++-------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 4168acff1d..d5fa11ae76 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -20,82 +20,101 @@ RowLayout { signal doubleClicked(var attr) - spacing: 4 + spacing: 2 - Label { - id: parameterLabel + Pane { + background: Rectangle { color: Qt.darker(parent.palette.window, 1.1) } + padding: 0 Layout.preferredWidth: labelWidth || implicitWidth Layout.fillHeight: true - horizontalAlignment: attribute.isOutput ? Qt.AlignRight : Qt.AlignLeft - elide: Label.ElideRight - padding: 5 - wrapMode: Label.WrapAtWordBoundaryOrAnywhere - text: attribute.label + RowLayout { + spacing: 0 + width: parent.width + height: parent.height + Label { + id: parameterLabel - // Tooltip hint with attribute's description - ToolTip.text: object.desc.description - ToolTip.visible: parameterMA.containsMouse && object.desc.description - ToolTip.delay: 800 + Layout.fillHeight: true + Layout.fillWidth: true + horizontalAlignment: attribute.isOutput ? Qt.AlignRight : Qt.AlignLeft + elide: Label.ElideRight + padding: 5 + wrapMode: Label.WrapAtWordBoundaryOrAnywhere - // make label bold if attribute's value is not the default one - font.bold: !object.isOutput && !object.isDefault + text: attribute.label - // make label italic if attribute is a link - font.italic: object.isLink + // Tooltip hint with attribute's description + ToolTip.text: object.desc.description + ToolTip.visible: parameterMA.containsMouse && object.desc.description + ToolTip.delay: 800 - background: Rectangle { color: Qt.darker(parent.palette.window, 1.2) } + // make label bold if attribute's value is not the default one + font.bold: !object.isOutput && !object.isDefault - MouseArea { - id: parameterMA - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.AllButtons - onDoubleClicked: root.doubleClicked(root.attribute) + // make label italic if attribute is a link + font.italic: object.isLink - property Component menuComp: Menu { - id: paramMenu - property bool isFileAttribute: attribute.type == "File" - property bool isFilepath: isFileAttribute && Filepath.isFile(attribute.value) + MouseArea { + id: parameterMA + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.AllButtons + onDoubleClicked: root.doubleClicked(root.attribute) - MenuItem { - text: "Reset To Default Value" - enabled: root.editable && !attribute.isDefault - onTriggered: _reconstruction.resetAttribute(attribute) - } + property Component menuComp: Menu { + id: paramMenu - MenuSeparator { - visible: paramMenu.isFileAttribute - height: visible ? implicitHeight : 0 - } + property bool isFileAttribute: attribute.type == "File" + property bool isFilepath: isFileAttribute && Filepath.isFile(attribute.value) - MenuItem { - visible: paramMenu.isFileAttribute - height: visible ? implicitHeight : 0 - text: paramMenu.isFilepath ? "Open Containing Folder" : "Open Folder" - onClicked: paramMenu.isFilepath ? Qt.openUrlExternally(Filepath.dirname(attribute.value)) : - Qt.openUrlExternally(Filepath.stringToUrl(attribute.value)) - } + MenuItem { + text: "Reset To Default Value" + enabled: root.editable && !attribute.isDefault + onTriggered: _reconstruction.resetAttribute(attribute) + } - MenuItem { - visible: paramMenu.isFilepath - height: visible ? implicitHeight : 0 - text: "Open File" - onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.value)) - } - } + MenuSeparator { + visible: paramMenu.isFileAttribute + height: visible ? implicitHeight : 0 + } - onClicked: { - forceActiveFocus() - if(mouse.button == Qt.RightButton) - { - var menu = menuComp.createObject(parameterLabel) - menu.parent = parameterLabel - menu.popup() + MenuItem { + visible: paramMenu.isFileAttribute + height: visible ? implicitHeight : 0 + text: paramMenu.isFilepath ? "Open Containing Folder" : "Open Folder" + onClicked: paramMenu.isFilepath ? Qt.openUrlExternally(Filepath.dirname(attribute.value)) : + Qt.openUrlExternally(Filepath.stringToUrl(attribute.value)) + } + + MenuItem { + visible: paramMenu.isFilepath + height: visible ? implicitHeight : 0 + text: "Open File" + onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.value)) + } + } + + onClicked: { + forceActiveFocus() + if(mouse.button == Qt.RightButton) + { + var menu = menuComp.createObject(parameterLabel) + menu.parent = parameterLabel + menu.popup() + } + } } } + MaterialLabel { + visible: attribute.desc.advanced + text: MaterialIcons.build + color: palette.mid + font.pointSize: 8 + padding: 4 + } } } From b6e4876494af97820f2e11697c076741885c666a Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 13 Dec 2018 20:20:30 +0100 Subject: [PATCH 084/293] [ui] move Panel to Controls module --- meshroom/ui/qml/{ => Controls}/Panel.qml | 0 meshroom/ui/qml/Controls/qmldir | 1 + meshroom/ui/qml/ImageGallery.qml | 1 + meshroom/ui/qml/LiveSfmView.qml | 2 ++ meshroom/ui/qml/WorkspaceView.qml | 1 + 5 files changed, 5 insertions(+) rename meshroom/ui/qml/{ => Controls}/Panel.qml (100%) diff --git a/meshroom/ui/qml/Panel.qml b/meshroom/ui/qml/Controls/Panel.qml similarity index 100% rename from meshroom/ui/qml/Panel.qml rename to meshroom/ui/qml/Controls/Panel.qml diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 354a1604e9..aac7ce36de 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -3,3 +3,4 @@ module Controls FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml +Panel 1.0 Panel.qml diff --git a/meshroom/ui/qml/ImageGallery.qml b/meshroom/ui/qml/ImageGallery.qml index e70fd59be6..11dc855798 100644 --- a/meshroom/ui/qml/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts 1.3 import MaterialIcons 2.2 import QtQml.Models 2.2 +import Controls 1.0 import Utils 1.0 /** diff --git a/meshroom/ui/qml/LiveSfmView.qml b/meshroom/ui/qml/LiveSfmView.qml index bff9d49791..b8190de99a 100644 --- a/meshroom/ui/qml/LiveSfmView.qml +++ b/meshroom/ui/qml/LiveSfmView.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts 1.3 import MaterialIcons 2.2 import Qt.labs.platform 1.0 as Platform // for FileDialog +import Controls 1.0 + /** * LiveSfMView provides controls for setting up and starting a live reconstruction. */ diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 056165fd95..661b15e0df 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -6,6 +6,7 @@ import Qt.labs.platform 1.0 as Platform import Viewer 1.0 import Viewer3D 1.0 import MaterialIcons 2.2 +import Controls 1.0 import Utils 1.0 From 00feb466675c13622867795037310fe2ab2a905c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 14 Dec 2018 16:02:04 +0100 Subject: [PATCH 085/293] [ui] extract NodeEditor from AttributeEditor * NodeEditor * exposes Node parameters: compatibility, attributes and logs * provides a placeholder when no active Node * AttributeEditor * only displays the list of Attributes * use 'AttributeEditor' for GroupAttributes in AttributeItemDelegate * Layout * move NodeEditor on the same SplitView level as GraphEditor * move current node name and menu to the Panel's header --- .../ui/qml/GraphEditor/AttributeEditor.qml | 166 ++++-------------- .../qml/GraphEditor/AttributeItemDelegate.qml | 42 ++--- meshroom/ui/qml/GraphEditor/NodeEditor.qml | 155 ++++++++++++++++ meshroom/ui/qml/GraphEditor/qmldir | 1 + meshroom/ui/qml/main.qml | 115 +++++------- 5 files changed, 246 insertions(+), 233 deletions(-) create mode 100644 meshroom/ui/qml/GraphEditor/NodeEditor.qml diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index 59f23689ce..7b5e89981a 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -5,156 +5,52 @@ import MaterialIcons 2.2 import Utils 1.0 /** - A component to display and edit a Node's attributes. -*/ -ColumnLayout { - id: root + * A component to display and edit the attributes of a Node. + */ - property variant node: null // the node to edit +ListView { + id: root + property variant attributes: null property bool readOnly: false - readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue") + property int labelWidth: 180 signal upgradeRequest() - signal attributeDoubleClicked(var attribute) - spacing: 0 + implicitHeight: contentHeight - Pane { - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(parent.palette.window, 1.15) } - padding: 2 + clip: true + spacing: 2 + ScrollBar.vertical: ScrollBar { id: scrollBar } - RowLayout { - width: parent.width + model: SortFilterDelegateModel { - Label { - Layout.fillWidth: true - elide: Text.ElideMiddle - text: node.label - horizontalAlignment: Text.AlignHCenter - padding: 6 - } + model: attributes + filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" + filterValue: false - ToolButton { - text: MaterialIcons.settings - font.family: MaterialIcons.fontFamily - onClicked: settingsMenu.popup() - checkable: true - checked: settingsMenu.visible - } + function modelData(item, roleName) { + return item.model.object.desc[roleName] } - Menu { - id: settingsMenu - MenuItem { - text: "Advanced Attributes" - checked: GraphEditorSettings.showAdvancedAttributes - onClicked: GraphEditorSettings.showAdvancedAttributes = !GraphEditorSettings.showAdvancedAttributes - } - MenuItem { - text: "Open Cache Folder" - onClicked: Qt.openUrlExternally(Filepath.stringToUrl(node.internalFolder)) - ToolTip.text: node.internalFolder - ToolTip.visible: hovered - ToolTip.delay: 500 - } - MenuSeparator {} - MenuItem { - text: "Clear Submitted Status" - onClicked: node.clearSubmittedChunks() - } - } - } - - // CompatibilityBadge banner for CompatibilityNode - Loader { - active: isCompatibilityNode - Layout.fillWidth: true - visible: active // for layout update - - sourceComponent: CompatibilityBadge { - canUpgrade: root.node.canUpgrade - issueDetails: root.node.issueDetails - onUpgradeRequest: root.upgradeRequest() - sourceComponent: bannerDelegate - } - } - - StackLayout { - Layout.fillHeight: true - Layout.fillWidth: true - currentIndex: tabBar.currentIndex - - Item { - - ListView { - id: attributesListView - - anchors.fill: parent - anchors.margins: 2 - clip: true - spacing: 2 - ScrollBar.vertical: ScrollBar { id: scrollBar } - - model: SortFilterDelegateModel { - - model: node ? node.attributes : null - filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" - filterValue: false - function modelData(item, roleName) { - return item.model.object.desc[roleName] - } - - Component { - id: delegateComponent - AttributeItemDelegate { - width: attributesListView.width - scrollBar.width - readOnly: root.isCompatibilityNode - labelWidth: 180 - attribute: object - onDoubleClicked: root.attributeDoubleClicked(attr) - } - } - } - - // Helper MouseArea to lose edit/activeFocus - // when clicking on the background - MouseArea { - anchors.fill: parent - onClicked: root.forceActiveFocus() - z: -1 - } + Component { + id: delegateComponent + AttributeItemDelegate { + width: ListView.view.width - scrollBar.width + readOnly: root.readOnly + labelWidth: root.labelWidth + attribute: object + onDoubleClicked: root.attributeDoubleClicked(attr) } } - - NodeLog { - id: nodeLog - - Layout.fillHeight: true - Layout.fillWidth: true - node: root.node - - } } - TabBar { - id: tabBar - Layout.fillWidth: true - width: childrenRect.width - position: TabBar.Footer - TabButton { - text: "Attributes" - width: implicitWidth - padding: 4 - leftPadding: 8 - rightPadding: leftPadding - } - TabButton { - text: "Log" - width: implicitWidth - leftPadding: 8 - rightPadding: leftPadding - } + // Helper MouseArea to lose edit/activeFocus + // when clicking on the background + MouseArea { + anchors.fill: parent + onClicked: root.forceActiveFocus() + z: -1 } } + diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index d5fa11ae76..63431b6775 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -348,37 +348,17 @@ RowLayout { Component { id: groupAttribute_component - ListView { - id: chilrenListView - implicitWidth: parent.width - implicitHeight: childrenRect.height - onCountChanged: forceLayout() - spacing: 2 - model: SortFilterDelegateModel { - model: attribute.value - filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" - filterValue: false - - function modelData(item, roleName) { - return item.model.object.desc[roleName] - } - - delegate: RowLayout { - id: row - width: chilrenListView.width - property var childAttrib: object - - Component.onCompleted: { - var cpt = Qt.createComponent("AttributeItemDelegate.qml") - var obj = cpt.createObject(row, - {'attribute': Qt.binding(function() { return row.childAttrib }), - 'readOnly': Qt.binding(function() { return root.readOnly }) - }) - obj.Layout.fillWidth = true - obj.labelWidth = 100 // reduce label width for children (space gain) - obj.doubleClicked.connect(function(attr) {root.doubleClicked(attr)}) - } - } + ColumnLayout { + id: groupItem + Component.onCompleted: { + var cpt = Qt.createComponent("AttributeEditor.qml"); + var obj = cpt.createObject(groupItem, + {'attributes': Qt.binding(function() { return attribute.value }), + 'readOnly': Qt.binding(function() { return root.readOnly }), + 'labelWidth': 100, // reduce label width for children (space gain) + }) + obj.Layout.fillWidth = true; + obj.attributeDoubleClicked.connect(function(attr) {root.doubleClicked(attr)}) } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml new file mode 100644 index 0000000000..1be97acd03 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -0,0 +1,155 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import Controls 1.0 + + +/** + * NodeEditor allows to visualize and edit the parameters of a Node. + * It mainly provides an attribute editor and a log inspector. + */ +Panel { + id: root + + property variant node + property bool readOnly: false + property bool isCompatibilityNode: node && node.compatibilityIssue !== undefined + + signal attributeDoubleClicked(var attribute) + signal upgradeRequest() + + title: "Node" + (node !== null ? " - " + node.label + "" : "") + icon: MaterialLabel { text: MaterialIcons.tune } + + headerBar: RowLayout { + MaterialToolButton { + text: MaterialIcons.more_vert + font.pointSize: 11 + padding: 2 + onClicked: settingsMenu.open() + checkable: true + checked: settingsMenu.visible + Menu { + id: settingsMenu + y: parent.height + MenuItem { + id: advancedToggle + text: "Advanced Attributes" + MaterialLabel { + anchors.right: parent.right; anchors.rightMargin: parent.padding; + text: MaterialIcons.build + anchors.verticalCenter: parent.verticalCenter + font.pointSize: 8 + } + checkable: true + checked: GraphEditorSettings.showAdvancedAttributes + onClicked: GraphEditorSettings.showAdvancedAttributes = !GraphEditorSettings.showAdvancedAttributes + } + MenuItem { + text: "Open Cache Folder" + enabled: root.node !== null + onClicked: Qt.openUrlExternally(Filepath.stringToUrl(root.node.internalFolder)) + } + MenuSeparator {} + MenuItem { + enabled: root.node !== null + text: "Clear Submitted Status" + onClicked: node.clearSubmittedChunks() + } + } + } + } + ColumnLayout { + anchors.fill: parent + + // CompatibilityBadge banner for CompatibilityNode + Loader { + active: root.isCompatibilityNode + Layout.fillWidth: true + visible: active // for layout update + + sourceComponent: CompatibilityBadge { + canUpgrade: root.node.canUpgrade + issueDetails: root.node.issueDetails + onUpgradeRequest: root.upgradeRequest() + sourceComponent: bannerDelegate + } + } + + Loader { + Layout.fillHeight: true + Layout.fillWidth: true + sourceComponent: root.node ? editor_component : placeholder_component + + Component { + id: placeholder_component + + Item { + Column { + anchors.centerIn: parent + MaterialLabel { + text: MaterialIcons.select_all + font.pointSize: 34 + color: Qt.lighter(palette.mid, 1.2) + anchors.horizontalCenter: parent.horizontalCenter + } + Label { + color: Qt.lighter(palette.mid, 1.2) + text: "Select a Node to edit its Attributes" + } + } + } + } + + Component { + id: editor_component + + ColumnLayout { + StackLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + currentIndex: tabBar.currentIndex + + AttributeEditor { + Layout.fillWidth: true + attributes: root.node.attributes + readOnly: root.isCompatibilityNode + onAttributeDoubleClicked: root.attributeDoubleClicked(attribute) + onUpgradeRequest: root.upgradeRequest() + } + + NodeLog { + id: nodeLog + + Layout.fillHeight: true + Layout.fillWidth: true + node: root.node + } + } + TabBar { + id: tabBar + + Layout.fillWidth: true + width: childrenRect.width + position: TabBar.Footer + TabButton { + text: "Attributes" + width: implicitWidth + padding: 4 + leftPadding: 8 + rightPadding: leftPadding + } + TabButton { + text: "Log" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } + } + } + } + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/qmldir b/meshroom/ui/qml/GraphEditor/qmldir index 22cbf96a5a..2d1e657476 100644 --- a/meshroom/ui/qml/GraphEditor/qmldir +++ b/meshroom/ui/qml/GraphEditor/qmldir @@ -1,6 +1,7 @@ module GraphEditor GraphEditor 1.0 GraphEditor.qml +NodeEditor 1.0 NodeEditor.qml Node 1.0 Node.qml NodeChunks 1.0 NodeChunks.qml Edge 1.0 Edge.qml diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 5502cb56df..dc67a89f7d 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -507,84 +507,65 @@ ApplicationWindow { } } - Panel { - id: graphEditorPanel - Layout.fillWidth: true - Layout.fillHeight: false - padding: 0 + Controls1.SplitView { + orientation: Qt.Horizontal + width: parent.width height: Math.round(parent.height * 0.3) - title: "Graph Editor" visible: settings_UILayout.showGraphEditor - function displayAttribute(attr) { - if( attr.desc.type === "File" - && _3dFileExtensions.indexOf(Filepath.extension(attr.value)) > - 1 ) - { - workspaceView.viewAttribute(attr); - return true; - } - return false; - } - - Controls1.SplitView { - orientation: Qt.Horizontal - anchors.fill: parent - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.margins: 2 + Panel { + id: graphEditorPanel + Layout.fillWidth: true + padding: 4 + title: "Graph Editor" + visible: settings_UILayout.showGraphEditor + + function displayAttribute(attr) { + if( attr.desc.type === "File" + && _3dFileExtensions.indexOf(Filepath.extension(attr.value)) > - 1 ) + { + workspaceView.viewAttribute(attr); + return true; + } + return false; + } - GraphEditor { - id: graphEditor + GraphEditor { + id: graphEditor - anchors.fill: parent - uigraph: _reconstruction - nodeTypesModel: _nodeTypes - readOnly: _reconstruction.computing + anchors.fill: parent + uigraph: _reconstruction + nodeTypesModel: _nodeTypes + readOnly: _reconstruction.computing - onNodeDoubleClicked: { - if(node.nodeType === "StructureFromMotion") - { - _reconstruction.sfm = node - return - } - for(var i=0; i < node.attributes.count; ++i) + onNodeDoubleClicked: { + if(node.nodeType === "StructureFromMotion") + { + _reconstruction.sfm = node + return + } + for(var i=0; i < node.attributes.count; ++i) + { + var attr = node.attributes.at(i) + if(attr.isOutput + && graphEditorPanel.displayAttribute(attr)) { - var attr = node.attributes.at(i) - if(attr.isOutput - && graphEditorPanel.displayAttribute(attr)) - { - break; - } + break; } } } } - Item { - implicitHeight: Math.round(parent.height * 0.2) - implicitWidth: Math.round(parent.width * 0.3) - - Loader { - anchors.fill: parent - anchors.margins: 2 - active: _reconstruction.selectedNode !== null - sourceComponent: Component { - AttributeEditor { - node: _reconstruction.selectedNode - // Make AttributeEditor readOnly when computing - readOnly: _reconstruction.computing - onAttributeDoubleClicked: { - graphEditorPanel.displayAttribute(attribute) - } - - onUpgradeRequest: { - var n = _reconstruction.upgradeNode(node) - _reconstruction.selectedNode = n; - } - } - } - } + } + + NodeEditor { + width: Math.round(parent.width * 0.3) + node: _reconstruction.selectedNode + // Make NodeEditor readOnly when computing + readOnly: _reconstruction.computing + onAttributeDoubleClicked: graphEditorPanel.displayAttribute(attribute) + onUpgradeRequest: { + var n = _reconstruction.upgradeNode(node); + _reconstruction.selectedNode = n; } } } From e7b49f31c7d7788be904bee6ac5450071f9f7968 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 14 Dec 2018 16:13:52 +0100 Subject: [PATCH 086/293] [ui][GraphEditor] use 'C' locale for FloatParam validator + minor fixes * using 'C' locale to ensure floating point values can be written using '.' decimal separator * make TextField larger by default for numbers and fill width when slider is not defined * avoid binding loop on height for ListAttributes by using contentHeight as implicitHeight * add explicitly id for Connections in ComboBox --- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 63431b6775..2ecc170205 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -184,13 +184,14 @@ RowLayout { Component { id: comboBox_component ComboBox { + id: combo enabled: root.editable model: attribute.desc.values Component.onCompleted: currentIndex = find(attribute.value) onActivated: _reconstruction.setAttribute(attribute, currentText) Connections { target: attribute - onValueChanged: currentIndex = find(attribute.value) + onValueChanged: combo.currentIndex = combo.find(attribute.value) } } } @@ -225,8 +226,10 @@ RowLayout { } DoubleValidator { id: doubleValidator + locale: 'C' // use '.' decimal separator disregarding the system locale } - implicitWidth: 70 + implicitWidth: 100 + Layout.fillWidth: !slider.active enabled: root.editable // cast value to string to avoid intrusive scientific notations on numbers property string displayValue: String(slider.active && slider.item.pressed ? slider.item.formattedValue : attribute.value) @@ -306,7 +309,7 @@ RowLayout { id: lv model: listAttribute_layout.expanded ? attribute.value : undefined visible: model != undefined && count > 0 - implicitHeight: Math.min(childrenRect.height, 300) + implicitHeight: Math.min(contentHeight, 300) Layout.fillWidth: true Layout.margins: 4 clip: true From 6ac4a9d712de69e38b9070b3cc814798248ee94a Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 2 Jan 2019 14:38:28 +0100 Subject: [PATCH 087/293] [desc][qt] use QVariantList for list-type properties * required for PySide2 > 5.11.1 (and compatible with 5.11.1) * AttributeItemDelegate: test for list length to determine whether to create a slider component (if range is set to None on Python side, it will be an empty list on the QML/JS side) --- meshroom/common/__init__.py | 5 +++-- meshroom/common/core.py | 1 + meshroom/common/qt.py | 1 + meshroom/core/desc.py | 8 ++++---- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/meshroom/common/__init__.py b/meshroom/common/__init__.py index 533b77179f..8607447b13 100644 --- a/meshroom/common/__init__.py +++ b/meshroom/common/__init__.py @@ -7,13 +7,14 @@ Property = None BaseObject = None Variant = None +VariantList = None if meshroom.backend == meshroom.Backend.PYSIDE: # PySide types - from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant + from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList elif meshroom.backend == meshroom.Backend.STANDALONE: # Core types - from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant + from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList class _BaseModel: diff --git a/meshroom/common/core.py b/meshroom/common/core.py index d56ea5ff68..54ba9cbef9 100644 --- a/meshroom/common/core.py +++ b/meshroom/common/core.py @@ -145,3 +145,4 @@ def parent(self): Property = CoreProperty BaseObject = CoreObject Variant = object +VariantList = object diff --git a/meshroom/common/qt.py b/meshroom/common/qt.py index 73b1440bc1..cb1087d5d1 100644 --- a/meshroom/common/qt.py +++ b/meshroom/common/qt.py @@ -373,3 +373,4 @@ def sort(self): Property = QtCore.Property BaseObject = QtCore.QObject Variant = "QVariant" +VariantList = "QVariantList" diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 9ec5f4bbcf..5a349c9ea7 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -1,4 +1,4 @@ -from meshroom.common import BaseObject, Property, Variant +from meshroom.common import BaseObject, Property, Variant, VariantList from meshroom.core import pyCompatibility from enum import Enum # available by default in python3. For python2: "pip install enum34" import collections @@ -129,7 +129,7 @@ def validateValue(self, value): except: raise ValueError('IntParam only supports int value (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) - range = Property(Variant, lambda self: self._range, constant=True) + range = Property(VariantList, lambda self: self._range, constant=True) class FloatParam(Param): @@ -145,7 +145,7 @@ def validateValue(self, value): except: raise ValueError('FloatParam only supports float value (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) - range = Property(Variant, lambda self: self._range, constant=True) + range = Property(VariantList, lambda self: self._range, constant=True) class ChoiceParam(Param): @@ -174,7 +174,7 @@ def validateValue(self, value): raise ValueError('Non exclusive ChoiceParam value should be iterable (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) return [self.conformValue(v) for v in value] - values = Property(Variant, lambda self: self._values, constant=True) + values = Property(VariantList, lambda self: self._values, constant=True) exclusive = Property(bool, lambda self: self._exclusive, constant=True) joinChar = Property(str, lambda self: self._joinChar, constant=True) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 2ecc170205..d98f24c732 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -247,7 +247,7 @@ RowLayout { Loader { id: slider Layout.fillWidth: true - active: attribute.desc.range != undefined + active: attribute.desc.range.length === 3 sourceComponent: Slider { readonly property int stepDecimalCount: stepSize < 1 ? String(stepSize).split(".").pop().length : 0 readonly property real formattedValue: value.toFixed(stepDecimalCount) From ed14294a29b9fd1983bc3518d80202289618908e Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 2 Jan 2019 16:12:59 +0100 Subject: [PATCH 088/293] [nodes][aliceVision] remove range sliders where unnecessary --- meshroom/nodes/aliceVision/CameraInit.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 48f59db445..dec785297a 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -10,19 +10,19 @@ Viewpoint = [ - desc.IntParam(name="viewId", label="Id", description="Image UID", value=-1, uid=[0], range=(0, 200, 1)), - desc.IntParam(name="poseId", label="Pose Id", description="Pose Id", value=-1, uid=[0], range=(0, 200, 1)), + desc.IntParam(name="viewId", label="Id", description="Image UID", value=-1, uid=[0], range=None), + desc.IntParam(name="poseId", label="Pose Id", description="Pose Id", value=-1, uid=[0], range=None), desc.File(name="path", label="Image Path", description="Image Filepath", value="", uid=[0]), - desc.IntParam(name="intrinsicId", label="Intrinsic", description="Internal Camera Parameters", value=-1, uid=[0], range=(0, 200, 1)), - desc.IntParam(name="rigId", label="Rig", description="Rig Parameters", value=-1, uid=[0], range=(0, 200, 1)), - desc.IntParam(name="subPoseId", label="Rig Sub-Pose", description="Rig Sub-Pose Parameters", value=-1, uid=[0], range=(0, 200, 1)), - desc.StringParam(name="metadata", label="Image Metadata", description="", value="", uid=[]), + 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), ] Intrinsic = [ - desc.IntParam(name="intrinsicId", label="Id", description="Intrinsic UID", value=-1, uid=[0], range=(0, 200, 1)), - desc.FloatParam(name="pxInitialFocalLength", label="Initial Focal Length", description="Initial Guess on the Focal Length", value=-1.0, uid=[0], range=(0.0, 200.0, 1.0)), - desc.FloatParam(name="pxFocalLength", label="Focal Length", description="Known/Calibrated Focal Length", value=-1.0, uid=[0], range=(0.0, 200.0, 1.0)), + 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.StringParam(name="deviceMake", label="Make", description="Camera Make", value="", uid=[]), # desc.StringParam(name="deviceModel", label="Model", description="Camera Model", value="", uid=[]), From 5a38295184fef396488538920d102ca3d58f15cb Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 2 Jan 2019 16:16:17 +0100 Subject: [PATCH 089/293] [ui][AttributeEditor] display attribute technical name in tooltip + convert attribute description from plain text to html --- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 4 ++-- meshroom/ui/qml/Utils/format.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index d98f24c732..fc4473456a 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -46,8 +46,8 @@ RowLayout { text: attribute.label // Tooltip hint with attribute's description - ToolTip.text: object.desc.description - ToolTip.visible: parameterMA.containsMouse && object.desc.description + ToolTip.text: "" + object.desc.name + "
" + Format.plainToHtml(object.desc.description) + ToolTip.visible: parameterMA.containsMouse ToolTip.delay: 800 // make label bold if attribute's value is not the default one diff --git a/meshroom/ui/qml/Utils/format.js b/meshroom/ui/qml/Utils/format.js index 5827ea3b69..72b732e08d 100644 --- a/meshroom/ui/qml/Utils/format.js +++ b/meshroom/ui/qml/Utils/format.js @@ -7,3 +7,9 @@ function intToString(v) { // (this 'toLocaleString' does not take any option) return v.toLocaleString(Qt.locale('en-US')).split('.')[0] } + +// Convert a plain text to an html escaped string. +function plainToHtml(t) { + var escaped = t.replace(/&/g, '&').replace(//g, '>'); // escape text + return escaped.replace(/\n/g, '
'); // replace line breaks +} From b50f9fb44da77494b0c1199be388046448a59e81 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 2 Jan 2019 18:34:25 +0100 Subject: [PATCH 090/293] [ui] GraphEditor: only display "Submit" in node menu if available --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 3548b52997..67eb2dd352 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -264,6 +264,8 @@ Item { MenuItem { text: "Submit" enabled: !root.readOnly && nodeMenu.canComputeNode + visible: uigraph.canSubmit + height: visible ? implicitHeight : 0 onTriggered: uigraph.submit(nodeMenu.currentNode) } MenuItem { From d10c779914a9a900139f417ff3b9a29b8ca1d17f Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 2 Jan 2019 18:44:11 +0100 Subject: [PATCH 091/293] [ui] GraphEditor: add "Delete From Here" in node contextual menu * ease the deletion of a branch from a given starting point * accessible with alt+del on a node * re-order menu to put the most destructive operation (Clear Data) at the very end --- meshroom/ui/graph.py | 12 ++++++++++ meshroom/ui/qml/GraphEditor/GraphEditor.qml | 25 ++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 60fbacf0c0..00470e1c1b 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -390,6 +390,18 @@ def moveNode(self, node, position): def removeNode(self, node): self.push(commands.RemoveNodeCommand(self._graph, node)) + @Slot(Node) + def removeNodesFrom(self, startNode): + """ + Remove all nodes starting from 'startNode' to graph leaves. + Args: + startNode (Node): the node to start from. + """ + with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)): + # Perform nodes removal from leaves to start node so that edges + # can be re-created in correct order on redo. + [self.removeNode(node) for node in reversed(self._graph.nodesFromNode(startNode)[0])] + @Slot(Attribute, Attribute) def addEdge(self, src, dst): if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute): diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 67eb2dd352..50d5bbe4ac 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import Controls 1.0 +import Utils 1.0 import MaterialIcons 2.2 /** @@ -274,7 +275,7 @@ Item { } MenuSeparator {} MenuItem { - text: "Duplicate" + text: "Duplicate Node" onTriggered: duplicateNode(nodeMenu.currentNode, false) } MenuItem { @@ -283,14 +284,21 @@ Item { } MenuSeparator {} MenuItem { - text: "Clear Data" + text: "Delete Node" enabled: !root.readOnly - onTriggered: nodeMenu.currentNode.clearData() + onTriggered: uigraph.removeNode(nodeMenu.currentNode) } MenuItem { - text: "Delete Node" + text: "Delete From Here" enabled: !root.readOnly - onTriggered: uigraph.removeNode(nodeMenu.currentNode) + onTriggered: uigraph.removeNodesFrom(nodeMenu.currentNode) + } + MenuSeparator {} + MenuItem { + text: "Clear Data" + palette.text: Colors.red + enabled: !root.readOnly + onTriggered: nodeMenu.currentNode.clearData() } } @@ -337,7 +345,12 @@ Item { onEntered: uigraph.hoveredNode = node onExited: uigraph.hoveredNode = null - Keys.onDeletePressed: uigraph.removeNode(node) + Keys.onDeletePressed: { + if(event.modifiers == Qt.AltModifier) + uigraph.removeNodesFrom(node) + else + uigraph.removeNode(node) + } Behavior on x { enabled: animatePosition From d1bf04bdf527b37a2a9b7ddf5c0b7a4087971f24 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 4 Jan 2019 10:28:46 +0100 Subject: [PATCH 092/293] [ui] Node: gather single and "From Here" actions in the same menu entry * add a ToolButton for duplicating/removing following nodes in corresponding entries instead of having a separate item * change text when those button are hovered to reflect the change of action * wording: rename "Delete Node" to "Remove Node" --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 34 +++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 50d5bbe4ac..ea148dd003 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -275,23 +275,33 @@ Item { } MenuSeparator {} MenuItem { - text: "Duplicate Node" + text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "") onTriggered: duplicateNode(nodeMenu.currentNode, false) + MaterialToolButton { + id: duplicateFollowingButton + height: parent.height + anchors { right: parent.right; rightMargin: parent.padding } + text: MaterialIcons.fast_forward + onClicked: { + duplicateNode(nodeMenu.currentNode, true); + nodeMenu.close(); + } + } } MenuItem { - text: "Duplicate From Here" - onTriggered: duplicateNode(nodeMenu.currentNode, true) - } - MenuSeparator {} - MenuItem { - text: "Delete Node" + text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "") enabled: !root.readOnly onTriggered: uigraph.removeNode(nodeMenu.currentNode) - } - MenuItem { - text: "Delete From Here" - enabled: !root.readOnly - onTriggered: uigraph.removeNodesFrom(nodeMenu.currentNode) + MaterialToolButton { + id: removeFollowingButton + height: parent.height + anchors { right: parent.right; rightMargin: parent.padding } + text: MaterialIcons.fast_forward + onClicked: { + uigraph.removeNodesFrom(nodeMenu.currentNode); + nodeMenu.close(); + } + } } MenuSeparator {} MenuItem { From b09068dc68713ce416c468e1bc35cf91e2d84ae0 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 4 Jan 2019 10:31:42 +0100 Subject: [PATCH 093/293] [ui] Node: add "Clear Data From Here" menu entry + confirmation dialog --- meshroom/core/graph.py | 5 +++ meshroom/ui/qml/GraphEditor/GraphEditor.qml | 49 +++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 72722584d2..a9b9438421 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1004,6 +1004,11 @@ def clearSubmittedNodes(self): for node in self.nodes: node.clearSubmittedChunks() + @Slot(Node) + def clearDataFrom(self, startNode): + for node in self.nodesFromNode(startNode)[0]: + node.clearData() + def iterChunksByStatus(self, status): """ Iterate over NodeChunks with the given status """ for node in self.nodes: diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index ea148dd003..3ca4af8065 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -305,10 +305,53 @@ Item { } MenuSeparator {} MenuItem { - text: "Clear Data" - palette.text: Colors.red + text: "Delete Data" + (deleteFollowingButton.hovered ? " From Here" : "" ) + "..." enabled: !root.readOnly - onTriggered: nodeMenu.currentNode.clearData() + + function showConfirmationDialog(deleteFollowing) { + var obj = deleteDataDialog.createObject(root, + { + "node": nodeMenu.currentNode, + "deleteFollowing": deleteFollowing + }); + obj.open() + nodeMenu.close(); + } + + onTriggered: showConfirmationDialog(false) + + MaterialToolButton { + id: deleteFollowingButton + anchors { right: parent.right; rightMargin: parent.padding } + height: parent.height + text: MaterialIcons.fast_forward + onClicked: parent.showConfirmationDialog(true) + } + + // Confirmation dialog for node cache deletion + Component { + id: deleteDataDialog + MessageDialog { + property var node + property bool deleteFollowing: false + + focus: true + modal: false + header.visible: false + + text: "Delete Data computed by '" + node.label + (deleteFollowing ? "' and following Nodes?" : "'?") + helperText: "Warning: This operation can not be undone." + standardButtons: Dialog.Yes | Dialog.Cancel + + onAccepted: { + if(deleteFollowing) + graph.clearDataFrom(node); + else + node.clearData(); + } + onClosed: destroy() + } + } } } From 34ca6072618ee90d4f0c3821c6eb7a3909f26f55 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 4 Jan 2019 18:06:35 +0100 Subject: [PATCH 094/293] [ui] GraphEditor : add "Clear Pending Status" tool at Graph level * allow to clear status of all submitted nodes in the graph * unify wording --- meshroom/core/graph.py | 1 + meshroom/ui/qml/GraphEditor/NodeEditor.qml | 4 ++-- meshroom/ui/qml/main.qml | 20 +++++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index a9b9438421..defc1e95f0 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -999,6 +999,7 @@ def stopExecution(self): for chunk in self.iterChunksByStatus(Status.RUNNING): chunk.stopProcess() + @Slot() def clearSubmittedNodes(self): """ Reset the status of already submitted nodes to Status.NONE """ for node in self.nodes: diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 1be97acd03..dd099905b6 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -54,7 +54,7 @@ Panel { MenuSeparator {} MenuItem { enabled: root.node !== null - text: "Clear Submitted Status" + text: "Clear Pending Status" onClicked: node.clearSubmittedChunks() } } @@ -115,7 +115,7 @@ Panel { AttributeEditor { Layout.fillWidth: true attributes: root.node.attributes - readOnly: root.isCompatibilityNode + readOnly: root.readOnly || root.isCompatibilityNode onAttributeDoubleClicked: root.attributeDoubleClicked(attribute) onUpgradeRequest: root.upgradeRequest() } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index dc67a89f7d..d8cc2e1bd3 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -518,7 +518,25 @@ ApplicationWindow { Layout.fillWidth: true padding: 4 title: "Graph Editor" - visible: settings_UILayout.showGraphEditor + + headerBar: RowLayout { + MaterialToolButton { + text: MaterialIcons.more_vert + font.pointSize: 11 + padding: 2 + onClicked: graphEditorMenu.open() + Menu { + id: graphEditorMenu + y: parent.height + x: -width + parent.width + MenuItem { + text: "Clear Pending Status" + enabled: !_reconstruction.computingLocally + onTriggered: _reconstruction.graph.clearSubmittedNodes() + } + } + } + } function displayAttribute(attr) { if( attr.desc.type === "File" From b5fc99762d2c04bf24f600764ba9da1c08f941e2 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 7 Jan 2019 11:53:50 +0100 Subject: [PATCH 095/293] [nodes] Texturing: minor change of label and advanced status --- meshroom/nodes/aliceVision/Texturing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index fcc5fce57e..e13cc449b5 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -92,7 +92,7 @@ class Texturing(desc.CommandLineNode): ), desc.IntParam( name='maxNbImagesForFusion', - label='Max Nb of Images For Fusion', + label='Max Number of Images For Fusion', description='''Max number of images to combine to create the final texture''', value=3, range=(0, 10, 1), @@ -122,6 +122,7 @@ class Texturing(desc.CommandLineNode): description='''Triangle visibility is based on the union of vertices visiblity.''', value=False, uid=[0], + advanced=True, ), desc.BoolParam( name='flipNormals', From 4541d825ad49f9b343cfb266e011d1213944f6a8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 4 Jan 2019 19:45:07 +0100 Subject: [PATCH 096/293] [ui] GraphEditor: consistent readOnly mode when computing * lock edit actions when graph is being computed * add an advanced option to control this behavior and unlock it (stored in persistent settings) --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 30 +++++++++++++++---- .../qml/GraphEditor/GraphEditorSettings.qml | 1 + meshroom/ui/qml/main.qml | 24 +++++++++++---- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 3ca4af8065..e8cd12bd72 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -57,6 +57,8 @@ Item { /// Duplicate a node and optionnally all the following ones function duplicateNode(node, duplicateFollowingNodes) { + if(root.readOnly) + return; var nodes = uigraph.duplicateNode(node, duplicateFollowingNodes) selectNode(nodes[0]) } @@ -113,9 +115,13 @@ Item { onClicked: { if(mouse.button == Qt.RightButton) { - // store mouse click position in 'draggable' coordinates as new node spawn position - newNodeMenu.spawnPosition = mouseArea.mapToItem(draggable, mouse.x, mouse.y) - newNodeMenu.popup() + if(readOnly) + lockedMenu.popup(); + else { + // store mouse click position in 'draggable' coordinates as new node spawn position + newNodeMenu.spawnPosition = mouseArea.mapToItem(draggable, mouse.x, mouse.y); + newNodeMenu.popup(); + } } } @@ -197,6 +203,17 @@ Item { } } + // Informative contextual menu when graph is read-only + Menu { + id: lockedMenu + MenuItem { + id: item + font.pointSize: 8 + enabled: false + text: "Computing - Graph is Locked!" + } + } + Item { id: draggable transformOrigin: Item.TopLeft @@ -259,12 +276,12 @@ Item { MenuItem { text: "Compute" - enabled: !root.readOnly && nodeMenu.canComputeNode + enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode onTriggered: uigraph.execute(nodeMenu.currentNode) } MenuItem { text: "Submit" - enabled: !root.readOnly && nodeMenu.canComputeNode + enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode visible: uigraph.canSubmit height: visible ? implicitHeight : 0 onTriggered: uigraph.submit(nodeMenu.currentNode) @@ -276,6 +293,7 @@ Item { MenuSeparator {} MenuItem { text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "") + enabled: !root.readOnly onTriggered: duplicateNode(nodeMenu.currentNode, false) MaterialToolButton { id: duplicateFollowingButton @@ -399,6 +417,8 @@ Item { onExited: uigraph.hoveredNode = null Keys.onDeletePressed: { + if(root.readOnly) + return; if(event.modifiers == Qt.AltModifier) uigraph.removeNodesFrom(node) else diff --git a/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml b/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml index 4255ceb7e3..f6d2d35006 100644 --- a/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditorSettings.qml @@ -8,4 +8,5 @@ import Qt.labs.settings 1.0 Settings { category: 'GraphEditor' property bool showAdvancedAttributes: false + property bool lockOnCompute: true } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index d8cc2e1bd3..690ce7eb6b 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -20,6 +20,9 @@ ApplicationWindow { minimumHeight: 500 visible: true + /// Whether graph is currently locked and therefore read-only + readonly property bool graphLocked: _reconstruction.computing && GraphEditorSettings.lockOnCompute + title: { var t = _reconstruction.graph.filepath || "Untitled" if(!_reconstruction.undoStack.clean) @@ -28,10 +31,8 @@ ApplicationWindow { return t } - property variant node: null // supported 3D files extensions readonly property var _3dFileExtensions: ['.obj', '.abc'] - onClosing: { // make sure document is saved before exiting application close.accepted = false @@ -253,7 +254,7 @@ ApplicationWindow { property string tooltip: 'Undo "' +_reconstruction.undoStack.undoText +'"' text: "Undo" shortcut: "Ctrl+Z" - enabled: _reconstruction.undoStack.canUndo && !_reconstruction.computing + enabled: _reconstruction.undoStack.canUndo && !graphLocked onTriggered: _reconstruction.undoStack.undo() } Action { @@ -262,7 +263,7 @@ ApplicationWindow { property string tooltip: 'Redo "' +_reconstruction.undoStack.redoText +'"' text: "Redo" shortcut: "Ctrl+Shift+Z" - enabled: _reconstruction.undoStack.canRedo && !_reconstruction.computing + enabled: _reconstruction.undoStack.canRedo && !graphLocked onTriggered: _reconstruction.undoStack.redo() } @@ -534,6 +535,17 @@ ApplicationWindow { enabled: !_reconstruction.computingLocally onTriggered: _reconstruction.graph.clearSubmittedNodes() } + Menu { + title: "Advanced" + MenuItem { + text: "Lock on Compute" + ToolTip.text: "Lock Graph when computing. This should only be disabled for advanced usage." + ToolTip.visible: hovered + checkable: true + checked: GraphEditorSettings.lockOnCompute + onClicked: GraphEditorSettings.lockOnCompute = !GraphEditorSettings.lockOnCompute + } + } } } } @@ -554,7 +566,7 @@ ApplicationWindow { anchors.fill: parent uigraph: _reconstruction nodeTypesModel: _nodeTypes - readOnly: _reconstruction.computing + readOnly: graphLocked onNodeDoubleClicked: { if(node.nodeType === "StructureFromMotion") @@ -579,7 +591,7 @@ ApplicationWindow { width: Math.round(parent.width * 0.3) node: _reconstruction.selectedNode // Make NodeEditor readOnly when computing - readOnly: _reconstruction.computing + readOnly: graphLocked onAttributeDoubleClicked: graphEditorPanel.displayAttribute(attribute) onUpgradeRequest: { var n = _reconstruction.upgradeNode(node); From b5c985b3fb4e41a104f77d589759032a7ca61c85 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 7 Jan 2019 15:25:06 +0100 Subject: [PATCH 097/293] [ui] GraphEditor: solo 3D media with Double Click + Control modifier * allow to solo a 3D media from the GraphEditor by double clicking on a node or an attribute with the Control modifier pressed * consistent with Viewer3D.MediaLibrary behavior (solo on Ctrl+Click on visibility button) * handle supported file extensions in Viewer3DSettings --- .../ui/qml/GraphEditor/AttributeEditor.qml | 4 +-- .../qml/GraphEditor/AttributeItemDelegate.qml | 4 +-- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 4 +-- meshroom/ui/qml/GraphEditor/NodeEditor.qml | 4 +-- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 15 ++++++++++- meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml | 10 +++++++ meshroom/ui/qml/WorkspaceView.qml | 9 +++---- meshroom/ui/qml/main.qml | 26 ++++++++----------- 8 files changed, 46 insertions(+), 30 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index 7b5e89981a..bd7dcbbef9 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -15,7 +15,7 @@ ListView { property int labelWidth: 180 signal upgradeRequest() - signal attributeDoubleClicked(var attribute) + signal attributeDoubleClicked(var mouse, var attribute) implicitHeight: contentHeight @@ -40,7 +40,7 @@ ListView { readOnly: root.readOnly labelWidth: root.labelWidth attribute: object - onDoubleClicked: root.attributeDoubleClicked(attr) + onDoubleClicked: root.attributeDoubleClicked(mouse, attr) } } } diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index fc4473456a..795bd59d2d 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -18,7 +18,7 @@ RowLayout { readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly - signal doubleClicked(var attr) + signal doubleClicked(var mouse, var attr) spacing: 2 @@ -62,7 +62,7 @@ RowLayout { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.AllButtons - onDoubleClicked: root.doubleClicked(root.attribute) + onDoubleClicked: root.doubleClicked(mouse, root.attribute) property Component menuComp: Menu { id: paramMenu diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index e8cd12bd72..83e4a0c99e 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -21,7 +21,7 @@ Item { // signals signal workspaceMoved() signal workspaceClicked() - signal nodeDoubleClicked(var node) + signal nodeDoubleClicked(var mouse, var node) // trigger initial fit() after initialization // (ensure GraphEditor has its final size) @@ -409,7 +409,7 @@ Item { } } - onDoubleClicked: root.nodeDoubleClicked(node) + onDoubleClicked: root.nodeDoubleClicked(mouse, node) onMoved: uigraph.moveNode(node, position) diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index dd099905b6..5cb63f869e 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -16,7 +16,7 @@ Panel { property bool readOnly: false property bool isCompatibilityNode: node && node.compatibilityIssue !== undefined - signal attributeDoubleClicked(var attribute) + signal attributeDoubleClicked(var mouse, var attribute) signal upgradeRequest() title: "Node" + (node !== null ? " - " + node.label + "" : "") @@ -116,7 +116,7 @@ Panel { Layout.fillWidth: true attributes: root.node.attributes readOnly: root.readOnly || root.isCompatibilityNode - onAttributeDoubleClicked: root.attributeDoubleClicked(attribute) + onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute) onUpgradeRequest: root.upgradeRequest() } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index eca8a239af..e9cf67f0eb 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -37,8 +37,21 @@ FocusScope { mediaLibrary.load(filepath); } + /// View 'attribute' in the 3D Viewer. Media will be loaded if needed. + /// Returns whether the attribute can be visualized (matching type and extension). function view(attribute) { - mediaLibrary.view(attribute) + if( attribute.desc.type === "File" + && Viewer3DSettings.supportedExtensions.indexOf(Filepath.extension(attribute.value)) > - 1 ) + { + mediaLibrary.view(attribute); + return true; + } + return false; + } + + /// Solo (i.e display only) the given attribute. + function solo(attribute) { + mediaLibrary.solo(mediaLibrary.find(attribute)); } function clear() { diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 202b6c216b..338024c776 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -11,6 +11,16 @@ Item { readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml") readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready + // supported 3D files extensions + readonly property var supportedExtensions: { + var exts = ['.obj']; + if(supportAlembic) + exts.push('.abc'); + if(supportDepthMap) + exts.push('.exr'); + return exts; + } + // Available render modes readonly property var renderModes: [ // Can't use ListModel because of MaterialIcons expressions {"name": "Solid", "icon": MaterialIcons.crop_din }, diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 661b15e0df..b62d0e10d4 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -21,6 +21,7 @@ Item { property variant reconstruction: _reconstruction readonly property variant cameraInits: _reconstruction.cameraInits property bool readOnly: false + readonly property Viewer3D viewer3D: viewer3D implicitWidth: 300 @@ -32,10 +33,6 @@ Item { viewer3D.load(filepath); } - function viewAttribute(attr) { - viewer3D.view(attr); - } - Connections { target: reconstruction onGraphChanged: viewer3D.clear() @@ -48,7 +45,7 @@ Item { function viewSfM() { if(!reconstruction.sfm) return; - viewAttribute(reconstruction.sfm.attribute('output')); + viewer3D.view(reconstruction.sfm.attribute('output')); } SystemPalette { id: activePalette } @@ -144,7 +141,7 @@ Item { anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter visible: outputReady && outputMediaIndex == -1 - onClicked: viewAttribute(_reconstruction.endNode.attribute("outputMesh")) + onClicked: viewer3D.view(_reconstruction.endNode.attribute("outputMesh")) } } } diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 690ce7eb6b..2bec9d199f 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -31,8 +31,6 @@ ApplicationWindow { return t } - // supported 3D files extensions - readonly property var _3dFileExtensions: ['.obj', '.abc'] onClosing: { // make sure document is saved before exiting application close.accepted = false @@ -505,6 +503,14 @@ ApplicationWindow { Layout.minimumHeight: 50 reconstruction: _reconstruction readOnly: _reconstruction.computing + + function viewIn3D(attribute, mouse) { + var loaded = viewer3D.view(attribute); + // solo media if Control modifier was held + if(loaded && mouse && mouse.modifiers & Qt.ControlModifier) + viewer3D.solo(attribute); + return loaded; + } } } @@ -550,15 +556,6 @@ ApplicationWindow { } } - function displayAttribute(attr) { - if( attr.desc.type === "File" - && _3dFileExtensions.indexOf(Filepath.extension(attr.value)) > - 1 ) - { - workspaceView.viewAttribute(attr); - return true; - } - return false; - } GraphEditor { id: graphEditor @@ -571,14 +568,13 @@ ApplicationWindow { onNodeDoubleClicked: { if(node.nodeType === "StructureFromMotion") { - _reconstruction.sfm = node - return + _reconstruction.sfm = node; } for(var i=0; i < node.attributes.count; ++i) { var attr = node.attributes.at(i) if(attr.isOutput - && graphEditorPanel.displayAttribute(attr)) + && workspaceView.viewIn3D(attr, mouse)) { break; } @@ -592,7 +588,7 @@ ApplicationWindow { node: _reconstruction.selectedNode // Make NodeEditor readOnly when computing readOnly: graphLocked - onAttributeDoubleClicked: graphEditorPanel.displayAttribute(attribute) + onAttributeDoubleClicked: workspaceView.viewIn3D(attribute, mouse) onUpgradeRequest: { var n = _reconstruction.upgradeNode(node); _reconstruction.selectedNode = n; From b46a2dbba142cf47a05ff6ce6c0f9f2d68516e7e Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 7 Jan 2019 16:48:17 +0100 Subject: [PATCH 098/293] [ui] Controls: add SearchBar component + better keyboard focus handling * use it in MetadataListView and GraphEditor 'add Node' menu * GraphEditor: forward MenuItem key events to searchBar to be able to continue editing the filter even if it lost active focus --- meshroom/ui/qml/Controls/SearchBar.qml | 41 ++++++++++++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + meshroom/ui/qml/GraphEditor/GraphEditor.qml | 32 +++++++-------- meshroom/ui/qml/Viewer/ImageMetadataView.qml | 16 ++------ 4 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 meshroom/ui/qml/Controls/SearchBar.qml diff --git a/meshroom/ui/qml/Controls/SearchBar.qml b/meshroom/ui/qml/Controls/SearchBar.qml new file mode 100644 index 0000000000..2fffc71b30 --- /dev/null +++ b/meshroom/ui/qml/Controls/SearchBar.qml @@ -0,0 +1,41 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 + + +/** + * Basic SearchBar component with an appropriate icon and a TextField. + */ +FocusScope { + property alias textField: field + property alias text: field.text + + implicitHeight: childrenRect.height + Keys.forwardTo: [field] + + function forceActiveFocus() { + field.forceActiveFocus() + } + + function clear() { + field.clear() + } + + RowLayout { + width: parent.width + + MaterialLabel { + text: MaterialIcons.search + } + + TextField { + id: field + focus: true + Layout.fillWidth: true + selectByMouse: true + } + } +} + + diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index aac7ce36de..2efd461a28 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -4,3 +4,4 @@ FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml Panel 1.0 Panel.qml +SearchBar 1.0 SearchBar.qml diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 83e4a0c99e..bed4741076 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -142,18 +142,14 @@ Item { if(visible) { // when menu is shown, // clear and give focus to the TextField filter - filterTextField.clear() - filterTextField.forceActiveFocus() + searchBar.clear() + searchBar.forceActiveFocus() } } - TextField { - id: filterTextField - selectByMouse: true + SearchBar { + id: searchBar width: parent.width - // ensure down arrow give focus to the first MenuItem - // (without this, we have to pressed the down key twice to do so) - Keys.onDownPressed: nextItemInFocusChain().forceActiveFocus() } Repeater { @@ -164,24 +160,28 @@ Item { id: menuItemDelegate font.pointSize: 8 padding: 3 + // Hide items that does not match the filter text - visible: modelData.toLowerCase().indexOf(filterTextField.text.toLocaleLowerCase()) > -1 + visible: modelData.toLowerCase().indexOf(searchBar.text.toLowerCase()) > -1 + // Reset menu currentIndex if highlighted items gets filtered out + onVisibleChanged: if(highlighted) newNodeMenu.currentIndex = 0 text: modelData + // Forward key events to the search bar to continue typing seamlessly + // even if this delegate took the activeFocus due to mouse hovering + Keys.forwardTo: [searchBar.textField] Keys.onPressed: { + event.accepted = false; switch(event.key) { case Qt.Key_Return: case Qt.Key_Enter: // create node on validation (Enter/Return keys) - newNodeMenu.createNode(modelData) - newNodeMenu.dismiss() - break; - case Qt.Key_Home: - // give focus back to filter - filterTextField.forceActiveFocus() + newNodeMenu.createNode(modelData); + newNodeMenu.close(); + event.accepted = true; break; default: - break; + searchBar.textField.forceActiveFocus(); } } // Create node on mouse click diff --git a/meshroom/ui/qml/Viewer/ImageMetadataView.qml b/meshroom/ui/qml/Viewer/ImageMetadataView.qml index 91bf5e45e3..44659bfca9 100644 --- a/meshroom/ui/qml/Viewer/ImageMetadataView.qml +++ b/meshroom/ui/qml/Viewer/ImageMetadataView.qml @@ -121,17 +121,9 @@ FloatingPane { ColumnLayout { anchors.fill: parent - // Search toolbar - RowLayout { - Label { - text: MaterialIcons.search - font.family: MaterialIcons.fontFamily - } - TextField { - id: filter - Layout.fillWidth: true - z: 2 - } + SearchBar { + id: searchBar + Layout.fillWidth: true } // Metadata ListView @@ -148,7 +140,7 @@ FloatingPane { model: metadataModel sortRole: "raw" filterRole: "raw" - filterValue: filter.text + filterValue: searchBar.text delegate: RowLayout { width: parent.width Label { From 2990129e0f49adcb2466b87ebbbd2fb9a54bc03f Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 7 Jan 2019 18:47:20 +0100 Subject: [PATCH 099/293] [ui] GraphEditor: display ClosedHandCursor while dragging --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index bed4741076..f1b5c35e3d 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -82,6 +82,8 @@ Item { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton drag.threshold: 0 + cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor + onWheel: { var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1/factor var scale = draggable.scale * zoomFactor From 029c35cfcf568ed16648ee45903f975b5d1c9626 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 9 Jan 2019 18:09:01 +0100 Subject: [PATCH 100/293] [ui] Viewer3D: let QmlAlembic manage entities visibility see https://github.com/alicevision/qmlAlembic/pull/11 --- meshroom/ui/qml/Viewer3D/MediaLoader.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index c24e8eb79f..59e049aba1 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -93,12 +93,8 @@ import Utils 1.0 if(obj.status === SceneLoader.Ready) { for(var i = 0; i < obj.pointClouds.length; ++i) { vertexCount += Scene3DHelper.vertexCount(obj.pointClouds[i]); - obj.pointClouds[i].enabled = Qt.binding(function() { return Viewer3DSettings.pointSize > 0; }); } cameraCount = obj.spawnCameraSelectors(); - for(var i = 0; i < obj.cameras.length; ++i) { - obj.cameras[i].enabled = Qt.binding(function() { return Viewer3DSettings.cameraScale > 0; }); - } } root.status = obj.status; }) From c0eb556eeaa9486cb5be54caf4cf8753c2b6ec7b Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Mon, 14 Jan 2019 18:24:49 +0100 Subject: [PATCH 101/293] refactoring: function readSfMData() to read sfm files --- bin/meshroom_photogrammetry | 23 ++-------- meshroom/nodes/aliceVision/CameraInit.py | 56 ++++++++++++++---------- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 5539c9810f..8ae234338f 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -8,6 +8,7 @@ import meshroom meshroom.setupEnvironment() import meshroom.core.graph +from meshroom.nodes.aliceVision.CameraInit import readSfMData from meshroom import multiview parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.') @@ -47,27 +48,9 @@ if not args.input and not args.inputImages: exit(1) if args.input and os.path.isfile(args.input): - # with open(args.input) as jsonFile: - with io.open(args.input, 'r', encoding='utf-8', errors='ignore') as jsonFile: - fileData = json.load(jsonFile) - intrinsics = fileData.get("intrinsics", []) + views, intrinsics = readSfMData(args.input) + print(views) print(intrinsics) - intrinsics = [{k: v for k, v in item.items()} for item in fileData.get("intrinsics", [])] - for intrinsic in intrinsics: - pp = intrinsic['principalPoint'] - intrinsic['principalPoint'] = {} - intrinsic['principalPoint']['x'] = pp[0] - intrinsic['principalPoint']['y'] = pp[1] - # convert empty string distortionParams (i.e: Pinhole model) to empty list - if intrinsic['distortionParams'] == '': - intrinsic['distortionParams'] = list() - print(intrinsics) - - # views = fileData.get("views", []) - views = [{k: v for k, v in item.items()} for item in fileData.get("views", [])] - for view in views: - view['metadata'] = json.dumps(view['metadata']) # convert metadata to string - # print(views) graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, inputImages=args.inputImages, output=args.output) graph.findNode('DepthMap_1').downscale.value = args.scale diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 48f59db445..55e34d7c50 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -45,6 +45,38 @@ value=False, uid=[0]), ] +def readSfMData(sfmFile): + """ Read views and intrinsics from a .sfm file + + Args: + sfmFile: the .sfm file containing views and intrinsics + + Returns: + The views and intrinsics of the .sfm as two separate lists + """ + 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) + + intrinsicsKeys = [i.name for i in Intrinsic] + + intrinsics = [{k: v for k, v in item.items() if k in intrinsicsKeys} for item in data.get("intrinsics", [])] + for intrinsic in intrinsics: + pp = intrinsic['principalPoint'] + intrinsic['principalPoint'] = {} + intrinsic['principalPoint']['x'] = pp[0] + intrinsic['principalPoint']['y'] = pp[1] + # convert empty string distortionParams (i.e: Pinhole model) to empty list + if intrinsic['distortionParams'] == '': + intrinsic['distortionParams'] = list() + print('intrinsics:', intrinsics) + viewsKeys = [v.name for v in Viewpoint] + views = [{k: v for k, v in item.items() if k in viewsKeys} for item in data.get("views", [])] + for view in views: + view['metadata'] = json.dumps(view['metadata']) # convert metadata to string + print('views:', views) + return views, intrinsics class CameraInit(desc.CommandLineNode): commandLine = 'aliceVision_cameraInit {allParams} --allowSingleView 1' # don't throw an error if there is only one image @@ -134,28 +166,7 @@ def buildIntrinsics(self, node, additionalViews=()): # Reload result of aliceVision_cameraInit cameraInitSfM = node.output.value - 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(cameraInitSfM, 'r', encoding='utf-8', errors='ignore') as f: - data = json.load(f) - - intrinsicsKeys = [i.name for i in Intrinsic] - intrinsics = [{k: v for k, v in item.items() if k in intrinsicsKeys} for item in data.get("intrinsics", [])] - for intrinsic in intrinsics: - pp = intrinsic['principalPoint'] - intrinsic['principalPoint'] = {} - intrinsic['principalPoint']['x'] = pp[0] - intrinsic['principalPoint']['y'] = pp[1] - # convert empty string distortionParams (i.e: Pinhole model) to empty list - if intrinsic['distortionParams'] == '': - intrinsic['distortionParams'] = list() - # print('intrinsics:', intrinsics) - viewsKeys = [v.name for v in Viewpoint] - views = [{k: v for k, v in item.items() if k in viewsKeys} for item in data.get("views", [])] - for view in views: - view['metadata'] = json.dumps(view['metadata']) # convert metadata to string - # print('views:', views) - return views, intrinsics + return readSfMData(cameraInitSfM) except Exception: raise @@ -198,4 +209,3 @@ def buildCommandLine(self, chunk): def processChunk(self, chunk): self.createViewpointsFile(chunk.node) desc.CommandLineNode.processChunk(self, chunk) - From 1fdbc7e6835a47b0c1dbe2d6ae140e0d31268c2a Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Mon, 14 Jan 2019 18:28:17 +0100 Subject: [PATCH 102/293] common call out of if --- bin/meshroom_photogrammetry | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 8ae234338f..7b2fd3754b 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -53,10 +53,10 @@ if args.input and os.path.isfile(args.input): print(intrinsics) graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, inputImages=args.inputImages, output=args.output) - graph.findNode('DepthMap_1').downscale.value = args.scale else: graph = multiview.photogrammetry(inputFolder=args.input, inputImages=args.inputImages, output=args.output) - graph.findNode('DepthMap_1').downscale.value = args.scale + +graph.findNode('DepthMap_1').downscale.value = args.scale if args.save: graph.save(args.save) From 13c541a161e178f30f823eb9020a3da1294bed83 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Mon, 14 Jan 2019 18:30:29 +0100 Subject: [PATCH 103/293] removed unused imports and clean --- bin/meshroom_photogrammetry | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 7b2fd3754b..bfd0fc3394 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -1,8 +1,5 @@ #!/usr/bin/env python import argparse -import json -import os -import io import meshroom meshroom.setupEnvironment() @@ -49,8 +46,8 @@ if not args.input and not args.inputImages: if args.input and os.path.isfile(args.input): views, intrinsics = readSfMData(args.input) - print(views) - print(intrinsics) + # print(views) + # print(intrinsics) graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, inputImages=args.inputImages, output=args.output) else: From d8a53ef35e69346cc243bec24da276217b763447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Wed, 16 Jan 2019 15:08:15 +0100 Subject: [PATCH 104/293] [nodes] `Meshing` Add option `addLandmarksToTheDensePointCloud` --- meshroom/nodes/aliceVision/Meshing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 238cfd0901..fcf1060386 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -201,6 +201,14 @@ class Meshing(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='addLandmarksToTheDensePointCloud', + label='Add Landmarks To The Dense Point Cloud', + description='Add SfM Landmarks to the dense point cloud.', + value=True, + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From a093673683bd38a48223ba75a2b8e818bcbee022 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 18 Jan 2019 10:09:27 +0100 Subject: [PATCH 105/293] [nodes] SfmTransform: update parameters doc --- meshroom/nodes/aliceVision/SfMTransform.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index 49ed61117e..3dc4435eec 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -18,7 +18,11 @@ class SfMTransform(desc.CommandLineNode): desc.ChoiceParam( name='method', label='Transformation Method', - description='''Transformation method (transformation, auto_from_cameras, auto_from_landmarks).''', + description="Transformation method:\n" + " * transformation: Apply a given transformation\n" + " * auto_from_cameras: Use cameras\n" + " * auto_from_landmarks: Use landmarks\n" + " * from_single_camera: Use a specific camera as the origin of the coordinate system", value='auto_from_landmarks', values=['transformation', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera'], exclusive=True, @@ -27,7 +31,9 @@ class SfMTransform(desc.CommandLineNode): desc.StringParam( name='transformation', label='Transformation', - description='''Align [X,Y,Z] to +Y-axis, rotate around Y by R deg, scale by S; syntax: X,Y,Z;R;S. (required only for 'transformation' method)''', + description="Required only for 'transformation' and 'from_single_camera' methods:\n" + " * transformation: Align [X,Y,Z] to +Y-axis, rotate around Y by R deg, scale by S; syntax: X,Y,Z;R;S\n" + " * from_single_camera: Camera UID or image filename", value='', uid=[0], ), From 3291c3b983739f886096a8beb103b4d08bfc4276 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Fri, 18 Jan 2019 13:52:51 +0100 Subject: [PATCH 106/293] [nodes] forgot to remove print debug --- 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 55e34d7c50..f8cc5aa29f 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -70,12 +70,12 @@ def readSfMData(sfmFile): # convert empty string distortionParams (i.e: Pinhole model) to empty list if intrinsic['distortionParams'] == '': intrinsic['distortionParams'] = list() - print('intrinsics:', intrinsics) + viewsKeys = [v.name for v in Viewpoint] views = [{k: v for k, v in item.items() if k in viewsKeys} for item in data.get("views", [])] for view in views: view['metadata'] = json.dumps(view['metadata']) # convert metadata to string - print('views:', views) + return views, intrinsics class CameraInit(desc.CommandLineNode): From c595d8d6648414d3524413b2e7187b9e49dda972 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 11:49:15 +0100 Subject: [PATCH 107/293] [ui] move ImageGallery into a dedicated module --- meshroom/ui/qml/{ => ImageGallery}/ImageDelegate.qml | 0 meshroom/ui/qml/{ => ImageGallery}/ImageGallery.qml | 0 meshroom/ui/qml/ImageGallery/qmldir | 4 ++++ meshroom/ui/qml/WorkspaceView.qml | 1 + 4 files changed, 5 insertions(+) rename meshroom/ui/qml/{ => ImageGallery}/ImageDelegate.qml (100%) rename meshroom/ui/qml/{ => ImageGallery}/ImageGallery.qml (100%) create mode 100644 meshroom/ui/qml/ImageGallery/qmldir diff --git a/meshroom/ui/qml/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml similarity index 100% rename from meshroom/ui/qml/ImageDelegate.qml rename to meshroom/ui/qml/ImageGallery/ImageDelegate.qml diff --git a/meshroom/ui/qml/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml similarity index 100% rename from meshroom/ui/qml/ImageGallery.qml rename to meshroom/ui/qml/ImageGallery/ImageGallery.qml diff --git a/meshroom/ui/qml/ImageGallery/qmldir b/meshroom/ui/qml/ImageGallery/qmldir new file mode 100644 index 0000000000..f6bbf7dbcf --- /dev/null +++ b/meshroom/ui/qml/ImageGallery/qmldir @@ -0,0 +1,4 @@ +module ImageGallery + +ImageGallery 1.0 ImageGallery.qml +ImageDelegate 1.0 ImageDelegate.qml diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index b62d0e10d4..d0f2c12ed7 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 // For SplitView import QtQuick.Layouts 1.3 import Qt.labs.platform 1.0 as Platform +import ImageGallery 1.0 import Viewer 1.0 import Viewer3D 1.0 import MaterialIcons 2.2 From fad2cc3e1c789fbf8e26c0e3d6c365d2bacac929 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 18:08:46 +0100 Subject: [PATCH 108/293] [ui] Reconstruction: prevent QML from evaluating destroyed objects * reset cameraInit property to None when underlying object is destroyed * test for viewpoints validity in 'isInViews' and 'isReconstructed' methods --- meshroom/ui/reconstruction.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 863bdd3e20..1c682ca3c8 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -9,6 +9,7 @@ from meshroom.core import Version from meshroom.core.node import Node, Status from meshroom.ui.graph import UIGraph +from meshroom.ui.utils import makeProperty class Message(QObject): @@ -240,15 +241,7 @@ def updateCameraInits(self): if set(self._cameraInits.objectList()) == set(cameraInits): return self._cameraInits.setObjectList(cameraInits) - self.setCameraInit(cameraInits[0] if cameraInits else None) - - def setCameraInit(self, cameraInit): - """ Set the internal CameraInit node. """ - # TODO: handle multiple CameraInit nodes - if self._cameraInit == cameraInit: - return - self._cameraInit = cameraInit - self.cameraInitChanged.emit() + self.cameraInit = cameraInits[0] if cameraInits else None def getCameraInitIndex(self): if not self._cameraInit: @@ -257,7 +250,7 @@ def getCameraInitIndex(self): def setCameraInitIndex(self, idx): camInit = self._cameraInits[idx] if self._cameraInits else None - self.setCameraInit(camInit) + self.cameraInit = camInit def lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ @@ -421,7 +414,7 @@ def onIntrinsicsAvailable(self, cameraInit, views, intrinsics): with self.groupedGraphModification("Set Views and Intrinsics"): self.setAttribute(cameraInit.viewpoints, views) self.setAttribute(cameraInit.intrinsics, intrinsics) - self.setCameraInit(cameraInit) + self.cameraInit = cameraInit def setBuildingIntrinsics(self, value): if self._buildingIntrinsics == value: @@ -430,7 +423,7 @@ def setBuildingIntrinsics(self, value): self.buildingIntrinsicsChanged.emit() cameraInitChanged = Signal() - cameraInit = Property(QObject, lambda self: self._cameraInit, notify=cameraInitChanged) + cameraInit = makeProperty(QObject, "_cameraInit", cameraInitChanged, resetOnDestroy=True) cameraInitIndex = Property(int, getCameraInitIndex, setCameraInitIndex, notify=cameraInitChanged) viewpoints = Property(QObject, getViewpoints, notify=cameraInitChanged) cameraInits = Property(QObject, lambda self: self._cameraInits, constant=True) @@ -502,11 +495,15 @@ def setEndNode(self, node=None): @Slot(QObject, result=bool) def isInViews(self, viewpoint): + if not viewpoint: + return # keys are strings (faster lookup) return str(viewpoint.viewId.value) in self._views @Slot(QObject, result=bool) def isReconstructed(self, viewpoint): + if not viewpoint: + return # fetch up-to-date poseId from sfm result (in case of rigs, poseId might have changed) view = self._views.get(str(viewpoint.poseId.value), None) # keys are strings (faster lookup) return view.get('poseId', -1) in self._poses if view else False From d6649fc36500a86ea24412972f25edb39eb40a82 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 18:19:06 +0100 Subject: [PATCH 109/293] [ui] ImageGallery: fix reconstruction status indicator update issues Bind reconstruction status to 'sfmReport' to ensure it is properly re-evaluated when switching from on SfM node to another. --- 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 11dc855798..54c4fde1ed 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -144,9 +144,9 @@ Panel { active: parent.inViews visible: active sourceComponent: MaterialLabel { - property bool reconstructed: _reconstruction.isReconstructed(model.object) + property bool reconstructed: _reconstruction.sfmReport && _reconstruction.isReconstructed(model.object) text: reconstructed ? MaterialIcons.check_circle : MaterialIcons.remove_circle - color: reconstructed ? "#4CAF50" : "#F44336" + color: reconstructed ? Colors.green : Colors.red ToolTip.text: reconstructed ? "Reconstructed" : "Not Reconstructed" } } From 997cb654427dd3cf1eac3138abe9378236aa74c9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 19:51:52 +0100 Subject: [PATCH 110/293] [ui] Panel: let content drive header and footer heights --- meshroom/ui/qml/Controls/Panel.qml | 47 +++++++++++++----------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/meshroom/ui/qml/Controls/Panel.qml b/meshroom/ui/qml/Controls/Panel.qml index 6d890f9c6f..a340bdf318 100644 --- a/meshroom/ui/qml/Controls/Panel.qml +++ b/meshroom/ui/qml/Controls/Panel.qml @@ -23,14 +23,12 @@ Page { QtObject { id: m - property int headerHeight: 24 - property int footerHeight: 22 property int hPadding: 6 - property int vPadding: 2 + property int vPadding: 4 readonly property color paneBackgroundColor: Qt.darker(root.palette.window, 1.15) } - padding: 2 + padding: 1 header: Pane { @@ -39,30 +37,28 @@ Page { leftPadding: m.hPadding; rightPadding: m.hPadding background: Rectangle { color: m.paneBackgroundColor } - Item { // Fix the height of the underlying RowLayout - implicitHeight: m.headerHeight + RowLayout { width: parent.width - RowLayout { - anchors.fill: parent - // Icon - Item { - id: iconPlaceHolder - width: childrenRect.width - height: childrenRect.height - Layout.alignment: Qt.AlignVCenter - visible: icon != "" - } + // Icon + Item { + id: iconPlaceHolder + width: childrenRect.width + height: childrenRect.height + Layout.alignment: Qt.AlignVCenter + visible: icon != "" + } - // Title - Label { - text: root.title - Layout.fillWidth: true - elide: Text.ElideRight - } - // - Row { id: headerLayout } + // Title + Label { + text: root.title + Layout.fillWidth: true + elide: Text.ElideRight + topPadding: m.vPadding + bottomPadding: m.vPadding } + // + Row { id: headerLayout } } } @@ -74,10 +70,9 @@ Page { background: Rectangle { color: m.paneBackgroundColor } // Content place holder - Item { + RowLayout { id: footerLayout width: parent.width - implicitHeight: m.footerHeight } } } From ef7c201c6311a131ecbfee0a53fe5b37bbc8c49a Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 19:54:48 +0100 Subject: [PATCH 111/293] [ui] ImageGallery: use image/camera icons instead of text in footer --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 54c4fde1ed..0cabaca6cc 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -280,20 +280,27 @@ Panel { } footerContent: RowLayout { - anchors.fill: parent // Image count - Label { + RowLayout { Layout.fillWidth: true - text: grid.model.count + " image" + (grid.model.count > 1 ? "s" : "") + (_reconstruction.nbCameras > 0 ? " / " + _reconstruction.nbCameras + " camera" + (_reconstruction.nbCameras > 1 ? "s": "") : "") - elide: Text.ElideRight + spacing: 8 + RowLayout { + MaterialLabel { text: MaterialIcons.image } + Label { text: grid.model.count } + } + RowLayout { + visible: _reconstruction.cameraInit && _reconstruction.nbCameras + MaterialLabel { text: MaterialIcons.videocam } + Label { text: _reconstruction.cameraInit ? _reconstruction.nbCameras : 0 } + } } + Item { Layout.fillHeight: true; Layout.fillWidth: true } + // Thumbnail size icon and slider - Label { + MaterialLabel { text: MaterialIcons.photo_size_select_large - font.family: MaterialIcons.fontFamily - font.pixelSize: 13 } Slider { id: thumbnailSizeSlider @@ -301,7 +308,6 @@ Panel { value: defaultCellSize to: 250 implicitWidth: 70 - height: parent.height } } From 4802f76f4e3ac6acdb64946cb52e7d62200da59a Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 16 Jan 2019 19:56:31 +0100 Subject: [PATCH 112/293] [ui] ImageGallery: set ScrollBar minimum size --- 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 0cabaca6cc..8813818f7f 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -40,7 +40,7 @@ Panel { Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.vertical: ScrollBar {} + ScrollBar.vertical: ScrollBar { minimumSize: 0.05 } focus: true clip: true From 7db38abdf053e334e59850f53d22ed16ec6f5b95 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 21 Jan 2019 10:28:05 +0100 Subject: [PATCH 113/293] [nodes][av] CameraInit: add initializationMode and groupCameraFallback * --allowIncompleteOutput option has been removed * clean up commented lines --- meshroom/nodes/aliceVision/CameraInit.py | 33 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index dec785297a..96837879d2 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -24,9 +24,6 @@ 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.StringParam(name="deviceMake", label="Make", description="Camera Make", value="", uid=[]), - # desc.StringParam(name="deviceModel", label="Model", description="Camera Model", value="", uid=[]), - # desc.StringParam(name="sensorWidth", label="Sensor Width", description="Camera Sensor Width", value="", 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=[]), @@ -34,6 +31,20 @@ 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)), ]), + + desc.ChoiceParam(name="initializationMode", label="Initialization Mode", + description="Defines how this Intrinsic was initialized:\n" + " * calibrated: calibrated externally.\n" + " * estimated: estimated from metadata and/or sensor width. \n" + " * unknown: unknown camera parameters (can still have default value guess)\n" + " * none: not set", + values=("calibrated", "estimated", "unknown", "none"), + value="none", + exclusive=True, + uid=[], + advanced=True + ), + desc.ListAttribute( name="distortionParams", elementDesc=desc.FloatParam(name="p", label="", description="", value=0.0, uid=[0], range=(-0.1, 0.1, 0.01)), @@ -81,6 +92,21 @@ class CameraInit(desc.CommandLineNode): range=(0, 180.0, 1), uid=[0], ), + desc.ChoiceParam( + name='groupCameraFallback', + label='Group Camera Fallback', + description="If there is no serial number in image metadata, devices cannot be accurately identified.\n" + "Therefore, internal camera parameters cannot be shared among images reliably.\n" + "A fallback grouping strategy must be chosen:\n" + " * global: group images from comparable devices (same make/model/focal) globally.\n" + " * folder: group images from comparable devices only within the same folder.\n" + " * image: never group images from comparable devices", + values=['global', 'folder', 'image'], + value='folder', + exclusive=True, + uid=[0], + advanced=True + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', @@ -122,7 +148,6 @@ def buildIntrinsics(self, node, additionalViews=()): os.makedirs(os.path.join(tmpCache, node.internalFolder)) self.createViewpointsFile(node, additionalViews) cmd = self.buildCommandLine(node.chunks[0]) - cmd += " --allowIncompleteOutput 1" # don't throw an error if the image intrinsic is undefined # logging.debug(' - commandLine:', cmd) subprocess = psutil.Popen(cmd, stdout=None, stderr=None, shell=True) stdout, stderr = subprocess.communicate() From 663a5d679dc0c6e4fe0b8079463ff3fa57ddaf9c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 21 Jan 2019 10:35:29 +0100 Subject: [PATCH 114/293] [ui] ImageGallery: introduce IntrinsicsIndicator New IntrinsicsIndicator component that displays the initialization mode of each Viewpoint's intrinsic with explanatory tooltip. --- .../ui/qml/ImageGallery/ImageDelegate.qml | 2 +- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 39 ++++---- .../qml/ImageGallery/IntrinsicsIndicator.qml | 94 +++++++++++++++++++ meshroom/ui/qml/Utils/Colors.qml | 1 + meshroom/ui/reconstruction.py | 32 +++++++ 5 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml index d29420eca3..367ca09722 100644 --- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml +++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml @@ -26,7 +26,7 @@ Item { id: _viewpoint property url source: viewpoint ? Filepath.stringToUrl(viewpoint.get("path").value) : '' property string metadataStr: viewpoint ? viewpoint.get("metadata").value : '' - property var metadata: metadataStr ? JSON.parse(viewpoint.get("metadata").value) : null + property var metadata: metadataStr ? JSON.parse(viewpoint.get("metadata").value) : {} } MouseArea { diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 8813818f7f..62f1b92ce7 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -80,6 +80,7 @@ Panel { } delegate: ImageDelegate { + id: imageDelegate viewpoint: object.value width: grid.cellWidth @@ -108,37 +109,29 @@ Panel { onRemoveRequest: sendRemoveRequest() Keys.onDeletePressed: sendRemoveRequest() - Row { + RowLayout { anchors.top: parent.top + anchors.left: parent.left anchors.right: parent.right - anchors.margins: 4 + anchors.margins: 2 spacing: 2 property bool valid: Qt.isQtObject(object) // object can be evaluated to null at some point during creation/deletion - property bool noMetadata: valid && !_reconstruction.hasMetadata(model.object) - property bool noIntrinsic: valid && !_reconstruction.hasValidIntrinsic(model.object) + property string intrinsicInitMode: valid ? _reconstruction.getIntrinsicInitMode(object) : "" property bool inViews: valid && _reconstruction.sfmReport && _reconstruction.isInViews(object) - // Missing metadata indicator - Loader { - active: parent.noMetadata - visible: active - sourceComponent: MaterialLabel { - text: MaterialIcons.info_outline - color: "#FF9800" - ToolTip.text: "No Metadata" - } - } - // Unknown camera instrinsics indicator - Loader { - active: parent.noIntrinsic - visible: active - sourceComponent: MaterialLabel { - text: MaterialIcons.camera - color: "#FF9800" - ToolTip.text: "No Camera Instrinsic Parameters (missing Metadata?)" - } + + // Camera Initialization indicator + IntrinsicsIndicator { + intrinsicInitMode: parent.intrinsicInitMode + metadata: imageDelegate.metadata + font.pointSize: 10 + padding: 2 + background: Rectangle { color: Colors.sysPalette.window; opacity: 0.6 } } + + Item { Layout.fillWidth: true } + // Reconstruction status indicator Loader { active: parent.inViews diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml new file mode 100644 index 0000000000..17e28d7898 --- /dev/null +++ b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml @@ -0,0 +1,94 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.4 +import MaterialIcons 2.2 +import Utils 1.0 + + +/** + * Display camera initialization status and the value of metadata + * that take part in this process. + */ +MaterialLabel { + id: root + + property string intrinsicInitMode + property var metadata: ({}) + + // access useful metadata + readonly property string make: metadata["Make"] + readonly property string model: metadata["Model"] + readonly property string focalLength: metadata["Exif:FocalLength"] + readonly property string focalLength35: metadata["Exif:FocalLengthIn35mmFilm"] + readonly property string bodySerialNumber: metadata["Exif:BodySerialNumber"] + readonly property string lensSerialNumber: metadata["Exif:LensSerialNumber"] + readonly property string sensorWidth: metadata["AliceVision:SensorWidth"] + readonly property string sensorWidthEstimation: metadata["AliceVision:SensorWidthEstimation"] + + property string statusText: "" + property string detailsText: "" + property string helperText: "" + + text: MaterialIcons.camera + + function metaStr(value) { + return value || "undefined" + } + + ToolTip.text: "Camera Intrinsics: " + statusText + "
" + + (detailsText ? detailsText + "
" : "") + + (helperText ? helperText + "
" : "") + + "
" + + "[Metadata]
" + + " - Make: " + metaStr(make) + "
" + + " - Model: " + metaStr(model) + "
" + + " - FocalLength: " + metaStr(focalLength) + "
" + + ((focalLength && sensorWidth) ? "" : " - FocalLengthIn35mmFilm: " + metaStr(focalLength35) + "
") + + " - SensorWidth: " + metaStr(sensorWidth) + (sensorWidthEstimation ? " (estimation: "+ sensorWidthEstimation + ")" : "") + + ((bodySerialNumber || lensSerialNumber) ? "" : "

Warning: SerialNumber metadata is missing.
Images from different devices might incorrectly share the same camera internal settings.") + + + state: intrinsicInitMode ? intrinsicInitMode : "unknown" + + states: [ + State { + name: "calibrated" + PropertyChanges { + target: root + color: Colors.green + statusText: "Calibrated" + detailsText: "Focal Length has been initialized externally." + } + }, + State { + name: "estimated" + PropertyChanges { + target: root + statusText: sensorWidth ? "Estimated" : "Approximated" + color: sensorWidth ? Colors.green : Colors.yellow + detailsText: "Focal Length was estimated from Metadata" + (sensorWidth ? " and Sensor Database." : " only.") + helperText: !sensorWidth ? "Add your Camera Model to the Sensor Database for more accurate results." : "" + } + }, + State { + name: "unknown" + PropertyChanges { + target: root + color: focalLength ? Colors.orange : Colors.red + statusText: "Unknown" + detailsText: "Focal Length could not be determined from metadata.
" + + "The default Field of View value was used as a fallback, which may lead to inaccurate result or failure." + helperText: "Check for missing Image metadata" + + (make && model && !sensorWidth ? " and/or add your Camera Model to the Sensor Database." : ".") + } + }, + State { + // fallback status when initialization mode is unset + name: "none" + PropertyChanges { + target: root + visible: false + } + } + + ] +} diff --git a/meshroom/ui/qml/Utils/Colors.qml b/meshroom/ui/qml/Utils/Colors.qml index 15e79b9f65..4dea34beb3 100644 --- a/meshroom/ui/qml/Utils/Colors.qml +++ b/meshroom/ui/qml/Utils/Colors.qml @@ -11,6 +11,7 @@ QtObject { readonly property color green: "#4CAF50" readonly property color orange: "#FF9800" + readonly property color yellow: "#FFEB3B" readonly property color red: "#F44336" readonly property color blue: "#03A9F4" } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 1c682ca3c8..84e4e225ba 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -514,6 +514,38 @@ def hasValidIntrinsic(self, viewpoint): allIntrinsicIds = [i.intrinsicId.value for i in self._cameraInit.intrinsics.value] return viewpoint.intrinsicId.value in allIntrinsicIds + @Slot(QObject, result=QObject) + def getIntrinsic(self, viewpoint): + """ + Get the intrinsic attribute associated to 'viewpoint' based on its intrinsicId. + + Args: + viewpoint (Attribute): the Viewpoint to consider. + Returns: + Attribute: the Viewpoint's corresponding intrinsic or None if not found. + """ + return next((i for i in self._cameraInit.intrinsics.value if i.intrinsicId.value == viewpoint.intrinsicId.value) + , None) + + @Slot(QObject, result=str) + def getIntrinsicInitMode(self, viewpoint): + """ + Get the initialization mode for the intrinsic associated to 'viewpoint'. + + Args: + viewpoint (Attribute): the Viewpoint to consider. + Returns: + str: the initialization mode of the Viewpoint's intrinsic or an empty string if none. + """ + intrinsic = self.getIntrinsic(viewpoint) + if not intrinsic: + return "" + try: + return intrinsic.initializationMode.value + except AttributeError: + # handle older versions that did not have this attribute + return "" + @Slot(QObject, result=bool) def hasMetadata(self, viewpoint): # Should be greater than 2 to avoid the particular case of "" From 9805ec4861037f212b23a81054062d23fe1c9cc0 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 22 Jan 2019 17:51:20 +0100 Subject: [PATCH 115/293] [ui] ImageGallery: tweak reconstruction status indicator style * use the same camera icon as in the rest of the UI * match IntrinsicIndicator style --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 62f1b92ce7..bc4643f31a 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -138,9 +138,12 @@ Panel { visible: active sourceComponent: MaterialLabel { property bool reconstructed: _reconstruction.sfmReport && _reconstruction.isReconstructed(model.object) - text: reconstructed ? MaterialIcons.check_circle : MaterialIcons.remove_circle + text: reconstructed ? MaterialIcons.videocam : MaterialIcons.videocam_off color: reconstructed ? Colors.green : Colors.red - ToolTip.text: reconstructed ? "Reconstructed" : "Not Reconstructed" + font.pointSize: 10 + padding: 2 + ToolTip.text: "Camera: " + (reconstructed ? "" : "Not ") + "Reconstructed" + background: Rectangle { color: Colors.sysPalette.window; opacity: 0.6 } } } } From d2d8090b5b9cf1d09201c9f0677efc840fbeedaf Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 22 Jan 2019 17:53:09 +0100 Subject: [PATCH 116/293] [ui] ImageGallery: add menu + advanced option to show view IDs --- .../ui/qml/ImageGallery/ImageDelegate.qml | 19 ++++++++++++++++ meshroom/ui/qml/ImageGallery/ImageGallery.qml | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml index 367ca09722..9100216f63 100644 --- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml +++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml @@ -15,6 +15,7 @@ Item { property alias source: _viewpoint.source property alias metadata: _viewpoint.metadata property bool readOnly: false + property bool displayViewId: false signal pressed(var mouse) signal removeRequest() @@ -25,6 +26,7 @@ Item { QtObject { id: _viewpoint property url source: viewpoint ? Filepath.stringToUrl(viewpoint.get("path").value) : '' + property int viewId: viewpoint ? viewpoint.get("viewId").value : -1 property string metadataStr: viewpoint ? viewpoint.get("metadata").value : '' property var metadata: metadataStr ? JSON.parse(viewpoint.get("metadata").value) : {} } @@ -91,6 +93,23 @@ Item { color: root.isCurrentItem ? parent.palette.highlight : "transparent" } } + + // Image viewId + Loader { + active: displayViewId + Layout.fillWidth: true + visible: active + sourceComponent: Label { + padding: imageLabel.padding + font.pointSize: imageLabel.font.pointSize + elide: imageLabel.elide + horizontalAlignment: imageLabel.horizontalAlignment + text: _viewpoint.viewId + background: Rectangle { + color: imageLabel.background.color + } + } + } } } } diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index bc4643f31a..664d60e485 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -30,6 +30,27 @@ Panel { title: "Images" implicitWidth: (root.defaultCellSize + 2) * 2 + headerBar: RowLayout { + MaterialToolButton { + text: MaterialIcons.more_vert + font.pointSize: 11 + padding: 2 + onClicked: graphEditorMenu.open() + Menu { + id: graphEditorMenu + y: parent.height + x: -width + parent.width + Menu { + title: "Advanced" + Action { + id: displayViewIdsAction + text: "Display View IDs" + checkable: true + } + } + } + } + } ColumnLayout { anchors.fill: parent spacing: 4 @@ -86,6 +107,7 @@ Panel { width: grid.cellWidth height: grid.cellHeight readOnly: root.readOnly + displayViewId: displayViewIdsAction.checked isCurrentItem: GridView.isCurrentItem From 6f1bcaa23e7c79ac0e7d4ffd4399a5b6c20ab585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Mon, 7 Jan 2019 12:15:30 +0100 Subject: [PATCH 117/293] [nodes] `DepthMap` Add option `exportIntermediateResults` --- meshroom/nodes/aliceVision/DepthMap.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index 6ba1f32ad3..b4753fbac0 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -87,6 +87,14 @@ class DepthMap(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.IntParam( + name='refineMaxTCams', + label='Refine: Nb Neighbour Cameras', + description='Refine: Number of neighbour cameras.', + value=6, + range=(1, 20, 1), + uid=[0], + ), desc.IntParam( name='refineNSamplesHalf', label='Refine: Number of Samples', @@ -114,14 +122,6 @@ class DepthMap(desc.CommandLineNode): uid=[0], advanced=True, ), - desc.IntParam( - name='refineMaxTCams', - label='Refine: Nb Neighbour Cameras', - description='Refine: Number of neighbour cameras.', - value=6, - range=(1, 20, 1), - uid=[0], - ), desc.IntParam( name='refineWSH', label='Refine: WSH', @@ -166,6 +166,14 @@ class DepthMap(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='exportIntermediateResults', + label='Export Intermediate Results', + description='Export intermediate results from the SGM and Refine steps.', + value=False, + uid=[], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From c27336c9be70ba322f578d290340eff073556e3d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 22 Jan 2019 19:38:34 +0100 Subject: [PATCH 118/293] [nodes] DepthMap: add nbGPUs param --- meshroom/nodes/aliceVision/DepthMap.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/meshroom/nodes/aliceVision/DepthMap.py b/meshroom/nodes/aliceVision/DepthMap.py index b4753fbac0..24fa430009 100644 --- a/meshroom/nodes/aliceVision/DepthMap.py +++ b/meshroom/nodes/aliceVision/DepthMap.py @@ -174,6 +174,15 @@ class DepthMap(desc.CommandLineNode): uid=[], advanced=True, ), + desc.IntParam( + name='nbGPUs', + label='Number of GPUs', + description='Number of GPUs to use (0 means use all available GPUs).', + value=0, + range=(0, 5, 1), + uid=[], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 45fda5ea14f3392212132111ad7b14bc667e36ac Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 22 Jan 2019 19:40:01 +0100 Subject: [PATCH 119/293] [nodes] Meshing: addLandmarksToTheDensePointCloud is false by default To align with https://github.com/alicevision/AliceVision/pull/585 --- meshroom/nodes/aliceVision/Meshing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index fcf1060386..11b2365fa8 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -205,7 +205,7 @@ class Meshing(desc.CommandLineNode): name='addLandmarksToTheDensePointCloud', label='Add Landmarks To The Dense Point Cloud', description='Add SfM Landmarks to the dense point cloud.', - value=True, + value=False, uid=[0], advanced=True, ), From 2e1ec27ea128123adb8cffab2332427b4a40c032 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 22 Jan 2019 19:46:16 +0100 Subject: [PATCH 120/293] [nodes] FeatureExtraction: keep force CPU by default for now To avoid conflicts of popsift with the vocabulary tree, we continue to use vlfeat by default for now. --- meshroom/nodes/aliceVision/FeatureExtraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index 9165add99b..92a97c816c 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -40,7 +40,7 @@ class FeatureExtraction(desc.CommandLineNode): name='forceCpuExtraction', label='Force CPU Extraction', description='Use only CPU feature extraction.', - value=False, + value=True, uid=[], advanced=True ), From 6c9ed427366e69b724a43a2d52f1491336348285 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 23 Jan 2019 18:51:05 +0100 Subject: [PATCH 121/293] [ui] Reconstruction: expose rebuildIntrinsics method Additional method to rebuild intrinsics from scratch; useful when the sensor database has been modified after an initial images drop. --- meshroom/ui/reconstruction.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 84e4e225ba..6736bbdbdc 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -356,11 +356,16 @@ def importImages(self, images, cameraInit): # Start the process of updating views and intrinsics self.runAsync(self.buildIntrinsics, args=(cameraInit, images,)) - def buildIntrinsics(self, cameraInit, additionalViews): + def buildIntrinsics(self, cameraInit, additionalViews, rebuild=False): """ Build up-to-date intrinsics and views based on already loaded + additional images. Does not modify the graph, can be called outside the main thread. Emits intrinsicBuilt(views, intrinsics) when done. + + Args: + cameraInit (Node): CameraInit node to build the intrinsics for + additionalViews: list of additional views to add to the CameraInit viewpoints + rebuild (bool): whether to rebuild already created intrinsics """ views = [] intrinsics = [] @@ -372,6 +377,13 @@ def buildIntrinsics(self, cameraInit, additionalViews): # * wait for the result before actually creating new nodes in the graph (see onIntrinsicsAvailable) inputs = cameraInit.toDict()["inputs"] if cameraInit else {} cameraInitCopy = Node("CameraInit", **inputs) + if rebuild: + # if rebuilding all intrinsics, for each Viewpoint: + for vp in cameraInitCopy.viewpoints.value: + vp.intrinsicId.resetValue() # reset intrinsic assignation + vp.metadata.resetValue() # and metadata (to clear any previous 'SensorWidth' entries) + # reset existing intrinsics list + cameraInitCopy.intrinsics.resetValue() try: self.setBuildingIntrinsics(True) @@ -387,9 +399,19 @@ def buildIntrinsics(self, cameraInit, additionalViews): self.setBuildingIntrinsics(False) # always emit intrinsicsBuilt signal to inform listeners # in other threads that computation is over - self.intrinsicsBuilt.emit(cameraInit, views, intrinsics) + self.intrinsicsBuilt.emit(cameraInit, views, intrinsics, rebuild) + + @Slot(Node) + def rebuildIntrinsics(self, cameraInit): + """ + Rebuild intrinsics of 'cameraInit' from scratch. - def onIntrinsicsAvailable(self, cameraInit, views, intrinsics): + Args: + cameraInit (Node): the CameraInit node + """ + self.runAsync(self.buildIntrinsics, args=(cameraInit, (), True)) + + def onIntrinsicsAvailable(self, cameraInit, views, intrinsics, rebuild=False): """ Update CameraInit with given views and intrinsics. """ augmentSfM = cameraInit is None commandTitle = "Add {} Images" @@ -401,6 +423,9 @@ def onIntrinsicsAvailable(self, cameraInit, views, intrinsics): views = [view for view in views if int(view["viewId"]) not in allViewIds] commandTitle = "Augment Reconstruction ({} Images)" + if rebuild: + commandTitle = "Rebuild '{}' Intrinsics".format(cameraInit.label) + # No additional views: early return if not views: return @@ -427,7 +452,7 @@ def setBuildingIntrinsics(self, value): cameraInitIndex = Property(int, getCameraInitIndex, setCameraInitIndex, notify=cameraInitChanged) viewpoints = Property(QObject, getViewpoints, notify=cameraInitChanged) cameraInits = Property(QObject, lambda self: self._cameraInits, constant=True) - intrinsicsBuilt = Signal(QObject, list, list) + intrinsicsBuilt = Signal(QObject, list, list, bool) buildingIntrinsicsChanged = Signal() buildingIntrinsics = Property(bool, lambda self: self._buildingIntrinsics, notify=buildingIntrinsicsChanged) liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) From 49e809df2b3cfeaf0376ce68acc4f36a5afdb5fc Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 23 Jan 2019 18:52:59 +0100 Subject: [PATCH 122/293] [ui] ImageGallery: add SensorDBDialog + 'rebuild intrinsics' feature * add SensorDBDialog explaining how to add a new entry in the sensor DB and ease the access to the sensor database * enable intrinsics rebuild at user level from this dialog * MessageDialog: react to clicks on hyperlinks --- meshroom/ui/qml/Controls/MessageDialog.qml | 3 + meshroom/ui/qml/ImageGallery/ImageGallery.qml | 15 ++++ .../ui/qml/ImageGallery/SensorDBDialog.qml | 76 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 meshroom/ui/qml/ImageGallery/SensorDBDialog.qml diff --git a/meshroom/ui/qml/Controls/MessageDialog.qml b/meshroom/ui/qml/Controls/MessageDialog.qml index 83e39d448a..cfb20df39a 100644 --- a/meshroom/ui/qml/Controls/MessageDialog.qml +++ b/meshroom/ui/qml/Controls/MessageDialog.qml @@ -83,17 +83,20 @@ Dialog { id: textLabel font.bold: true visible: text != "" + onLinkActivated: Qt.openUrlExternally(link) } // Detailed text Label { id: detailedLabel text: text visible: text != "" + onLinkActivated: Qt.openUrlExternally(link) } // Additional helper text Label { id: helperLabel visible: text != "" + onLinkActivated: Qt.openUrlExternally(link) } } diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 664d60e485..b4e33d516f 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -40,6 +40,13 @@ Panel { id: graphEditorMenu y: parent.height x: -width + parent.width + MenuItem { + text: "Edit Sensor Database..." + onTriggered: { + sensorDBDialog.open() + } + } + Menu { title: "Advanced" Action { @@ -51,6 +58,14 @@ Panel { } } } + + SensorDBDialog { + id: sensorDBDialog + sensorDatabase: Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) + readOnly: _reconstruction.computing + onUpdateIntrinsicsRequest: _reconstruction.rebuildIntrinsics(cameraInit) + } + ColumnLayout { anchors.fill: parent spacing: 4 diff --git a/meshroom/ui/qml/ImageGallery/SensorDBDialog.qml b/meshroom/ui/qml/ImageGallery/SensorDBDialog.qml new file mode 100644 index 0000000000..3fe17e7543 --- /dev/null +++ b/meshroom/ui/qml/ImageGallery/SensorDBDialog.qml @@ -0,0 +1,76 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + +import MaterialIcons 2.2 +import Controls 1.0 + + +MessageDialog { + id: root + + property url sensorDatabase + property bool readOnly: false + + signal updateIntrinsicsRequest() + + icon.text: MaterialIcons.camera + icon.font.pointSize: 10 + + modal: true + parent: Overlay.overlay + canCopy: false + + title: "Sensor Database" + text: "Add missing Camera Models to the Sensor Database to improve your results." + detailedText: "If a warning is displayed on your images, adding your Camera Model to the Sensor Database\n"+ + "can help fix it and improve your reconstruction results." + helperText: 'To update the Sensor Database (complete guide):
' + + ' - Look for the "sensor width" in millimeters of your Camera Model
' + + ' - Add a new line in the Database following this pattern: Make;Model;SensorWidthInMM
' + + ' - Click on "' + rebuildIntrinsics.text + '" once the Database has been saved
' + + ' - Contribute to the online Database' + + ColumnLayout { + RowLayout { + Layout.fillWidth: true + spacing: 2 + + Label { + text: "Sensor Database:" + } + + TextField { + id: sensorDBTextField + Layout.fillWidth: true + text: Filepath.normpath(sensorDatabase) + selectByMouse: true + readOnly: true + } + MaterialToolButton { + text: MaterialIcons.assignment + ToolTip.text: "Copy Path" + onClicked: { + sensorDBTextField.selectAll(); + sensorDBTextField.copy(); + ToolTip.text = "Path has been copied!" + } + onHoveredChanged: if(!hovered) ToolTip.text = "Copy Path" + } + MaterialToolButton { + text: MaterialIcons.open_in_new + ToolTip.text: "Open in External Editor" + onClicked: Qt.openUrlExternally(sensorDatabase) + } + } + } + Button { + id: rebuildIntrinsics + text: "Update Intrinsics" + enabled: !readOnly + onClicked: updateIntrinsicsRequest() + Layout.alignment: Qt.AlignCenter + } + standardButtons: Dialog.Close + onAccepted: close() +} From e5e90ee1ef4016e124f950294331e8eeb19d3e46 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 23 Jan 2019 18:55:45 +0100 Subject: [PATCH 123/293] [ui] improve wording on "building intrinsics" modal popup more explicit text that works both for new images import and intrinsics rebuild --- meshroom/ui/qml/main.qml | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 2bec9d199f..6cdea81f21 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -210,30 +210,23 @@ ApplicationWindow { return true } - Dialog { + MessageDialog { // Popup displayed while the application // is busy building intrinsics while importing images id: buildingIntrinsicsDialog modal: true - x: _window.width / 2 - width/2 - y: _window.height / 2 - height/2 visible: _reconstruction.buildingIntrinsics closePolicy: Popup.NoAutoClose - title: "Import Images" - padding: 15 - - ColumnLayout { - anchors.fill: parent - Label { - text: "Extracting images metadata... " - horizontalAlignment: Text.AlignHCenter + title: "Initializing Cameras" + icon.text: MaterialIcons.camera + icon.font.pointSize: 10 + canCopy: false + standardButtons: Dialog.NoButton - Layout.fillWidth: true - } - ProgressBar { - indeterminate: true - Layout.fillWidth: true - } + detailedText: "Extracting images metadata and creating Camera intrinsics..." + ProgressBar { + indeterminate: true + Layout.fillWidth: true } } From 35163f0a69d1f1c4b10048327f8a4b82875da293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 24 Jan 2019 10:37:01 +0100 Subject: [PATCH 124/293] [nodes] `Meshing` add option `saveRawDensePointCloud` --- meshroom/nodes/aliceVision/Meshing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index fcf1060386..0032cd1932 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -209,6 +209,14 @@ class Meshing(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='saveRawDensePointCloud', + label='Save Raw Dense Point Cloud', + description='Save dense point cloud before cut and filtering.', + value=False, + uid=[], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 8a702b9866d63d4e9cd9c8fc49a57799b91ab1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 24 Jan 2019 10:38:59 +0100 Subject: [PATCH 125/293] [nodes] `Meshing` change output `outputDenseReconstruction` to `outputDensePointCloud` --- meshroom/nodes/aliceVision/Meshing.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index 0032cd1932..3295b9804c 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -1,4 +1,4 @@ -__version__ = "2.0" +__version__ = "3.0" from meshroom.core import desc @@ -231,17 +231,16 @@ class Meshing(desc.CommandLineNode): outputs = [ desc.File( name="output", - label="Output mesh", + label="Output Dense Point Cloud", + description="Output dense point cloud with visibilities (SfMData file format).", + value="{cache}/{nodeType}/{uid0}/densePointCloud.abc", + uid=[], + ), + desc.File( + name="outputMesh", + label="Output Mesh", description="Output mesh (OBJ file format).", value="{cache}/{nodeType}/{uid0}/mesh.obj", uid=[], - ), - desc.File( - name="outputDenseReconstruction", - label="Output reconstruction", - description="Output dense reconstruction (BIN file format).", - value="{cache}/{nodeType}/{uid0}/denseReconstruction.bin", - uid=[], - group="", - ), + ), ] From 7c8e9a7d8faecc9bcebdd7df486be8ca20edf615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 24 Jan 2019 10:40:23 +0100 Subject: [PATCH 126/293] [nodes] `MeshFiltering` Rename option `input` to `inputMesh` - Rename option `output` to `outputMesh` --- meshroom/nodes/aliceVision/MeshFiltering.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meshroom/nodes/aliceVision/MeshFiltering.py b/meshroom/nodes/aliceVision/MeshFiltering.py index a413327742..4a1ae7ea41 100644 --- a/meshroom/nodes/aliceVision/MeshFiltering.py +++ b/meshroom/nodes/aliceVision/MeshFiltering.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc @@ -8,8 +8,8 @@ class MeshFiltering(desc.CommandLineNode): inputs = [ desc.File( - name='input', - label='Input', + name='inputMesh', + label='Input Mesh', description='''Input Mesh (OBJ file format).''', value='', uid=[0], @@ -59,8 +59,8 @@ class MeshFiltering(desc.CommandLineNode): outputs = [ desc.File( - name='output', - label='Output', + name='outputMesh', + label='Output Mesh', description='''Output mesh (OBJ file format).''', value=desc.Node.internalFolder + 'mesh.obj', uid=[], From 52379e4200fd3262154779cea868a1c99d3dbd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 24 Jan 2019 10:42:23 +0100 Subject: [PATCH 127/293] [nodes] `Texturing` Remove option `inputDenseReconstruction` --- meshroom/nodes/aliceVision/Texturing.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index e13cc449b5..92f18d837b 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -1,4 +1,4 @@ -__version__ = "3.0" +__version__ = "4.0" from meshroom.core import desc @@ -22,13 +22,6 @@ class Texturing(desc.CommandLineNode): value='', uid=[0], ), - desc.File( - name='inputDenseReconstruction', - label='Input Dense Reconstruction', - description='Path to the dense reconstruction result (mesh with per vertex visibility).', - value='', - uid=[0], - ), desc.File( name='inputMesh', label='Other Input Mesh', From a290e5da06d5017d837f6228ac355a56fa4b6664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 24 Jan 2019 10:44:12 +0100 Subject: [PATCH 128/293] [multiview] Update MVS pipeline description for using dense point cloud instead of binary file --- meshroom/multiview.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 766040334a..4e96900ae7 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -132,12 +132,11 @@ def mvsPipeline(graph, sfm=None): depthMapsFolder=depthMapFilter.depthMapsFolder, depthMapsFilterFolder=depthMapFilter.output) meshFiltering = graph.addNewNode('MeshFiltering', - input=meshing.output) + inputMesh=meshing.outputMesh) texturing = graph.addNewNode('Texturing', - input=meshing.input, + input=meshing.output, imagesFolder=depthMap.imagesFolder, - inputDenseReconstruction=meshing.outputDenseReconstruction, - inputMesh=meshFiltering.output) + inputMesh=meshFiltering.outputMesh) return [ prepareDenseScene, From e9969d5ebe7ac3472bcd1eca7079438346b6564a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Fri, 25 Jan 2019 16:27:06 +0100 Subject: [PATCH 129/293] [nodes] Add new node `ExportColoredPointCloud` --- .../aliceVision/ExportColoredPointCloud.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 meshroom/nodes/aliceVision/ExportColoredPointCloud.py diff --git a/meshroom/nodes/aliceVision/ExportColoredPointCloud.py b/meshroom/nodes/aliceVision/ExportColoredPointCloud.py new file mode 100644 index 0000000000..ced880b46e --- /dev/null +++ b/meshroom/nodes/aliceVision/ExportColoredPointCloud.py @@ -0,0 +1,36 @@ +__version__ = "1.0" + +from meshroom.core import desc + + +class ExportColoredPointCloud(desc.CommandLineNode): + commandLine = 'aliceVision_exportColoredPointCloud {allParams}' + + inputs = [ + desc.File( + name='input', + label='Input SfMData', + description='SfMData file containing a complete SfM.', + 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='output', + label='Output Point Cloud Filepath', + description='Output point cloud with visibilities as SfMData file.', + value="{cache}/{nodeType}/{uid0}/pointCloud.abc", + uid=[], + ), + ] From d92449947040a81b4f1d79078360395a27b4bef5 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Jan 2019 10:46:19 +0100 Subject: [PATCH 130/293] [nodes] sfm: rename param refineIntrinsics into lockAllIntrinsics --- meshroom/nodes/aliceVision/StructureFromMotion.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 8e426dd371..613bb5a7f7 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -169,13 +169,12 @@ class StructureFromMotion(desc.CommandLineNode): uid=[0], ), desc.BoolParam( - name='refineIntrinsics', - label='Refine intrinsic parameters.', - description='The intrinsics parameters of the cameras (focal length, \n' - 'principal point, distortion if any) are kept constant ' - 'during the reconstruction.\n' + name='lockAllIntrinsics', + label='Force Lock of All Intrinsic Camera Parameters.', + 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=True, + value=False, uid=[0], ), desc.File( From 349de9a3a602fe8d91bdef84d751731f059ee92c Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 28 Jan 2019 10:22:25 +0100 Subject: [PATCH 131/293] [nodes] expose estimator parameters on sfm and features matching --- meshroom/nodes/aliceVision/FeatureMatching.py | 11 ++++++++++ .../nodes/aliceVision/StructureFromMotion.py | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/meshroom/nodes/aliceVision/FeatureMatching.py b/meshroom/nodes/aliceVision/FeatureMatching.py index 0c487150d6..d032f876cc 100644 --- a/meshroom/nodes/aliceVision/FeatureMatching.py +++ b/meshroom/nodes/aliceVision/FeatureMatching.py @@ -105,6 +105,17 @@ class FeatureMatching(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.FloatParam( + name='geometricError', + label='Geometric Validation Error', + description='Maximum error (in pixels) allowed for features matching during geometric verification.\n' + 'If set to 0, it will select a threshold according to the localizer estimator used\n' + '(if ACRansac, it will analyze the input data to select the optimal value).', + value=0.0, + range=(0.0, 10.0, 0.1), + uid=[0], + advanced=True, + ), desc.IntParam( name='maxMatches', label='Max Matches', diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 387732e41c..1761e6d613 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -62,6 +62,26 @@ class StructureFromMotion(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.IntParam( + name='localizerEstimatorMaxIterations', + label='Localizer Max Ransac Iterations', + description='Maximum number of iterations allowed in ransac step.', + value=4096, + range=(1, 20000, 1), + uid=[0], + advanced=True, + ), + desc.FloatParam( + name='localizerEstimatorError', + label='Localizer Max Ransac Error', + description='Maximum error (in pixels) allowed for camera localization (resectioning).\n' + 'If set to 0, it will select a threshold according to the localizer estimator used\n' + '(if ACRansac, it will analyze the input data to select the optimal value).', + value=0.0, + range=(0.0, 100.0, 0.1), + uid=[0], + advanced=True, + ), desc.BoolParam( name='lockScenePreviouslyReconstructed', label='Lock Scene Previously Reconstructed', From 7cccea34f2e5e2f56a656941f458273f577dd27b Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 28 Jan 2019 11:17:25 +0100 Subject: [PATCH 132/293] [ui] reconstruction: fix missing boolean return value --- meshroom/ui/reconstruction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 6736bbdbdc..9cc02d7111 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -521,14 +521,14 @@ def setEndNode(self, node=None): @Slot(QObject, result=bool) def isInViews(self, viewpoint): if not viewpoint: - return + return False # keys are strings (faster lookup) return str(viewpoint.viewId.value) in self._views @Slot(QObject, result=bool) def isReconstructed(self, viewpoint): if not viewpoint: - return + return False # fetch up-to-date poseId from sfm result (in case of rigs, poseId might have changed) view = self._views.get(str(viewpoint.poseId.value), None) # keys are strings (faster lookup) return view.get('poseId', -1) in self._poses if view else False From 5d133461dbba71e4329b1bb39b1c316e036a53b8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 28 Jan 2019 18:03:11 +0100 Subject: [PATCH 133/293] [ui] ImageGallery: new ImageBadge component --- meshroom/ui/qml/ImageGallery/ImageBadge.qml | 15 +++++++++++++++ meshroom/ui/qml/ImageGallery/ImageGallery.qml | 9 +-------- .../ui/qml/ImageGallery/IntrinsicsIndicator.qml | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 meshroom/ui/qml/ImageGallery/ImageBadge.qml diff --git a/meshroom/ui/qml/ImageGallery/ImageBadge.qml b/meshroom/ui/qml/ImageGallery/ImageBadge.qml new file mode 100644 index 0000000000..7609be8cd6 --- /dev/null +++ b/meshroom/ui/qml/ImageGallery/ImageBadge.qml @@ -0,0 +1,15 @@ +import QtQuick 2.9 +import MaterialIcons 2.2 +import Utils 1.0 + + +/** + * ImageBadge is a preset MaterialLabel to display an icon bagdge on an image. + */ +MaterialLabel { + id: root + + font.pointSize: 10 + padding: 2 + background: Rectangle { color: Colors.sysPalette.window; opacity: 0.6 } +} diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index b4e33d516f..90440fc0dc 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -157,14 +157,10 @@ Panel { property string intrinsicInitMode: valid ? _reconstruction.getIntrinsicInitMode(object) : "" property bool inViews: valid && _reconstruction.sfmReport && _reconstruction.isInViews(object) - // Camera Initialization indicator IntrinsicsIndicator { intrinsicInitMode: parent.intrinsicInitMode metadata: imageDelegate.metadata - font.pointSize: 10 - padding: 2 - background: Rectangle { color: Colors.sysPalette.window; opacity: 0.6 } } Item { Layout.fillWidth: true } @@ -173,14 +169,11 @@ Panel { Loader { active: parent.inViews visible: active - sourceComponent: MaterialLabel { + sourceComponent: ImageBadge { property bool reconstructed: _reconstruction.sfmReport && _reconstruction.isReconstructed(model.object) text: reconstructed ? MaterialIcons.videocam : MaterialIcons.videocam_off color: reconstructed ? Colors.green : Colors.red - font.pointSize: 10 - padding: 2 ToolTip.text: "Camera: " + (reconstructed ? "" : "Not ") + "Reconstructed" - background: Rectangle { color: Colors.sysPalette.window; opacity: 0.6 } } } } diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml index 17e28d7898..7004676c62 100644 --- a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml +++ b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml @@ -8,7 +8,7 @@ import Utils 1.0 * Display camera initialization status and the value of metadata * that take part in this process. */ -MaterialLabel { +ImageBadge { id: root property string intrinsicInitMode From b73ddfc09cc1cef6d895cd23c5f6978728a63fe0 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 28 Jan 2019 18:06:30 +0100 Subject: [PATCH 134/293] [ui] ImageGallery: introduce Rig indicator Display rig indicator on Viewpoints that belongs to a rig. --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 90440fc0dc..0d590bbaeb 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -163,6 +163,20 @@ Panel { metadata: imageDelegate.metadata } + // Rig indicator + Loader { + id: rigIndicator + property int rigId: parent.valid ? object.value.get("rigId").value : -1 + active: rigId >= 0 + sourceComponent: ImageBadge { + property int rigSubPoseId: model.object.value.get("subPoseId").value + text: MaterialIcons.link + ToolTip.text: "Rig: Initialized
" + + "Rig ID: " + rigIndicator.rigId + "
" + + "SubPose: " + rigSubPoseId + } + } + Item { Layout.fillWidth: true } // Reconstruction status indicator From 88c43fe3165a2122e9cfb247c7e7f00dc91427a3 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 12:49:33 +0100 Subject: [PATCH 135/293] [bin/photogrammetry] fix missing import --- bin/meshroom_photogrammetry | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index bfd0fc3394..966c81d748 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -1,5 +1,6 @@ #!/usr/bin/env python import argparse +import os import meshroom meshroom.setupEnvironment() From 1cc2561a21517c97bab9af74f271a3a2a8eaeb81 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 14:36:37 +0100 Subject: [PATCH 136/293] [multiview] unify image files helper functions Move everything related to image files detection to the 'multiview' module. --- meshroom/multiview.py | 33 ++++++++++++++++++++------------- meshroom/ui/reconstruction.py | 17 ++++------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 9aa4a23f3a..ef4d320d6d 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -2,22 +2,29 @@ __version__ = "1.0" import os -import fnmatch -import re from meshroom.core.graph import Graph, GraphModification +# Supported image extensions +imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw') -def findFiles(folder, patterns): - rules = [re.compile(fnmatch.translate(pattern), re.IGNORECASE) for pattern in patterns] - outFiles = [] - for name in os.listdir(folder): - for rule in rules: - if rule.match(name): - filepath = os.path.join(folder, name) - outFiles.append(filepath) - break - return outFiles + +def isImageFile(filepath): + """ Return whether filepath is a path to an image file supported by Meshroom. """ + return os.path.splitext(filepath)[1].lower() in imageExtensions + + +def findImageFiles(folder): + """ + Return all files that are images in 'folder' based on their extensions. + + Args: + folder (str): the folder to look into + + Returns: + list: the list of image files. + """ + return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)] def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), inputIntrinsics=(), output=''): @@ -38,7 +45,7 @@ def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), inputIntr sfmNodes, mvsNodes = photogrammetryPipeline(graph) cameraInit = sfmNodes[0] if inputFolder: - images = findFiles(inputFolder, ['*.jpg', '*.png']) + images = findImageFiles(inputFolder) cameraInit.viewpoints.extend([{'path': image} for image in images]) if inputImages: cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 9cc02d7111..b71d80f676 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -99,8 +99,7 @@ def update(self): to include those images to the reconstruction. """ # Get all new images in the watched folder - filesInFolder = [os.path.join(self._folder, f) for f in os.listdir(self._folder)] - imagesInFolder = [f for f in filesInFolder if Reconstruction.isImageFile(f)] + imagesInFolder = multiview.findImageFiles(self._folder) newImages = set(imagesInFolder).difference(self.allImages) for imagePath in newImages: # print('[LiveSfmManager] New image file : {}'.format(imagePath)) @@ -160,8 +159,6 @@ class Reconstruction(UIGraph): Specialization of a UIGraph designed to manage a 3D reconstruction. """ - imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw') - def __init__(self, graphFilepath='', parent=None): super(Reconstruction, self).__init__(graphFilepath, parent) self._buildingIntrinsics = False @@ -332,11 +329,6 @@ def handleFilesDrop(self, drop, cameraInit): """ self.importImages(self.getImageFilesFromDrop(drop), cameraInit) - @staticmethod - def isImageFile(filepath): - """ Return whether filepath is a path to an image file supported by Meshroom. """ - return os.path.splitext(filepath)[1].lower() in Reconstruction.imageExtensions - @staticmethod def getImageFilesFromDrop(drop): urls = drop.property("urls") @@ -345,10 +337,9 @@ def getImageFilesFromDrop(drop): for url in urls: localFile = url.toLocalFile() if os.path.isdir(localFile): # get folder content - files = [os.path.join(localFile, f) for f in os.listdir(localFile)] - else: - files = [localFile] - images.extend([f for f in files if Reconstruction.isImageFile(f)]) + images.extend(multiview.findImageFiles(localFile)) + elif multiview.isImageFile(localFile): + images.append(localFile) return images def importImages(self, images, cameraInit): From c6a3f0d58718de519d6ef50789b8c640f1e7f9c8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 14:56:24 +0100 Subject: [PATCH 137/293] [nodes] CameraInit.buildInstrincs: copy node outside graph if necessary Instead of assuming that the CameraInit instance does not belong to a graph, handle this by making a temporary copy of it to work with. --- meshroom/nodes/aliceVision/CameraInit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/CameraInit.py b/meshroom/nodes/aliceVision/CameraInit.py index 45ec15d881..733f716764 100644 --- a/meshroom/nodes/aliceVision/CameraInit.py +++ b/meshroom/nodes/aliceVision/CameraInit.py @@ -171,7 +171,10 @@ def buildIntrinsics(self, node, additionalViews=()): The updated views and intrinsics as two separate lists """ assert isinstance(node.nodeDesc, CameraInit) - assert node.graph is None + if node.graph: + # make a copy of the node outside the graph + # to change its cache folder without modifying the original node + node = node.graph.copyNode(node)[0] tmpCache = tempfile.mkdtemp() node.updateInternals(tmpCache) From 44371211d5896dca89e09ad68df1bfcd7154497f Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 15:06:58 +0100 Subject: [PATCH 138/293] [bin][photogrammetry] initialize CameraInit node using 'buildIntrinsics' * build the complete image files list in meshroom_photogrammetry from input arguments * initialize CameraInit with input sfm data (views/intrinsics) if any * call buildIntrinsics with the resolved input images list --- bin/meshroom_photogrammetry | 31 +++++++++++++++++++++---------- meshroom/multiview.py | 15 ++++----------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 966c81d748..8ad506db82 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -6,14 +6,14 @@ import meshroom meshroom.setupEnvironment() import meshroom.core.graph -from meshroom.nodes.aliceVision.CameraInit import readSfMData from meshroom import multiview parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.') -parser.add_argument('--input', metavar='FOLDER', type=str, +parser.add_argument('--input', metavar='FOLDER_OR_SFM', type=str, default='', - help='Input folder or json file.') + help='Input folder with images or SfM file (.sfm, .json).') parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', + default=[], help='Input images.') parser.add_argument('--output', metavar='FOLDER', type=str, required=True, help='Output folder.') @@ -45,17 +45,28 @@ if not args.input and not args.inputImages: print('Nothing to compute. You need to set --input or --inputImages.') exit(1) -if args.input and os.path.isfile(args.input): - views, intrinsics = readSfMData(args.input) - # print(views) - # print(intrinsics) - graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, inputImages=args.inputImages, output=args.output) -else: - graph = multiview.photogrammetry(inputFolder=args.input, inputImages=args.inputImages, output=args.output) +views, intrinsics = [], [] +# Build image files list from inputImages arguments +images = [f for f in args.inputImages if multiview.isImageFile(f)] + +if os.path.isdir(args.input): + # args.input is a folder: extend images list with images in that folder + images += multiview.findImageFiles(args.input) +elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json', '.sfm'): + # args.input is a sfmData file: setup pre-calibrated views and intrinsics + from meshroom.nodes.aliceVision.CameraInit import readSfMData + views, intrinsics = readSfMData(args.input) + +graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) graph.findNode('DepthMap_1').downscale.value = args.scale +cameraInit = graph.findNode('CameraInit') +views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) +cameraInit.viewpoints.value = views +cameraInit.intrinsics.value = intrinsics + if args.save: graph.save(args.save) print('File successfully saved:', args.save) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index ef4d320d6d..307a4f1af1 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -27,12 +27,11 @@ def findImageFiles(folder): return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)] -def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), inputIntrinsics=(), output=''): +def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output=''): """ Create a new Graph with a complete photogrammetry pipeline. Args: - inputFolder (str, optional): folder containing image files inputImages (list of str, optional): list of image file paths inputViewpoints (list of Viewpoint, optional): list of Viewpoints output (str, optional): the path to export reconstructed model to @@ -44,15 +43,9 @@ def photogrammetry(inputFolder='', inputImages=(), inputViewpoints=(), inputIntr with GraphModification(graph): sfmNodes, mvsNodes = photogrammetryPipeline(graph) cameraInit = sfmNodes[0] - if inputFolder: - images = findImageFiles(inputFolder) - cameraInit.viewpoints.extend([{'path': image} for image in images]) - if inputImages: - cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) - if inputViewpoints: - cameraInit.viewpoints.extend(inputViewpoints) - if inputIntrinsics: - cameraInit.intrinsics.extend(inputIntrinsics) + cameraInit.viewpoints.extend([{'path': image} for image in inputImages]) + cameraInit.viewpoints.extend(inputViewpoints) + cameraInit.intrinsics.extend(inputIntrinsics) if output: texturing = mvsNodes[-1] From 6276dfcd30029518837af186979cb0c6603d9ab3 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 15:10:01 +0100 Subject: [PATCH 139/293] [bin][photogrammetry] clean up parameters * use choices for '--scale' parameter + fix doc + apply only if set * fix metavars --- bin/meshroom_photogrammetry | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 8ad506db82..ba7c0ed8e8 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -18,11 +18,13 @@ parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', parser.add_argument('--output', metavar='FOLDER', type=str, required=True, help='Output folder.') -parser.add_argument('--save', metavar='FOLDER', type=str, required=False, - help='Save the workflow to a meshroom files (instead of running it).') +parser.add_argument('--save', metavar='FILE', type=str, required=False, + help='Save the resulting pipeline to a Meshroom file (instead of running it).') -parser.add_argument('--scale', type=int, default=2, - help='Downscale factor for MVS steps. Possible values are: 1, 2, 4, 8, 16.') +parser.add_argument('--scale', type=int, default=-1, + choices=[-1, 1, 2, 4, 8, 16], + help='Downscale factor override for DepthMap estimation. ' + 'By default (-1): use pipeline default value.') parser.add_argument('--toNode', metavar='NODE', type=str, nargs='*', default=None, @@ -60,7 +62,8 @@ elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json' graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) -graph.findNode('DepthMap_1').downscale.value = args.scale +if args.scale > 0: + graph.findNode('DepthMap_1').downscale.value = args.scale cameraInit = graph.findNode('CameraInit') views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) From 026cd36ff464fefaa6ca6ee5932fb046f00b00a4 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 15:12:55 +0100 Subject: [PATCH 140/293] [core.graph] update graph after load Ensure graph internal data is updated after loading. --- meshroom/core/graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index defc1e95f0..858820ef02 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1078,6 +1078,7 @@ def loadGraph(filepath): """ graph = Graph("") graph.load(filepath) + graph.update() return graph From 60dc02ea746d5d9f47fad7445ebe0ffe03654378 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 15:34:27 +0100 Subject: [PATCH 141/293] [bin][photogrammetry] enable to input a pre-configured pipeline graph --- bin/meshroom_photogrammetry | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index ba7c0ed8e8..486375711d 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -15,6 +15,11 @@ parser.add_argument('--input', metavar='FOLDER_OR_SFM', type=str, parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', default=[], help='Input images.') + +parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, + help='Meshroom file containing a pre-configured photogrammetry pipeline to run on input images. ' + 'Requirements: the graph must contain one CameraInit and one Publish node.') + parser.add_argument('--output', metavar='FOLDER', type=str, required=True, help='Output folder.') @@ -47,6 +52,9 @@ if not args.input and not args.inputImages: print('Nothing to compute. You need to set --input or --inputImages.') exit(1) +if args.pipeline and not args.output: + print('--output must be set when using --pipeline.') + exit(1) views, intrinsics = [], [] # Build image files list from inputImages arguments @@ -60,7 +68,29 @@ elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json' from meshroom.nodes.aliceVision.CameraInit import readSfMData views, intrinsics = readSfMData(args.input) -graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) +# initialize photogrammetry pipeline +if args.pipeline: + # custom pipeline + graph = meshroom.core.graph.loadGraph(args.pipeline) + try: + cameraInit = graph.findNode('CameraInit') + # reset graph inputs + cameraInit.viewpoints.resetValue() + cameraInit.intrinsics.resetValue() + except KeyError: + raise RuntimeError("meshroom_photogrammetry requires a pipeline graph with exactly one 'CameraInit' node.") + + if not graph.canComputeLeaves: + raise RuntimeError("Graph cannot be computed. Check for compatibility issues.") + + try: + publish = graph.findNode('Publish') + publish.output.value = args.output + except KeyError: + raise RuntimeError("meshroom_photogrammetry requires a pipeline graph with exactly one 'Publish' node.") +else: + # default pipeline + graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) if args.scale > 0: graph.findNode('DepthMap_1').downscale.value = args.scale From 26cb361ecd509073a58b347c64baf17099a07dfa Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 25 Jan 2019 16:42:58 +0100 Subject: [PATCH 142/293] [ui] custom MessageHandler to turn Qt messages into Python logs + filter out inoffensive but non silenced messages coming from QML even when 'outputWarningsToStandardError' is set to False on the QML engine --- meshroom/ui/app.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 6d64a6d484..9e5131dc27 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -1,7 +1,7 @@ import logging import os -from PySide2.QtCore import Qt, Slot, QJsonValue, Property +from PySide2.QtCore import Qt, Slot, QJsonValue, Property, qInstallMessageHandler, QtMsgType from PySide2.QtGui import QIcon from PySide2.QtWidgets import QApplication @@ -15,12 +15,47 @@ from meshroom.ui.utils import QmlInstantEngine +class MessageHandler(object): + """ + MessageHandler that translates Qt logs to Python logging system. + Also contains and filters a list of blacklisted QML warnings that end up in the + standard error even when setOutputWarningsToStandardError is set to false on the engine. + """ + + outputQmlWarnings = bool(os.environ.get("MESHROOM_OUTPUT_QML_WARNINGS", False)) + + logFunctions = { + QtMsgType.QtDebugMsg: logging.debug, + QtMsgType.QtWarningMsg: logging.warning, + QtMsgType.QtInfoMsg: logging.info, + QtMsgType.QtFatalMsg: logging.fatal, + QtMsgType.QtCriticalMsg: logging.critical, + QtMsgType.QtSystemMsg: logging.critical + } + + # Warnings known to be inoffensive and related to QML but not silenced + # even when 'MESHROOM_OUTPUT_QML_WARNINGS' is set to False + qmlWarningsBlacklist = ( + 'Failed to download scene at QUrl("")', + 'QVariant(Invalid) Please check your QParameters', + 'Texture will be invalid for this frame', + ) + + @classmethod + def handler(cls, messageType, context, message): + """ Message handler remapping Qt logs to Python logging system. """ + # discard blacklisted Qt messages related to QML when 'output qml warnings' is set to false + if not cls.outputQmlWarnings and any(w in message for w in cls.qmlWarningsBlacklist): + return + MessageHandler.logFunctions[messageType](message) + + class MeshroomApp(QApplication): """ Meshroom UI Application. """ def __init__(self, args): args = [args[0], '-style', 'fusion'] + args[1:] # force Fusion style by default - super(MeshroomApp, self).__init__(args) + self.setOrganizationName('AliceVision') self.setApplicationName('Meshroom') self.setAttribute(Qt.AA_EnableHighDpiScaling) @@ -40,7 +75,9 @@ def __init__(self, args): self.engine.addFilesFromDirectory(qmlDir, recursive=True) self.engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False)) # whether to output qml warnings to stderr (disable by default) - self.engine.setOutputWarningsToStandardError(bool(os.environ.get("MESHROOM_OUTPUT_QML_WARNINGS", False))) + self.engine.setOutputWarningsToStandardError(MessageHandler.outputQmlWarnings) + qInstallMessageHandler(MessageHandler.handler) + self.engine.addImportPath(qmlDir) components.registerTypes() From fc3e17e6d4dcb2d48d3390d6eedcebc89ec19013 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 25 Jan 2019 16:46:52 +0100 Subject: [PATCH 143/293] [ui] Graph: force '.mg' extensions on saved files Depending on the 'Save File Dialog' used (OS/Qt fallback implementation), it is possible to save the meshroom graph with another extension, making it impossible to reload it via File>Open (due to the filter allowing only *.mg files). --- meshroom/ui/graph.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 00470e1c1b..ea61d8a347 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -257,7 +257,11 @@ def loadUrl(self, url): @Slot(QUrl) def saveAs(self, url): - self._graph.save(url.toLocalFile()) + localFile = url.toLocalFile() + # ensure file is saved with ".mg" extension + if os.path.splitext(localFile)[-1] != ".mg": + localFile += ".mg" + self._graph.save(localFile) self._undoStack.setClean() @Slot() From 4d6195cf81f68fe61315010ebb9c70c1b0e044ab Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 25 Jan 2019 18:10:32 +0100 Subject: [PATCH 144/293] [ui] Node: use TextMetrics to expand elided attribute pins at hover --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 46 ++++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 73547b0fcd..27f78f0e1d 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -134,30 +134,38 @@ RowLayout { point1y: parent.width / 2 point2x: dragTarget.x + dragTarget.width/2 point2y: dragTarget.y + dragTarget.height/2 - color: nameLabel.palette.text + color: nameLabel.color } } // Attribute name - Label { - id: nameLabel - text: attribute.name - elide: Text.ElideMiddle + Item { + id: nameContainer Layout.fillWidth: true - font.pointSize: 5 - horizontalAlignment: attribute.isOutput ? Text.AlignRight : Text.AlignLeft - - Loader { - active: parent.truncated && (connectMA.containsMouse || connectMA.drag.active || dropArea.containsDrag) - anchors.right: root.layoutDirection == Qt.LeftToRight ? undefined : nameLabel.right - // Non-elided label - sourceComponent: Label { - leftPadding: root.layoutDirection == Qt.LeftToRight ? 0 : 1 - rightPadding: root.layoutDirection == Qt.LeftToRight ? 1 : 0 - text: attribute.name - background: Rectangle { - color: parent.palette.window - } + implicitHeight: childrenRect.height + + TextMetrics { + id: metrics + property bool truncated: width > nameContainer.width + text: nameLabel.text + font: nameLabel.font + } + + Label { + id: nameLabel + + property bool hovered: (connectMA.containsMouse || connectMA.drag.active || dropArea.containsDrag) + text: attribute.name + elide: hovered ? Text.ElideNone : Text.ElideMiddle + width: hovered ? contentWidth : parent.width + font.pointSize: 5 + horizontalAlignment: attribute.isOutput ? Text.AlignRight : Text.AlignLeft + anchors.right: attribute.isOutput ? parent.right : undefined + + background: Rectangle { + visible: parent.hovered && metrics.truncated + anchors { fill: parent; leftMargin: -1; rightMargin: -1 } + color: parent.palette.window } } } From 80400531d5970b11cfb01eef5cab965e3fcb9cdd Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 29 Jan 2019 16:15:49 +0100 Subject: [PATCH 145/293] [ui] IntrinsicsIndicator: use 'var' to store metadata avoid warnings when a meta is not available and we get an "undefined" value --- .../ui/qml/ImageGallery/IntrinsicsIndicator.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml index 7004676c62..b65d2fdc5d 100644 --- a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml +++ b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml @@ -15,14 +15,14 @@ ImageBadge { property var metadata: ({}) // access useful metadata - readonly property string make: metadata["Make"] - readonly property string model: metadata["Model"] - readonly property string focalLength: metadata["Exif:FocalLength"] - readonly property string focalLength35: metadata["Exif:FocalLengthIn35mmFilm"] - readonly property string bodySerialNumber: metadata["Exif:BodySerialNumber"] - readonly property string lensSerialNumber: metadata["Exif:LensSerialNumber"] - readonly property string sensorWidth: metadata["AliceVision:SensorWidth"] - readonly property string sensorWidthEstimation: metadata["AliceVision:SensorWidthEstimation"] + readonly property var make: metadata["Make"] + readonly property var model: metadata["Model"] + readonly property var focalLength: metadata["Exif:FocalLength"] + readonly property var focalLength35: metadata["Exif:FocalLengthIn35mmFilm"] + readonly property var bodySerialNumber: metadata["Exif:BodySerialNumber"] + readonly property var lensSerialNumber: metadata["Exif:LensSerialNumber"] + readonly property var sensorWidth: metadata["AliceVision:SensorWidth"] + readonly property var sensorWidthEstimation: metadata["AliceVision:SensorWidthEstimation"] property string statusText: "" property string detailsText: "" From 22a7e79830d20f40477052ce0097c36d2ef21145 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 30 Jan 2019 09:22:03 +0100 Subject: [PATCH 146/293] [bin][photogrammetry] set downscale on all DepthMap nodes --- bin/meshroom_photogrammetry | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 486375711d..4a3ae99d06 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -92,8 +92,10 @@ else: # default pipeline graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) +# setup DepthMap downscaling if args.scale > 0: - graph.findNode('DepthMap_1').downscale.value = args.scale + for node in graph.nodesByType('DepthMap'): + node.downscale.value = args.scale cameraInit = graph.findNode('CameraInit') views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) From 75cb5cb77978d5365edb4be84ed30db57f090eaf Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 30 Jan 2019 09:29:42 +0100 Subject: [PATCH 147/293] [bin][photogrammetry] allow no --output + improve help Allow no --output parameter, explicitly print cache folder before computation start in that case. --- bin/meshroom_photogrammetry | 80 +++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 4a3ae99d06..86003d4db0 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -11,20 +11,29 @@ from meshroom import multiview parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.') parser.add_argument('--input', metavar='FOLDER_OR_SFM', type=str, default='', - help='Input folder with images or SfM file (.sfm, .json).') + help='Input folder containing images or file (.sfm or .json) ' + 'with images paths and optionally predefined camera intrinsics.') parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', default=[], help='Input images.') parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, help='Meshroom file containing a pre-configured photogrammetry pipeline to run on input images. ' - 'Requirements: the graph must contain one CameraInit and one Publish node.') + 'If not set, the default photogrammetry pipeline will be used. ' + 'Requirements: the graph must contain one CameraInit node, ' + 'and one Publish node if --output is set.') -parser.add_argument('--output', metavar='FOLDER', type=str, required=True, - help='Output folder.') +parser.add_argument('--output', metavar='FOLDER', type=str, required=False, + help='Output folder where results should be copied to. ' + 'If not set, results will have to be retrieved directly from the cache folder.') + +parser.add_argument('--cache', metavar='FOLDER', type=str, + default=None, + help='Custom cache folder to write computation results. ' + 'If not set, the default cache folder will be used: ' + meshroom.core.defaultCacheFolder) parser.add_argument('--save', metavar='FILE', type=str, required=False, - help='Save the resulting pipeline to a Meshroom file (instead of running it).') + help='Save the configured Meshroom project to a file (instead of running it).') parser.add_argument('--scale', type=int, default=-1, choices=[-1, 1, 2, 4, 8, 16], @@ -34,9 +43,7 @@ parser.add_argument('--scale', type=int, default=-1, parser.add_argument('--toNode', metavar='NODE', type=str, nargs='*', default=None, help='Process the node(s) with its dependencies.') -parser.add_argument('--cache', metavar='FOLDER', type=str, - default=None, - help='Choose a custom cache folder') + parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.', action='store_true') parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.', @@ -44,18 +51,20 @@ parser.add_argument('--forceCompute', help='Compute in all cases even if already args = parser.parse_args() -if not args.output and not args.save: - print('Nothing to do. You need to set --output or --save.') - exit(1) + +def getOnlyNodeOfType(g, nodeType): + """ Helper function to get a node of 'nodeType' in the graph 'g' and raise if no or multiple candidates. """ + nodes = g.nodesByType(nodeType) + if len(nodes) != 1: + raise RuntimeError("meshroom_photogrammetry requires a pipeline graph with exactly one '{}' node, {} found." + .format(nodeType, len(nodes))) + return nodes[0] + if not args.input and not args.inputImages: print('Nothing to compute. You need to set --input or --inputImages.') exit(1) -if args.pipeline and not args.output: - print('--output must be set when using --pipeline.') - exit(1) - views, intrinsics = [], [] # Build image files list from inputImages arguments images = [f for f in args.inputImages if multiview.isImageFile(f)] @@ -72,49 +81,44 @@ elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json' if args.pipeline: # custom pipeline graph = meshroom.core.graph.loadGraph(args.pipeline) - try: - cameraInit = graph.findNode('CameraInit') - # reset graph inputs - cameraInit.viewpoints.resetValue() - cameraInit.intrinsics.resetValue() - except KeyError: - raise RuntimeError("meshroom_photogrammetry requires a pipeline graph with exactly one 'CameraInit' node.") + cameraInit = getOnlyNodeOfType(graph, 'CameraInit') + # reset graph inputs + cameraInit.viewpoints.resetValue() + cameraInit.intrinsics.resetValue() if not graph.canComputeLeaves: raise RuntimeError("Graph cannot be computed. Check for compatibility issues.") - try: - publish = graph.findNode('Publish') + if args.output: + publish = getOnlyNodeOfType(graph, 'Publish') publish.output.value = args.output - except KeyError: - raise RuntimeError("meshroom_photogrammetry requires a pipeline graph with exactly one 'Publish' node.") else: # default pipeline graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) + cameraInit = getOnlyNodeOfType(graph, 'CameraInit') + +views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) +cameraInit.viewpoints.value = views +cameraInit.intrinsics.value = intrinsics # setup DepthMap downscaling if args.scale > 0: for node in graph.nodesByType('DepthMap'): node.downscale.value = args.scale -cameraInit = graph.findNode('CameraInit') -views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) -cameraInit.viewpoints.value = views -cameraInit.intrinsics.value = intrinsics - if args.save: graph.save(args.save) print('File successfully saved:', args.save) exit(0) -if args.cache: - graph.cacheDir = args.cache +# setup cache directory +graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder -if not graph.cacheDir: - graph.cacheDir = meshroom.core.defaultCacheFolder +if not args.output: + print('No output set, results will be available in {}'.format(graph.cacheDir)) -toNodes = None -if args.toNode: - toNodes = graph.findNodes(args.toNode) +# find end nodes (None will compute all graph) +toNodes = graph.findNodes(args.toNode) if args.toNode else None +# start computation meshroom.core.graph.executeGraph(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus) From 905ed48cf9455b00b9a47ea30e382b0ca8ec3f94 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 30 Jan 2019 09:36:02 +0100 Subject: [PATCH 148/293] [core] fix flake8 error remove type annotations --- 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 6736449d2e..36e0b31132 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -337,8 +337,8 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): self.packageName = self.packageVersion = "" self._internalFolder = "" - self._name = None # type: str - self.graph = None # type: Graph + self._name = None + self.graph = None self.dirty = True # whether this node's outputs must be re-evaluated on next Graph update self._chunks = ListModel(parent=self) self._uids = dict() From 28e7e64f3d2a73c42f63c1608ae07060e9e3e153 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 30 Jan 2019 09:47:07 +0100 Subject: [PATCH 149/293] [ui] show unsaved project warning dialog also when computing from GraphEditor --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 7 +- meshroom/ui/qml/main.qml | 97 +++++++++++++-------- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index f1b5c35e3d..b87fc085d5 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -21,7 +21,10 @@ Item { // signals signal workspaceMoved() signal workspaceClicked() + signal nodeDoubleClicked(var mouse, var node) + signal computeRequest(var node) + signal submitRequest(var node) // trigger initial fit() after initialization // (ensure GraphEditor has its final size) @@ -279,14 +282,14 @@ Item { MenuItem { text: "Compute" enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode - onTriggered: uigraph.execute(nodeMenu.currentNode) + onTriggered: computeRequest(nodeMenu.currentNode) } MenuItem { text: "Submit" enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode visible: uigraph.canSubmit height: visible ? implicitHeight : 0 - onTriggered: uigraph.submit(nodeMenu.currentNode) + onTriggered: submitRequest(nodeMenu.currentNode) } MenuItem { text: "Open Folder" diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 6cdea81f21..164dd224ec 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -139,24 +139,66 @@ ApplicationWindow { onRejected: closed(Platform.Dialog.Rejected) } - MessageDialog { - id: unsavedComputeDialog + Item { + id: computeManager - canCopy: false - icon.text: MaterialIcons.warning - preset: "Warning" - title: "Unsaved Project" - text: "Data will be computed in the default cache folder if project remains unsaved." - detailedText: "Default cache folder: " + _reconstruction.graph.cacheDir - helperText: "Save project first?" - standardButtons: Dialog.Discard | Dialog.Cancel | Dialog.Save - Component.onCompleted: { - // set up discard button text - standardButton(Dialog.Discard).text = "Continue without Saving" + property bool warnIfUnsaved: true + + // evaluate if global reconstruction computation can be started + property bool canStartComputation: _reconstruction.viewpoints.count >= 2 // at least 2 images + && !_reconstruction.computing // computation is not started + && _reconstruction.graph.canComputeLeaves // graph has no uncomputable nodes + + // evaluate if graph computation can be submitted externally + property bool canSubmit: _reconstruction.canSubmit // current setup allows to compute externally + && canStartComputation // can be computed + && _reconstruction.graph.filepath // graph is saved on disk + + function compute(node, force) { + if(!force && warnIfUnsaved && !_reconstruction.graph.filepath) + { + unsavedComputeDialog.currentNode = node; + unsavedComputeDialog.open(); + } + else + _reconstruction.execute(node); } - onDiscarded: { close(); _reconstruction.execute(null) } - onAccepted: saveAsAction.trigger() + function submit(node) { + _reconstruction.submit(node); + } + + + MessageDialog { + id: unsavedComputeDialog + + property var currentNode: null + + canCopy: false + icon.text: MaterialIcons.warning + parent: Overlay.overlay + preset: "Warning" + title: "Unsaved Project" + text: "Data will be computed in the default cache folder if project remains unsaved." + detailedText: "Default cache folder: " + _reconstruction.graph.cacheDir + helperText: "Save project first?" + standardButtons: Dialog.Discard | Dialog.Cancel | Dialog.Save + + CheckBox { + Layout.alignment: Qt.AlignRight + text: "Don't ask again for this session" + padding: 0 + onToggled: computeManager.warnIfUnsaved = !checked + } + + Component.onCompleted: { + // set up discard button text + standardButton(Dialog.Discard).text = "Continue without Saving" + } + + onDiscarded: { close(); computeManager.compute(currentNode, true) } + onAccepted: saveAsAction.trigger() + } } Platform.FileDialog { @@ -418,16 +460,6 @@ ApplicationWindow { Item { Layout.fillWidth: true } Row { - // evaluate if global reconstruction computation can be started - property bool canStartComputation: _reconstruction.viewpoints.count >= 2 // at least 2 images - && !_reconstruction.computing // computation is not started - && _reconstruction.graph.canComputeLeaves // graph has no uncomputable nodes - - // evaluate if graph computation can be submitted externally - property bool canSubmit: _reconstruction.canSubmit // current setup allows to compute externally - && canStartComputation // can be computed - && _reconstruction.graph.filepath // graph is saved on disk - // disable controls if graph is executed externally enabled: !_reconstruction.computingExternally Layout.alignment: Qt.AlignHCenter @@ -438,13 +470,8 @@ ApplicationWindow { palette.button: enabled ? buttonColor : disabledPalette.button palette.window: enabled ? buttonColor : disabledPalette.window palette.buttonText: enabled ? "white" : disabledPalette.buttonText - enabled: parent.canStartComputation - onClicked: { - if(!_reconstruction.graph.filepath) - unsavedComputeDialog.open() - else - _reconstruction.execute(null) - } + enabled: computeManager.canStartComputation + onClicked: computeManager.compute(null) } Button { text: "Stop" @@ -454,9 +481,9 @@ ApplicationWindow { Item { width: 20; height: 1 } Button { visible: _reconstruction.canSubmit - enabled: parent.canSubmit + enabled: computeManager.canSubmit text: "Submit" - onClicked: _reconstruction.submit(null) + onClicked: computeManager.submit(null) } } Item { Layout.fillWidth: true; Layout.fillHeight: true } @@ -573,6 +600,8 @@ ApplicationWindow { } } } + onComputeRequest: computeManager.compute(node) + onSubmitRequest: computeManager.submit(node) } } From b75ed86fa171fa8f93389ee5c6d1cdc71e300d86 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 09:52:52 +0100 Subject: [PATCH 150/293] [nodes][aliceVision] bump FeatureMatching version attributes have been changed since last release, without version being increased --- meshroom/nodes/aliceVision/FeatureMatching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/FeatureMatching.py b/meshroom/nodes/aliceVision/FeatureMatching.py index d032f876cc..43f1e21608 100644 --- a/meshroom/nodes/aliceVision/FeatureMatching.py +++ b/meshroom/nodes/aliceVision/FeatureMatching.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc From 10c3454bad8d48524e8af34726920ec777359f47 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 15:58:23 +0100 Subject: [PATCH 151/293] [ui][3D] MediaLoader: bind 'enabled' property of AlembicEntity Ensure AlembicEntity is properly disabled when parent is. Fix pickable hidden cameras issue. --- meshroom/ui/qml/Viewer3D/MediaLoader.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index 59e049aba1..9cf51ddd6a 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -86,7 +86,8 @@ import Utils 1.0 var obj = Viewer3DSettings.abcLoaderComp.createObject(abcLoaderEntity, { 'source': source, 'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }), - 'locatorScale': Qt.binding(function() { return Viewer3DSettings.cameraScale }) + 'locatorScale': Qt.binding(function() { return Viewer3DSettings.cameraScale }), + 'enabled': Qt.binding(function() { return root.enabled }) }); obj.statusChanged.connect(function() { From 6e6114ca834cef3b0febf215a64ef9e4913a8ed4 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 31 Jan 2019 16:01:05 +0100 Subject: [PATCH 152/293] Allow version override from env variable --- meshroom/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index 78d9816eb2..0c00415dbe 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -1,5 +1,9 @@ __version__ = "2018.1.0" +import os +# Allow override from env variable +__version__ = os.environ.get("REZ_MESHROOM_VERSION", __version__) + import logging from enum import Enum From 6a297d5bff352a45a1ae90230450c74f251de835 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 16:07:33 +0100 Subject: [PATCH 153/293] [ui][Viewer3D] fix cache update and reparenting issues * don't try to put object in cache on MediaLoader 'Component.onCompleted' callback ; only effect was to put back in cache a media retrieved from cache during 'onFinalSourceChanged' * redesign 'updateModelAndCache' as 'updateModel' and 'updateCacheAndModel' for this purpose * wait for MediaLoader full initialization (@ 'NodeInstiator.onObjectAdded' callback) to reparent an object retrieved from cache; otherwise it silently fails. --- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 34 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index 1decce67fd..57951a743c 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -146,6 +146,9 @@ Entity { delegate: MediaLoader { id: mediaLoader + // whether MediaLoader has been fully instantiated by the NodeInstantiator + property bool fullyInstantiated: false + // explicitely store some attached model properties for outside use and ease binding readonly property var attribute: model.attribute readonly property int idx: index @@ -192,13 +195,17 @@ Entity { model.requested = false; } - function updateModelAndCache(forceRequest) { + function updateCacheAndModel(forceRequest) { // don't cache explicitely unloaded media if(model.requested && object && dependencyReady) { // cache current object if(cache.add(Filepath.urlToString(mediaLoader.source), object)); object = null; } + updateModel(forceRequest); + } + + function updateModel(forceRequest) { // update model's source path if input is an attribute if(attribute) { model.source = rawSource; @@ -212,13 +219,15 @@ Entity { // keep 'source' -> 'entity' reference m.sourceToEntity[modelSource] = mediaLoader; // always request media loading when delegate has been created - updateModelAndCache(true); + updateModel(true); // if external media failed to open, remove element from model if(!attribute && !object) remove(index) } - onCurrentSourceChanged: updateModelAndCache(false) + onCurrentSourceChanged: { + updateCacheAndModel(false) + } onFinalSourceChanged: { // update media visibility @@ -229,7 +238,12 @@ Entity { cached = cachedObject !== undefined; if(cached) { object = cachedObject; - object.parent = mediaLoader; + // only change cached object parent if mediaLoader has been fully instantiated + // by the NodeInstantiator; otherwise re-parenting will fail silently and the object will disappear... + // see "onFullyInstantiatedChanged" and parent NodeInstantiator's "onObjectAdded" + if(fullyInstantiated) { + object.parent = mediaLoader; + } } mediaLoader.source = Filepath.stringToUrl(finalSource); if(object) { @@ -246,6 +260,12 @@ Entity { } } + onFullyInstantiatedChanged: { + // delayed reparenting of object coming from the cache + if(object) + object.parent = mediaLoader; + } + onStatusChanged: { model.status = status // remove model entry for external media that failed to load @@ -261,6 +281,12 @@ Entity { } ] } + + onObjectAdded: { + // notify object that it is now fully instantiated + object.fullyInstantiated = true; + } + onObjectRemoved: { delete m.sourceToEntity[object.modelSource]; } From 971d4d7f0edba6c09e1ea57a3b4ebb451bea74c5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 16:10:53 +0100 Subject: [PATCH 154/293] [ui][Viewer3D] MediaCache: add debug mode add 'debug' mode to ease debugging of 3D Media cache --- meshroom/ui/qml/Viewer3D/MediaCache.qml | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaCache.qml b/meshroom/ui/qml/Viewer3D/MediaCache.qml index 5531b52575..863e03148a 100644 --- a/meshroom/ui/qml/Viewer3D/MediaCache.qml +++ b/meshroom/ui/qml/Viewer3D/MediaCache.qml @@ -1,3 +1,4 @@ +import QtQuick 2.9 import Qt3D.Core 2.1 import Qt3D.Render 2.1 @@ -6,8 +7,35 @@ import Utils 1.0 Entity { id: root + /// Enable debug mode (show cache entity with a scale applied) + property bool debug: false + enabled: false // disabled entity + components: [ + Transform { + id: transform + scale: 1 + } + ] + + StateGroup { + states: [ + State { + when: root.debug + name: "debug" + PropertyChanges { + target: root + enabled: true + } + PropertyChanges { + target: transform + scale: 0.2 + } + } + ] + } + property int cacheSize: 2 property var mediaCache: {[]} @@ -27,7 +55,7 @@ Entity { return false; if(contains(source)) return true; - // console.debug("[cache] add: " + source) + if(debug) { console.log("[cache] add: " + source); } mediaCache[source] = object; object.parent = root; // remove oldest entry in cache @@ -43,10 +71,11 @@ Entity { var obj = mediaCache[source]; delete mediaCache[source]; - // console.debug("[cache] pop: " + source) + if(debug) { console.log("[cache] pop: " + source); } // delete cached obj if file does not exist on disk anymore if(!Filepath.exists(source)) { + if(debug){ console.log("[cache] destroy: " + source); } obj.destroy(); obj = undefined; } From 7546e9ad1000caf1e40540ba42352e8954c3aa67 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 17:35:29 +0100 Subject: [PATCH 155/293] [ui] IntrinsicsIndicator: display distortion model Directly use intrinsic attribute in IntrinsicsIndicator to retrieve useful values, instead of dedicated methods in 'Reconstruction'. --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 3 +-- .../qml/ImageGallery/IntrinsicsIndicator.qml | 14 +++++++++++-- meshroom/ui/reconstruction.py | 21 ++----------------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 0d590bbaeb..c10156ef8c 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -154,12 +154,11 @@ Panel { spacing: 2 property bool valid: Qt.isQtObject(object) // object can be evaluated to null at some point during creation/deletion - property string intrinsicInitMode: valid ? _reconstruction.getIntrinsicInitMode(object) : "" property bool inViews: valid && _reconstruction.sfmReport && _reconstruction.isInViews(object) // Camera Initialization indicator IntrinsicsIndicator { - intrinsicInitMode: parent.intrinsicInitMode + intrinsic: parent.valid ? _reconstruction.getIntrinsic(object) : null metadata: imageDelegate.metadata } diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml index b65d2fdc5d..bdf84cdc1a 100644 --- a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml +++ b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml @@ -11,7 +11,11 @@ import Utils 1.0 ImageBadge { id: root - property string intrinsicInitMode + // Intrinsic GroupAttribute + property var intrinsic: null + + readonly property string intrinsicInitMode: intrinsic ? childAttributeValue(intrinsic, "initializationMode", "none") : "unknown" + readonly property string distortionModel: intrinsic ? childAttributeValue(intrinsic, "type", "") : "" property var metadata: ({}) // access useful metadata @@ -29,6 +33,11 @@ ImageBadge { property string helperText: "" text: MaterialIcons.camera + + function childAttributeValue(attribute, childName, defaultValue) { + var attr = attribute.value.get(childName); + return attr ? attr.value : defaultValue; + } function metaStr(value) { return value || "undefined" @@ -37,6 +46,7 @@ ImageBadge { ToolTip.text: "Camera Intrinsics: " + statusText + "
" + (detailsText ? detailsText + "
" : "") + (helperText ? helperText + "
" : "") + + (distortionModel ? 'Distortion Model: ' + distortionModel + "
" : "") + "
" + "[Metadata]
" + " - Make: " + metaStr(make) + "
" @@ -47,7 +57,7 @@ ImageBadge { + ((bodySerialNumber || lensSerialNumber) ? "" : "

Warning: SerialNumber metadata is missing.
Images from different devices might incorrectly share the same camera internal settings.") - state: intrinsicInitMode ? intrinsicInitMode : "unknown" + state: intrinsicInitMode states: [ State { diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b71d80f676..b16c623a3c 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -540,28 +540,11 @@ def getIntrinsic(self, viewpoint): Returns: Attribute: the Viewpoint's corresponding intrinsic or None if not found. """ + if not viewpoint: + return None return next((i for i in self._cameraInit.intrinsics.value if i.intrinsicId.value == viewpoint.intrinsicId.value) , None) - @Slot(QObject, result=str) - def getIntrinsicInitMode(self, viewpoint): - """ - Get the initialization mode for the intrinsic associated to 'viewpoint'. - - Args: - viewpoint (Attribute): the Viewpoint to consider. - Returns: - str: the initialization mode of the Viewpoint's intrinsic or an empty string if none. - """ - intrinsic = self.getIntrinsic(viewpoint) - if not intrinsic: - return "" - try: - return intrinsic.initializationMode.value - except AttributeError: - # handle older versions that did not have this attribute - return "" - @Slot(QObject, result=bool) def hasMetadata(self, viewpoint): # Should be greater than 2 to avoid the particular case of "" From 2a0b695ea765c4e5e188a692d5a5337bf5cea320 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 31 Jan 2019 16:27:33 +0100 Subject: [PATCH 156/293] [core] more thorough compatibility issues detection * add 'matchDescription' method on desc.Attribute. * desc.ListAttribute/desc.GroupAttribute: 'matchDescription' ensure that value perfectly match element/group description. Enable recursive checking for child attributes. * nodeFactory: detect DescriptionConflicts in pre-process pass by checking that serialized node's attributes perfectly match their descriptions * CompatibilityNode.attributeDescFromNameAttribute: consider link expressions as valid values * test_compatibility: more complete description conflicts unit testing --- meshroom/core/desc.py | 47 ++++++++- meshroom/core/node.py | 42 ++++---- tests/test_compatibility.py | 192 +++++++++++++++++++++++++++++++----- 3 files changed, 236 insertions(+), 45 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 5a349c9ea7..321a6ab4c3 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -31,8 +31,21 @@ def __init__(self, name, label, description, value, advanced, uid, group): type = Property(str, lambda self: self.__class__.__name__, constant=True) def validateValue(self, value): + """ Return validated/conformed 'value'. + + Raises: + ValueError: if value does not have the proper type + """ return value + def matchDescription(self, value): + """ Returns whether the value perfectly match attribute's description. """ + try: + self.validateValue(value) + except ValueError: + return False + return True + class ListAttribute(Attribute): """ A list of Attributes """ @@ -49,10 +62,19 @@ def __init__(self, elementDesc, name, label, description, group='allParams', joi joinChar = Property(str, lambda self: self._joinChar, constant=True) def validateValue(self, value): - if not isinstance(value, collections.Iterable): - raise ValueError('ListAttribute only supports iterable input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) + if not isinstance(value, (list, tuple)): + raise ValueError('ListAttribute only supports list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) return value + def matchDescription(self, value): + """ Check that 'value' content matches ListAttribute's element description. """ + if not super(ListAttribute, self).matchDescription(value): + return False + # list must be homogeneous: only test first element + if value: + return self._elementDesc.matchDescription(value[0]) + return True + class GroupAttribute(Attribute): """ A macro Attribute composed of several Attributes """ @@ -67,10 +89,31 @@ def __init__(self, groupDesc, name, label, description, group='allParams', joinC groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) def validateValue(self, value): + """ Ensure value is a dictionary with keys compatible with the group description. """ if not isinstance(value, dict): raise ValueError('GroupAttribute only supports dict input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) + invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc]) + if invalidKeys: + raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys)) return value + def matchDescription(self, value): + """ + Check that 'value' contains the exact same set of keys as GroupAttribute's group description + and that every child value match corresponding child attribute description. + """ + if not super(GroupAttribute, self).matchDescription(value): + return False + attrMap = {attr.name: attr for attr in self._groupDesc} + # must have the exact same child attributes + if sorted(value.keys()) != sorted(attrMap.keys()): + return False + for k, v in value.items(): + # each child value must match corresponding child attribute description + if not attrMap[k].matchDescription(v): + return False + return True + def retrieveChildrenUids(self): allUids = [] for desc in self._groupDesc: diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 36e0b31132..6fcf02e8f2 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -775,6 +775,7 @@ def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.U # store attributes that could be used during node upgrade if matchDesc: self._commonInputs.append(attrName) + # create outputs attributes for attrName, value in self.outputs.items(): self._addAttribute(attrName, value, True) @@ -842,7 +843,7 @@ def attributeDescFromName(refAttributes, name, value): Try to find a matching attribute description in refAttributes for given attribute 'name' and 'value'. Args: - refAttributes ([Attribute]): reference Attributes to look for a description + refAttributes ([desc.Attribute]): reference Attributes to look for a description name (str): attribute's name value: attribute's value @@ -851,13 +852,12 @@ def attributeDescFromName(refAttributes, name, value): """ # from original node description based on attribute's name attrDesc = next((d for d in refAttributes if d.name == name), None) - if attrDesc: - # ensure value is valid for this description - try: - attrDesc.validateValue(value) - except ValueError: - attrDesc = None - return attrDesc + # consider this value matches description: + # - if it's a serialized link expression (no proper value to set/evaluate) + # - or if it passes the 'matchDescription' test + if attrDesc and (Attribute.isLinkExpression(value) or attrDesc.matchDescription(value)): + return attrDesc + return None def _addAttribute(self, name, val, isOutput): """ @@ -982,23 +982,31 @@ def nodeFactory(nodeDict, name=None): compatibilityIssue = CompatibilityIssue.VersionConflict # in other cases, check attributes compatibility between serialized node and its description else: - descAttrNames = set([attr.name for attr in nodeDesc.inputs + nodeDesc.outputs]) - attrNames = set([name for name in list(inputs.keys()) + list(outputs.keys())]) - if attrNames != descAttrNames: + # check that the node has the exact same set of inputs/outputs as its description + if sorted([attr.name for attr in nodeDesc.inputs]) != sorted(inputs.keys()) or \ + sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys()): compatibilityIssue = CompatibilityIssue.DescriptionConflict + # verify that all inputs match their descriptions + for attrName, value in inputs.items(): + if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): + compatibilityIssue = CompatibilityIssue.DescriptionConflict + break + # verify that all outputs match their descriptions + for attrName, value in outputs.items(): + if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value): + compatibilityIssue = CompatibilityIssue.DescriptionConflict + break - # no compatibility issues: instantiate a Node if compatibilityIssue is None: - n = Node(nodeType, position, **inputs) - # otherwise, instantiate a CompatibilityNode + node = Node(nodeType, position, **inputs) else: logging.warning("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name)) - n = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) + node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) # retro-compatibility: no internal folder saved # can't spawn meaningful CompatibilityNode with precomputed outputs # => automatically try to perform node upgrade if not internalFolder and nodeDesc: logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name)) - n = n.upgrade() + node = node.upgrade() - return n + return node diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 307331ce57..0e3ede52b8 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -3,6 +3,8 @@ import tempfile import os + +import copy import pytest import meshroom.core @@ -12,6 +14,27 @@ from meshroom.core.node import CompatibilityNode, CompatibilityIssue, Node +SampleGroupV1 = [ + desc.IntParam(name="a", label="a", description="", value=0, uid=[0], range=None), + desc.ListAttribute( + name="b", + elementDesc=desc.FloatParam(name="p", label="", description="", value=0.0, uid=[0], range=None), + label="b", + description="", + ) +] + +SampleGroupV2 = [ + desc.IntParam(name="a", label="a", description="", value=0, uid=[0], range=None), + desc.ListAttribute( + name="b", + elementDesc=desc.GroupAttribute(name="p", label="", description="", groupDesc=SampleGroupV1), + label="b", + description="", + ) +] + + class SampleNodeV1(desc.Node): """ Version 1 Sample Node """ inputs = [ @@ -29,7 +52,52 @@ class SampleNodeV2(desc.Node): """ inputs = [ desc.File(name='in', label='Input', description='', value='', uid=[0],), - desc.StringParam(name='paramA', label='ParamA', description='', value='', uid=[]) # No impact on UID + desc.StringParam(name='paramA', label='ParamA', description='', value='', uid=[]), # No impact on UID + ] + outputs = [ + desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[]) + ] + +class SampleNodeV3(desc.Node): + """ + Changes from V3: + * 'paramA' has been removed' + """ + inputs = [ + desc.File(name='in', label='Input', description='', value='', uid=[0], ), + ] + outputs = [ + desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[]) + ] + +class SampleNodeV4(desc.Node): + """ + Changes from V3: + * 'paramA' has been added + """ + inputs = [ + desc.File(name='in', label='Input', description='', value='', uid=[0], ), + desc.ListAttribute(name='paramA', label='ParamA', + elementDesc=desc.GroupAttribute( + groupDesc=SampleGroupV1, name='gA', label='gA', description=''), + description='') + ] + outputs = [ + desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[]) + ] + + +class SampleNodeV5(desc.Node): + """ + Changes from V4: + * 'paramA' elementDesc has changed from SampleGroupV1 to SampleGroupV2 + """ + inputs = [ + desc.File(name='in', label='Input', description='', value='', uid=[0]), + desc.ListAttribute(name='paramA', label='ParamA', + elementDesc=desc.GroupAttribute( + groupDesc=SampleGroupV2, name='gA', label='gA', description=''), + description='') ] outputs = [ desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[]) @@ -76,43 +144,115 @@ def test_description_conflict(): """ Test compatibility behavior for conflicting node descriptions. """ - registerNodeType(SampleNodeV1) + # copy registered node types to be able to restore them + originalNodeTypes = copy.copy(meshroom.core.nodesDesc) + nodeTypes = [SampleNodeV1, SampleNodeV2, SampleNodeV3, SampleNodeV4, SampleNodeV5] + nodes = [] g = Graph('') - n = g.addNewNode("SampleNodeV1") + + # register and instantiate instances of all node types except last one + for nt in nodeTypes[:-1]: + registerNodeType(nt) + n = g.addNewNode(nt.__name__) + + if nt == SampleNodeV4: + # initialize list attribute with values to create a conflict with V5 + n.paramA.value = [{'a': 0, 'b': [1.0, 2.0]}] + + nodes.append(n) + graphFile = os.path.join(tempfile.mkdtemp(), "test_description_conflict.mg") g.save(graphFile) - internalFolder = n.internalFolder - nodeName = n.name - # replace SampleNodeV1 by SampleNodeV2 - # 'SampleNodeV1' is still registered but implementation has changed - meshroom.core.nodesDesc[SampleNodeV1.__name__] = SampleNodeV2 + # reload file as-is, ensure no compatibility issue is detected (no CompatibilityNode instances) + g = loadGraph(graphFile) + assert all(isinstance(n, Node) for n in g.nodes) + + # offset node types register to create description conflicts + # each node type name now reference the next one's implementation + for i, nt in enumerate(nodeTypes[:-1]): + meshroom.core.nodesDesc[nt.__name__] = nodeTypes[i+1] # reload file g = loadGraph(graphFile) os.remove(graphFile) - assert len(g.nodes) == 1 - n = g.node(nodeName) - # Node description clashes between what has been saved - assert isinstance(n, CompatibilityNode) - assert n.issue == CompatibilityIssue.DescriptionConflict - assert len(n.attributes) == 3 - assert hasattr(n, "input") - assert not hasattr(n, "in") - assert n.internalFolder == internalFolder + assert len(g.nodes) == len(nodes) + for srcNode in nodes: + nodeName = srcNode.name + compatNode = g.node(srcNode.name) + # Node description clashes between what has been saved + assert isinstance(compatNode, CompatibilityNode) + assert srcNode.internalFolder == compatNode.internalFolder - # perform upgrade - g.upgradeNode(nodeName) - n = g.node(nodeName) + # case by case description conflict verification + if isinstance(srcNode.nodeDesc, SampleNodeV1): + # V1 => V2: 'input' has been renamed to 'in' + assert len(compatNode.attributes) == 3 + assert hasattr(compatNode, "input") + assert not hasattr(compatNode, "in") - assert isinstance(n, Node) - assert not hasattr(n, "input") - assert hasattr(n, "in") - # check uid has changed (not the same set of attributes) - assert n.internalFolder != internalFolder - unregisterNodeType(SampleNodeV1) + # perform upgrade + upgradedNode = g.upgradeNode(nodeName)[0] + assert isinstance(upgradedNode, Node) and isinstance(upgradedNode.nodeDesc, SampleNodeV2) + + assert not hasattr(upgradedNode, "input") + assert hasattr(upgradedNode, "in") + # check uid has changed (not the same set of attributes) + assert upgradedNode.internalFolder != srcNode.internalFolder + + elif isinstance(srcNode.nodeDesc, SampleNodeV2): + # V2 => V3: 'paramA' has been removed' + assert len(compatNode.attributes) == 3 + assert hasattr(compatNode, "paramA") + + # perform upgrade + upgradedNode = g.upgradeNode(nodeName)[0] + assert isinstance(upgradedNode, Node) and isinstance(upgradedNode.nodeDesc, SampleNodeV3) + + assert not hasattr(upgradedNode, "paramA") + # check uid is identical (paramA not part of uid) + assert upgradedNode.internalFolder == srcNode.internalFolder + + elif isinstance(srcNode.nodeDesc, SampleNodeV3): + # V3 => V4: 'paramA' has been added + assert len(compatNode.attributes) == 2 + assert not hasattr(compatNode, "paramA") + + # perform upgrade + upgradedNode = g.upgradeNode(nodeName)[0] + assert isinstance(upgradedNode, Node) and isinstance(upgradedNode.nodeDesc, SampleNodeV4) + + assert hasattr(upgradedNode, "paramA") + assert isinstance(upgradedNode.paramA.attributeDesc, desc.ListAttribute) + # paramA child attributes invalidate UID + assert upgradedNode.internalFolder != srcNode.internalFolder + + elif isinstance(srcNode.nodeDesc, SampleNodeV4): + # V4 => V5: 'paramA' elementDesc has changed from SampleGroupV1 to SampleGroupV2 + assert len(compatNode.attributes) == 3 + assert hasattr(compatNode, "paramA") + groupAttribute = compatNode.paramA.attributeDesc.elementDesc + + assert isinstance(groupAttribute, desc.GroupAttribute) + # check that Compatibility node respect SampleGroupV1 description + for elt in groupAttribute.groupDesc: + assert isinstance(elt, next(a for a in SampleGroupV1 if a.name == elt.name).__class__) + + # perform upgrade + upgradedNode = g.upgradeNode(nodeName)[0] + assert isinstance(upgradedNode, Node) and isinstance(upgradedNode.nodeDesc, SampleNodeV5) + + assert hasattr(upgradedNode, "paramA") + # parameter was incompatible, value could not be restored + assert upgradedNode.paramA.isDefault + assert upgradedNode.internalFolder != srcNode.internalFolder + else: + raise ValueError("Unexpected node type: " + srcNode.nodeType) + + # restore original node types + meshroom.core.nodesDesc = originalNodeTypes def test_upgradeAllNodes(): From 4f2c4d80b92f39a088d7b9285d1b88de64c7117d Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 1 Feb 2019 16:04:09 +0100 Subject: [PATCH 157/293] [tests] multiviewPipeline: add de/serialization testing --- tests/test_multiviewPipeline.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_multiviewPipeline.py b/tests/test_multiviewPipeline.py index c85e37293e..c5ef6d86c1 100644 --- a/tests/test_multiviewPipeline.py +++ b/tests/test_multiviewPipeline.py @@ -1,6 +1,11 @@ #!/usr/bin/env python # coding:utf-8 +import os +import tempfile + import meshroom.multiview +from meshroom.core.graph import loadGraph +from meshroom.core.node import Node def test_multiviewPipeline(): @@ -83,3 +88,14 @@ def test_multiviewPipeline(): for uidIndex in attr.desc.uid: assert attr.uid(uidIndex) == otherAttr.uid(uidIndex) + # test serialization/deserialization + for graph in [graph1, graph2, graph3, graph4]: + filename = tempfile.mktemp() + graph.save(filename) + loadedGraph = loadGraph(filename) + os.remove(filename) + # check that all nodes have been properly de-serialized + # - same node set + assert sorted([n.name for n in loadedGraph.nodes]) == sorted([n.name for n in graph.nodes]) + # - no compatibility issues + assert all(isinstance(n, Node) for n in loadedGraph.nodes) From cc3a53aee18f95d8899ef0f6b5396f572a2d063f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 31 Jan 2019 18:48:55 +0100 Subject: [PATCH 158/293] [nodes] `DepthMapFilter` Add option `computeNormalMaps` --- meshroom/nodes/aliceVision/DepthMapFilter.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/DepthMapFilter.py b/meshroom/nodes/aliceVision/DepthMapFilter.py index 73c33fd0d9..a4f2ed5d14 100644 --- a/meshroom/nodes/aliceVision/DepthMapFilter.py +++ b/meshroom/nodes/aliceVision/DepthMapFilter.py @@ -1,4 +1,4 @@ -__version__ = "2.0" +__version__ = "3.0" from meshroom.core import desc @@ -86,6 +86,14 @@ class DepthMapFilter(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='computeNormalMaps', + label='Compute Normal Maps', + description='Compute normal maps per depth map.', + value=False, + uid=[0], + advanced=True, + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 1793a308610b59113d990eeed6fc55d29568a1a9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 6 Feb 2019 14:04:45 +0100 Subject: [PATCH 159/293] [ui][Viewer3D] Locator3D: properly initialize buffers with 'new' Fix crash using PySide2 5.12: "calling a TypedArray constructor without new is invalid" --- meshroom/ui/qml/Viewer3D/Locator3D.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Locator3D.qml b/meshroom/ui/qml/Viewer3D/Locator3D.qml index 20ad95cf30..3ff40133b7 100644 --- a/meshroom/ui/qml/Viewer3D/Locator3D.qml +++ b/meshroom/ui/qml/Viewer3D/Locator3D.qml @@ -20,7 +20,7 @@ Entity { name: defaultPositionAttributeName buffer: Buffer { type: Buffer.VertexBuffer - data: Float32Array([ + data: new Float32Array([ 0.0, 0.001, 0.0, 1.0, 0.001, 0.0, 0.0, 0.001, 0.0, @@ -38,7 +38,7 @@ Entity { name: defaultColorAttributeName buffer: Buffer { type: Buffer.VertexBuffer - data: Float32Array([ + data: new Float32Array([ Colors.red.r, Colors.red.g, Colors.red.b, Colors.red.r, Colors.red.g, Colors.red.b, Colors.green.r, Colors.green.g, Colors.green.b, From 829aa24a4aef95c284af854b9ea81d666becc182 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 7 Feb 2019 18:14:16 +0100 Subject: [PATCH 160/293] [ui] CameraController: avoid allocating new objects on bindings Change existing objects values instead of re-creating point/size objects, on bindings that can be evaluated very often. --- meshroom/ui/qml/Viewer3D/DefaultCameraController.qml | 5 +++-- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml index aad3942b92..72bc882924 100644 --- a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml +++ b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml @@ -44,7 +44,8 @@ Entity { sourceDevice: mouseSourceDevice onPressed: { _pressed = true; - currentPosition = lastPosition = Qt.point(mouse.x, mouse.y); + currentPosition.x = lastPosition.x = mouse.x; + currentPosition.y = lastPosition.y = mouse.y; mousePressed(mouse); } onReleased: { @@ -52,7 +53,7 @@ Entity { mouseReleased(mouse); } onClicked: mouseClicked(mouse) - onPositionChanged: { currentPosition = Qt.point(mouse.x, mouse.y) } + onPositionChanged: { currentPosition.x = mouse.x; currentPosition.y = mouse.y } onDoubleClicked: mouseDoubleClicked(mouse) onWheel: { var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.2; diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index e9cf67f0eb..fc7006c182 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -130,7 +130,10 @@ FocusScope { DefaultCameraController { id: cameraController - windowSize: Qt.size(root.width, root.height) + windowSize { + width: root.width + height: root.height + } rotationSpeed: 10 trackballSize: 0.4 From 9bd53c7c89659b955604e18baf07073991dacfb3 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 7 Feb 2019 18:24:52 +0100 Subject: [PATCH 161/293] [ui] CameraController: check KeyboardHandler status for all actions with modifiers --- meshroom/ui/qml/Viewer3D/DefaultCameraController.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml index 72bc882924..7904f6933a 100644 --- a/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml +++ b/meshroom/ui/qml/Viewer3D/DefaultCameraController.qml @@ -14,7 +14,9 @@ Entity { property real translateSpeed: 75.0 property real tiltSpeed: 500.0 property real panSpeed: 500.0 - property bool moving: pressed || (actionAlt.active && keyboardHandler._pressed) + readonly property bool moving: actionLMB.active + readonly property bool panning: (keyboardHandler._pressed && actionLMB.active && actionShift.active) || actionMMB.active + readonly property bool zooming: keyboardHandler._pressed && actionRMB.active && actionAlt.active property alias focus: keyboardHandler.focus readonly property bool pickingActive: actionControl.active && keyboardHandler._pressed property alias rotationSpeed: trackball.rotationSpeed @@ -159,19 +161,19 @@ Entity { components: [ FrameAction { onTriggered: { - if(actionMMB.active || (actionLMB.active && actionShift.active)) { // translate + if(panning) { // translate var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03; var tx = axisMX.value * root.translateSpeed * d; var ty = axisMY.value * root.translateSpeed * d; root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt)) return; } - if(actionLMB.active){ // trackball rotation + if(moving){ // trackball rotation trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt); mouseHandler.lastPosition = mouseHandler.currentPosition; return; } - if(actionAlt.active && actionRMB.active) { // zoom with alt + RMD + if(zooming) { // zoom with alt + RMD var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.1; var tz = axisMX.value * root.translateSpeed * d; root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter) From a9cb4afe2ac58690f43507b47c10156ad787cf68 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 7 Feb 2019 18:27:29 +0100 Subject: [PATCH 162/293] [ui] DepthMapLoader: update to DepthMapEntity 2.0 * map render modes to custom visualization modes --- meshroom/ui/qml/Viewer3D/DepthMapLoader.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/DepthMapLoader.qml b/meshroom/ui/qml/Viewer3D/DepthMapLoader.qml index 3f7c7e1a99..32f7ae1aed 100644 --- a/meshroom/ui/qml/Viewer3D/DepthMapLoader.qml +++ b/meshroom/ui/qml/Viewer3D/DepthMapLoader.qml @@ -1,4 +1,4 @@ -import DepthMapEntity 1.0 +import DepthMapEntity 2.0 /** * Support for Depth Map files (EXR) in Qt3d. @@ -6,4 +6,9 @@ import DepthMapEntity 1.0 */ DepthMapEntity { id: root + + pointSize: Viewer3DSettings.pointSize * (Viewer3DSettings.fixedPointSize ? 1.0 : 0.001) + // map render modes to custom visualization modes + displayMode: Viewer3DSettings.renderMode == 1 ? DepthMapEntity.Points : DepthMapEntity.Triangles + displayColor: Viewer3DSettings.renderMode == 2 } From 6199de0b4516bd0a1af549bce91f463686d06d96 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 7 Feb 2019 19:11:06 +0100 Subject: [PATCH 163/293] [ui] Inspector3D: improve pointSize/cameraScale sliders * reduce pointSize maximum value for better precision * replace texts by icon * add tooltips with values --- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 984474dfbf..fe6b80b066 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -40,12 +40,15 @@ FloatingPane { columnSpacing: 6 rowSpacing: 3 - Label { text: "Points"; padding: 2 } + MaterialLabel { font.family: MaterialIcons.fontFamily; text: MaterialIcons.grain; padding: 2 } RowLayout { Slider { - Layout.fillWidth: true; from: 0; to: 10; stepSize: 0.1 + Layout.fillWidth: true; from: 0; to: 5; stepSize: 0.1 value: Viewer3DSettings.pointSize onValueChanged: Viewer3DSettings.pointSize = value + ToolTip.text: "Point Size: " + value.toFixed(2) + ToolTip.visible: hovered || pressed + ToolTip.delay: 150 } MaterialToolButton { text: MaterialIcons.center_focus_strong @@ -57,7 +60,7 @@ FloatingPane { } } - Label { text: "Cameras"; padding: 2 } + MaterialLabel { font.family: MaterialIcons.fontFamily; text: MaterialIcons.videocam; padding: 2 } Slider { value: Viewer3DSettings.cameraScale from: 0 @@ -66,6 +69,9 @@ FloatingPane { Layout.fillWidth: true padding: 0 onMoved: Viewer3DSettings.cameraScale = value + ToolTip.text: "Camera Scale: " + value.toFixed(2) + ToolTip.visible: hovered || pressed + ToolTip.delay: 150 } Flow { Layout.columnSpan: 2 From da1b842493078fec4f238601fa033d73075ca6f5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 7 Feb 2019 19:55:05 +0100 Subject: [PATCH 164/293] [ui] MediaLibrary: enable dynamicRoles on mediaModel If an outside 3D media file is the first thing ever loaded, 'model.attribute' will be initialized to 'null' and will prevent an actual Attribute to be associated to this role. This breaks media loading from within Meshroom. --- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index 57951a743c..dd3ec84e1b 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -30,7 +30,7 @@ Entity { QtObject { id: m - property ListModel mediaModel: ListModel {} + property ListModel mediaModel: ListModel { dynamicRoles: true } property var sourceToEntity: ({}) readonly property var mediaElement: ({ From 45faa860c9e09507ece061bedb80fb8f08a4a76b Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 8 Feb 2019 15:37:06 +0100 Subject: [PATCH 165/293] [ui] AttributeEditor: remove SortFilterDelegateModel Usage of DelegateModel for model filtering has not proven to be the most stable solution, and might be responsible for random crashes happening during engine's garbage collection. Implement Loader-based alternative: * first delegate is a Loader which creates the AttributeItemDelegate if necessary * compensate spacing using negative height when element is hidden --- .../ui/qml/GraphEditor/AttributeEditor.qml | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml index bd7dcbbef9..62a6b2f2a2 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeEditor.qml @@ -19,29 +19,23 @@ ListView { implicitHeight: contentHeight - clip: true spacing: 2 + clip: true ScrollBar.vertical: ScrollBar { id: scrollBar } - model: SortFilterDelegateModel { + model: attributes - model: attributes - filterRole: GraphEditorSettings.showAdvancedAttributes ? "" : "advanced" - filterValue: false - - function modelData(item, roleName) { - return item.model.object.desc[roleName] - } + delegate: Loader { + active: !object.desc.advanced || GraphEditorSettings.showAdvancedAttributes + visible: active + height: item ? item.implicitHeight : -spacing // compensate for spacing if item is hidden - Component { - id: delegateComponent - AttributeItemDelegate { - width: ListView.view.width - scrollBar.width - readOnly: root.readOnly - labelWidth: root.labelWidth - attribute: object - onDoubleClicked: root.attributeDoubleClicked(mouse, attr) - } + sourceComponent: AttributeItemDelegate { + width: root.width - scrollBar.width + readOnly: root.readOnly + labelWidth: root.labelWidth + attribute: object + onDoubleClicked: root.attributeDoubleClicked(mouse, attr) } } From c5fd979c54ba3b0e1dff7ca682ea5ec656e08a90 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 22 Feb 2019 15:53:51 +0100 Subject: [PATCH 166/293] [copying] add the obvious AliceVision dependency --- COPYING.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/COPYING.md b/COPYING.md index 9e57912430..41257d2498 100644 --- a/COPYING.md +++ b/COPYING.md @@ -4,21 +4,27 @@ Meshroom is licensed under the [MPL2 license](LICENSE-MPL2.md). ## Third parties licenses + * __AliceVision__ + [https://github.com/alicevision/AliceVision](https://github.com/alicevision/AliceVision) + Copyright (c) 2018 AliceVision contributors. + Distributed under the [MPL2 license](https://opensource.org/licenses/MPL-2.0). + See [COPYING](https://github.com/alicevision/AliceVision/blob/develop/COPYING.md) for full third parties licenses. + * __Python__ [https://www.python.org](https://www.python.org) Copyright (c) 2001-2018 Python Software Foundation - Distributed under the [PSFL V2](https://www.python.org/download/releases/2.7/license/). - + Distributed under the [PSFL V2 license](https://www.python.org/download/releases/2.7/license/). + * __Qt/PySide2__ [https://www.qt.io](https://www.qt.io) Copyright (C) 2018 The Qt Company Ltd and other contributors. - Distributed under the [LGPL V3](https://opensource.org/licenses/LGPL-3.0). - + Distributed under the [LGPL V3 license](https://opensource.org/licenses/LGPL-3.0). + * __qmlAlembic__ [https://github.com/alicevision/qmlAlembic](https://github.com/alicevision/qmlAlembic) Copyright (c) 2018 AliceVision contributors. Distributed under the [MPL2 license](https://opensource.org/licenses/MPL-2.0). - + * __QtOIIO__ [https://github.com/alicevision/QtOIIO](https://github.com/alicevision/QtOIIO) Copyright (c) 2018 AliceVision contributors. From b147788e5c5e80e8a1a3548f8d48b8fc3414ee51 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 27 Feb 2019 15:00:16 +0100 Subject: [PATCH 167/293] [bin] use sys.exit + build meshroom_compute executable * cx_Freeze removes builtin 'exit' function, use sys.exit in executables instead * build: generate an executable for "meshroom_compute" --- bin/meshroom_compute | 5 +++-- bin/meshroom_newNodeType | 6 +++--- bin/meshroom_photogrammetry | 5 +++-- bin/meshroom_statistics | 3 ++- bin/meshroom_status | 5 +++-- setup.py | 7 ++++++- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/bin/meshroom_compute b/bin/meshroom_compute index f9fe21a010..13231f0357 100755 --- a/bin/meshroom_compute +++ b/bin/meshroom_compute @@ -1,5 +1,6 @@ #!/usr/bin/env python import argparse +import sys import meshroom meshroom.setupEnvironment() @@ -47,7 +48,7 @@ if args.node: for chunk in chunks: if chunk.status.status in submittedStatuses: print('Error: Node is already submitted with status "{}". See file: "{}"'.format(chunk.status.status.name, chunk.statusFile)) - # exit(-1) + # sys.exit(-1) if args.iteration != -1: chunk = node.chunks[args.iteration] chunk.process(args.forceCompute) @@ -56,7 +57,7 @@ if args.node: else: if args.iteration != -1: print('Error: "--iteration" only make sense when used with "--node".') - exit(-1) + sys.exit(-1) toNodes = None if args.toNode: toNodes = graph.findNodes([args.toNode]) diff --git a/bin/meshroom_newNodeType b/bin/meshroom_newNodeType index a79248e288..2ee23f27bd 100755 --- a/bin/meshroom_newNodeType +++ b/bin/meshroom_newNodeType @@ -86,7 +86,7 @@ elif sys.stdin.isatty(): if not inputCmdLineDoc: print('No input documentation.') print('Usage: YOUR_COMMAND --help | {cmd}'.format(cmd=os.path.splitext(__file__)[0])) - exit(-1) + sys.exit(-1) fileStr = '''import sys @@ -131,7 +131,7 @@ elif args.parser == 'basic': args_re = re.compile('()--(?P\w+)()()()()') else: print('Error: Unknown input parser "{}"'.format(args.parser)) - exit(-1) + sys.exit(-1) choiceValues1_re = re.compile('\* (?P\w+):') choiceValues2_re = re.compile('\((?P.+?)\)') @@ -299,7 +299,7 @@ outputFilepath = os.path.join(args.output, args.node + '.py') if not args.force and os.path.exists(outputFilepath): print('Plugin "{}" already exists "{}".'.format(args.node, outputFilepath)) - exit(-1) + sys.exit(-1) with open(outputFilepath, 'w') as pluginFile: pluginFile.write(fileStr) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 86003d4db0..2910ec7c43 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -1,6 +1,7 @@ #!/usr/bin/env python import argparse import os +import sys import meshroom meshroom.setupEnvironment() @@ -63,7 +64,7 @@ def getOnlyNodeOfType(g, nodeType): if not args.input and not args.inputImages: print('Nothing to compute. You need to set --input or --inputImages.') - exit(1) + sys.exit(1) views, intrinsics = [], [] # Build image files list from inputImages arguments @@ -109,7 +110,7 @@ if args.scale > 0: if args.save: graph.save(args.save) print('File successfully saved:', args.save) - exit(0) + sys.exit(0) # setup cache directory graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder diff --git a/bin/meshroom_statistics b/bin/meshroom_statistics index 4fc473bf21..57636ed303 100755 --- a/bin/meshroom_statistics +++ b/bin/meshroom_statistics @@ -1,6 +1,7 @@ #!/usr/bin/env python import argparse import os +import sys from pprint import pprint from collections import Iterable, defaultdict @@ -44,7 +45,7 @@ args = parser.parse_args() if not os.path.exists(args.graphFile): print('ERROR: No graph file "{}".'.format(args.graphFile)) - exit(-1) + sys.exit(-1) graph = pg.loadGraph(args.graphFile) diff --git a/bin/meshroom_status b/bin/meshroom_status index 6d5efcb407..95abdcdd8c 100755 --- a/bin/meshroom_status +++ b/bin/meshroom_status @@ -1,6 +1,7 @@ #!/usr/bin/env python import argparse import os +import sys from pprint import pprint import meshroom @@ -22,7 +23,7 @@ args = parser.parse_args() if not os.path.exists(args.graphFile): print('ERROR: No graph file "{}".'.format(args.node, args.graphFile)) - exit(-1) + sys.exit(-1) graph = meshroom.core.graph.loadGraph(args.graphFile) @@ -32,7 +33,7 @@ if args.node: node = graph.node(args.node) if node is None: print('ERROR: node "{}" does not exist in file "{}".'.format(args.node, args.graphFile)) - exit(-1) + sys.exit(-1) for chunk in node.chunks: print('{}: {}'.format(chunk.name, chunk.status.status.name)) if args.verbose: diff --git a/setup.py b/setup.py index 26e9191a6c..3a132728a3 100644 --- a/setup.py +++ b/setup.py @@ -113,6 +113,11 @@ def __init__(self, script, initScript=None, base=None, targetName=None, icons=No "bin/meshroom_photogrammetry" ) +meshroomCompute = PlatformExecutable( + "bin/meshroom_compute" +) + + setup( name="Meshroom", description="Meshroom", @@ -127,5 +132,5 @@ def __init__(self, script, initScript=None, base=None, targetName=None, icons=No ], version=meshroom.__version__, options={"build_exe": build_exe_options}, - executables=[meshroomExe, meshroomPhotog], + executables=[meshroomExe, meshroomPhotog, meshroomCompute], ) From caff4c06ba7fc99802d735339d975d8adbd62bd3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 27 Feb 2019 15:54:04 +0100 Subject: [PATCH 168/293] [docker] Update Qt from 5.11.0 to 5.11.1 --- Dockerfile | 2 +- docker/qt-installer-noninteractive.qs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 093b51580a..f7e2dacea3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" ENV MESHROOM_DEV=/opt/Meshroom \ MESHROOM_BUILD=/tmp/Meshroom_build \ MESHROOM_BUNDLE=/opt/Meshroom_bundle \ - QT_DIR=/opt/qt/5.11.0/gcc_64 \ + QT_DIR=/opt/qt/5.11.1/gcc_64 \ PATH="${PATH}:${MESHROOM_BUNDLE}" COPY . "${MESHROOM_DEV}" diff --git a/docker/qt-installer-noninteractive.qs b/docker/qt-installer-noninteractive.qs index 169631d4df..18224cd17a 100644 --- a/docker/qt-installer-noninteractive.qs +++ b/docker/qt-installer-noninteractive.qs @@ -46,14 +46,14 @@ Controller.prototype.ComponentSelectionPageCallback = function() { widget.deselectAll(); // widget.selectComponent("qt"); - // widget.selectComponent("qt.qt5.5110"); - widget.selectComponent("qt.qt5.5110.gcc_64"); - // widget.selectComponent("qt.qt5.5110.qtscript"); - // widget.selectComponent("qt.qt5.5110.qtscript.gcc_64"); - // widget.selectComponent("qt.qt5.5110.qtwebengine"); - // widget.selectComponent("qt.qt5.5110.qtwebengine.gcc_64"); - // widget.selectComponent("qt.qt5.5110.qtwebglplugin"); - // widget.selectComponent("qt.qt5.5110.qtwebglplugin.gcc_64"); + // widget.selectComponent("qt.qt5.5111"); + widget.selectComponent("qt.qt5.5111.gcc_64"); + // widget.selectComponent("qt.qt5.5111.qtscript"); + // widget.selectComponent("qt.qt5.5111.qtscript.gcc_64"); + // widget.selectComponent("qt.qt5.5111.qtwebengine"); + // widget.selectComponent("qt.qt5.5111.qtwebengine.gcc_64"); + // widget.selectComponent("qt.qt5.5111.qtwebglplugin"); + // widget.selectComponent("qt.qt5.5111.qtwebglplugin.gcc_64"); // widget.selectComponent("qt.tools"); gui.clickButton(buttons.NextButton); From 73c35a607c7fcd7399c5c9a1cdeac6e7f4e56338 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 27 Feb 2019 19:17:42 +0100 Subject: [PATCH 169/293] [docker] remove unnecessary files to reduce bundle size --- Dockerfile | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index f7e2dacea3..93fb664b4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,13 +40,22 @@ RUN yum install -y centos-release-scl RUN yum install -y rh-python36 # Install Meshroom requirements and freeze bundle -RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" +RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ + find ${MESHROOM_BUNDLE} -name "*Qt5Web*" -delete && \ + find ${MESHROOM_BUNDLE} -name "*Qt5Designer*" -delete && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/libclang.so* && \ + rm -rf ${MESHROOM_BUNDLE}/lib/PySide2/typesystems/ ${MESHROOM_BUNDLE}/lib/PySide2/examples/ ${MESHROOM_BUNDLE}/lib/PySide2/include/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/translations/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/resources/ && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/libQt5* && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/QtWeb* && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/libicu* && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-lupdate ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-rcc ${MESHROOM_BUNDLE}/lib/PySide2/shiboken2 # Install Qt (to build plugins) WORKDIR /tmp/qt -RUN curl -LO http://download.qt.io/official_releases/online_installers/qt-unified-linux-x64-online.run -RUN chmod u+x qt-unified-linux-x64-online.run -RUN ./qt-unified-linux-x64-online.run --verbose --platform minimal --script "${MESHROOM_DEV}/docker/qt-installer-noninteractive.qs" +RUN curl -LO http://download.qt.io/official_releases/online_installers/qt-unified-linux-x64-online.run && \ + chmod u+x qt-unified-linux-x64-online.run && \ + ./qt-unified-linux-x64-online.run --verbose --platform minimal --script "${MESHROOM_DEV}/docker/qt-installer-noninteractive.qs" && \ + rm ./qt-unified-linux-x64-online.run WORKDIR ${MESHROOM_BUILD} # Temporary workaround for qmlAlembic build @@ -54,7 +63,9 @@ RUN rm -rf "${AV_INSTALL}/lib" && ln -s "${AV_INSTALL}/lib64" "${AV_INSTALL}/lib # Build Meshroom plugins RUN cmake "${MESHROOM_DEV}" -DALICEVISION_ROOT="${AV_INSTALL}" -DQT_DIR="${QT_DIR}" -DCMAKE_INSTALL_PREFIX="${MESHROOM_BUNDLE}/qtPlugins" -RUN make -j8 +RUN make -j8 && cd /tmp && rm -rf ${MESHROOM_BUILD} RUN mv "${AV_BUNDLE}" "${MESHROOM_BUNDLE}/aliceVision" +RUN rm -rf ${MESHROOM_BUNDLE}/aliceVision/share/doc ${MESHROOM_BUNDLE}/aliceVision/share/eigen3 ${MESHROOM_BUNDLE}/aliceVision/share/fonts ${MESHROOM_BUNDLE}/aliceVision/share/lemon ${MESHROOM_BUNDLE}/aliceVision/share/libraw ${MESHROOM_BUNDLE}/aliceVision/share/man/ aliceVision/share/pkgconfig + From c2382c196011c0dd9528da8ad8e87909de1f4c85 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 5 Feb 2019 20:29:53 +0100 Subject: [PATCH 170/293] Release 2019.1.0 --- CHANGES.md | 58 ++++++++++++++++++++++++++++++++++++++++--- meshroom/__init__.py | 2 +- meshroom/multiview.py | 2 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2e7cb3a958..280838f9c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,59 @@ For algorithmic changes related to the photogrammetric pipeline, please refer to [AliceVision changelog](https://github.com/alicevision/AliceVision/blob/develop/CHANGES.md). -## Release 2018.1.0 - +## Release 2019.1.0 (2019.02.27) + +Based on [AliceVision 2.1.0](https://github.com/alicevision/AliceVision/tree/v2.1.0). + +Release Notes Summary: + - 3D Viewer: Load and compare multiple assets with cache mechanism and improved navigation + - Display camera intrinsic information extracted from metadata analysis + - Easier access to a more complete sensor database with a more reliable camera model matching algorithm. + - Attribute Editor: Hide advanced/experimental parameters by default to improve readability and simplify access to the most useful, high-level settings. Advanced users can still enable them to have full access to internal thresholds. + - Graph Editor: Improved set of contextual tools with `duplicate`/`remove`/`delete data` actions with `From Here` option. + - Nodes: Homogenization of inputs / outputs parameters + - Meshing: Better, faster and configurable estimation of the space to reconstruct based on the sparse point cloud (new option `estimateSpaceFromSfM`). Favors high-density areas and helps removing badly defined ones. + - Draft Meshing (no CUDA required): the result of the sparse reconstruction can now be directly meshed to get a 3D model preview without computing the depth maps. + - MeshFiltering: Now keeps all reconstructed parts by default. + - StructureFromMotion: Add support for rig of cameras + - Support for reconstruction with projected light patterns and texturing with another set of images + +Full Release Notes: + - Viewer3D: New Trackball camera manipulator for improved navigation in the scene + - Viewer3D: New library system to load multiple 3D objects of the same type simultaneously, simplifying results comparisons + - Viewer3D: Add media loading overlay with BusyIndicator + - Viewer3D: Points and cameras size are now configurable via dedicated sliders. + - CameraInit: Add option to lock specific cameras intrinsics (if you have high-quality internal calibration information) + - StructureFromMotion: Triangulate points if the input scene contains valid camera poses and intrinsics without landmarks + - PrepareDenseScene: New `imagesFolders` option to override input images. This enables to use images with light patterns projected for SfM and MVS parts and do the Texturing with another set of images. + - NodeLog: Cross-platform monospace display + - Remove `CameraConnection` and `ExportUndistortedImages` nodes + - Multi-machine parallelization of `PrepareDenseScene` + - Meshing: Add option `estimateSpaceFromSfM` and observation angles check to better estimate the bounding box of the reconstruction and avoid useless reconstruction of the environment + - Console: Filter non silenced, inoffensive warnings from QML + log Qt messages via Python logging + - Command line (meshroom_photogrammetry): Add --pipeline parameter to use a pre-configured pipeline graph + - Command line (meshroom_photogrammetry): Add possibility to provide pre-calibrated intrinsics. + - Command line (meshroom_compute): Provide `meshroom_compute` executable in packaged release. + - Image Gallery: Display Camera Intrinsics initialization status with detailed explanation, edit Sensor Database dialog, advanced menu to display view UIDs + - StructureFromMotion: Expose advanced estimator parameters + - FeatureMatching: Expose advanced estimator parameters + - DepthMap: New option `exportIntermediateResults` disabled by default, so less data storage by default than before. + - DepthMap: Use multiple GPUs by default if available and add `nbGPUs` param to limit it + - Meshing: Add option `addLandmarksToTheDensePointCloud` + - SfMTransform: New option to align on one specific camera + - Graph Editor: Consistent read-only mode when computing, that can be unlocked in advanced settings + - Graph Editor: Improved Node Menu: "duplicate"/"remove"/"delete data" with "From Here" accessible on the same entry via an additional button + - Graph Editor: Confirmation popup before deleting node data + - Graph Editor: Add "Clear Pending Status" action at Graph level + - Graph Editor: Solo media in 3D viewer with Ctrl + double click on node/attribute + - Param Editor: Fix several bugs related to attributes edition + - Scene Compatibility: Improves detection of deeper compatibility issues, by adding an additional recursive (taking List/GroupAttributes children into account) exact description matching test when de-serializing a Node. + +See [AliceVision 2.1.0 Release Notes](https://github.com/alicevision/AliceVision/blob/v2.1.0/CHANGES.md) +for more details about algorithmic changes. + + +## Release 2018.1.0 (2018.08.09) + First release of Meshroom. - Based on [AliceVision 2.0.0](https://github.com/alicevision/AliceVision/tree/v2.0.0). \ No newline at end of file + Based on [AliceVision 2.0.0](https://github.com/alicevision/AliceVision/tree/v2.0.0). diff --git a/meshroom/__init__.py b/meshroom/__init__.py index 0c00415dbe..8ee42b3cf4 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2018.1.0" +__version__ = "2019.1.0" import os # Allow override from env variable diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 307a4f1af1..8bb7646f38 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -1,5 +1,5 @@ # Multiview pipeline version -__version__ = "1.0" +__version__ = "2.1" import os From 7343cabbfeb9f49d0ef8157d329e2c5c9ec3fa16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20De=20Lillo?= Date: Thu, 31 Jan 2019 10:44:51 +0100 Subject: [PATCH 171/293] [nodes] `Texturing` Add option `useUDIM` --- meshroom/nodes/aliceVision/Texturing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 92f18d837b..7921af2ead 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -68,6 +68,13 @@ class Texturing(desc.CommandLineNode): exclusive=True, uid=[0], ), + desc.BoolParam( + name='useUDIM', + label='Use UDIM', + description='Use UDIM UV mapping.', + value=True, + uid=[0], + ), desc.BoolParam( name='fillHoles', label='Fill Holes', From 1a22fc8ae80b135d38cacbd6e8fc003aa82f6247 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 1 Mar 2019 17:22:57 +0100 Subject: [PATCH 172/293] [core] add GroupAttribute.childAttribute method * easier access to child attributes * callable from QML --- meshroom/core/attribute.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 3377a74c38..5c341886b2 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -4,7 +4,7 @@ import re import weakref -from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel +from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot from meshroom.core import desc, pyCompatibility, hashValue @@ -371,6 +371,22 @@ def _set_value(self, exportedValue): for key, value in exportedValue.items(): self._value.get(key).value = value + @Slot(str, result=Attribute) + def childAttribute(self, key): + """ + Get child attribute by name or None if none was found. + + Args: + key (str): the name of the child attribute + + Returns: + Attribute: the child attribute or None + """ + try: + return self._value.get(key) + except KeyError: + return None + def uid(self, uidIndex): uids = [] for k, v in self._value.items(): From aae6098b5ac4c74e10f045f87cb363eca2470fc9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 1 Mar 2019 17:29:50 +0100 Subject: [PATCH 173/293] [ui] Viewpoints: use 'childAttribute' method to access child attributes IntrinsicsIndicator: fix exceptions when trying to access a viewpoint's 'initializationMode' attribute that did not exist in previous versions --- meshroom/ui/qml/ImageGallery/ImageGallery.qml | 6 +++--- meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index c10156ef8c..586addea0d 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -108,7 +108,7 @@ Panel { // override modelData to return basename of viewpoint's path for sorting function modelData(item, roleName) { - var value = item.model.object.value.get(roleName).value + var value = item.model.object.childAttribute(roleName).value if(roleName == sortRole) return Filepath.basename(value) else @@ -165,10 +165,10 @@ Panel { // Rig indicator Loader { id: rigIndicator - property int rigId: parent.valid ? object.value.get("rigId").value : -1 + property int rigId: parent.valid ? object.childAttribute("rigId").value : -1 active: rigId >= 0 sourceComponent: ImageBadge { - property int rigSubPoseId: model.object.value.get("subPoseId").value + property int rigSubPoseId: model.object.childAttribute("subPoseId").value text: MaterialIcons.link ToolTip.text: "Rig: Initialized
" + "Rig ID: " + rigIndicator.rigId + "
" + diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml index bdf84cdc1a..faee10615e 100644 --- a/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml +++ b/meshroom/ui/qml/ImageGallery/IntrinsicsIndicator.qml @@ -35,7 +35,7 @@ ImageBadge { text: MaterialIcons.camera function childAttributeValue(attribute, childName, defaultValue) { - var attr = attribute.value.get(childName); + var attr = attribute.childAttribute(childName); return attr ? attr.value : defaultValue; } From c0507bf8e688dbcd88b069a8f2b180934e41f993 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 7 Mar 2019 14:41:14 +0100 Subject: [PATCH 174/293] [bin] added parameter override for meshroom_photogrammetry The user can specify a json file to override the default parameter of each node. --- bin/meshroom_photogrammetry | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 2910ec7c43..ffbd10ee56 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -20,10 +20,13 @@ parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, help='Meshroom file containing a pre-configured photogrammetry pipeline to run on input images. ' - 'If not set, the default photogrammetry pipeline will be used. ' + 'If not set, the default photogrammetry pipeline will be used. ' 'Requirements: the graph must contain one CameraInit node, ' 'and one Publish node if --output is set.') +parser.add_argument('--overrides', metavar='SETTINGS', type=str, default=None, + help='A JSON file containing the graph parameters override.') + parser.add_argument('--output', metavar='FOLDER', type=str, required=False, help='Output folder where results should be copied to. ' 'If not set, results will have to be retrieved directly from the cache folder.') @@ -102,6 +105,15 @@ views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) cameraInit.viewpoints.value = views cameraInit.intrinsics.value = intrinsics +if args.overrides: + import io + import json + with io.open(args.overrides, 'r', encoding='utf-8', errors='ignore') as f: + data = json.load(f) + for nodeName, overrides in data.items(): + for attrName, value in overrides.items(): + graph.findNode(nodeName).attribute(attrName).value = value + # setup DepthMap downscaling if args.scale > 0: for node in graph.nodesByType('DepthMap'): From 0ab7d4560734530f3fba3c29807dcb11cf4cd1b2 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 26 Mar 2019 12:09:16 +0100 Subject: [PATCH 175/293] [desc] allow List/GroupAttribute to be marked as advanced parameters --- meshroom/core/desc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 321a6ab4c3..34674d7753 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -49,13 +49,13 @@ def matchDescription(self, value): class ListAttribute(Attribute): """ A list of Attributes """ - def __init__(self, elementDesc, name, label, description, group='allParams', joinChar=' '): + def __init__(self, elementDesc, name, label, description, group='allParams', advanced=False, joinChar=' '): """ :param elementDesc: the Attribute description of elements to store in that list """ self._elementDesc = elementDesc self._joinChar = joinChar - super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group, advanced=False) + super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group, advanced=advanced) elementDesc = Property(Attribute, lambda self: self._elementDesc, constant=True) uid = Property(Variant, lambda self: self.elementDesc.uid, constant=True) @@ -78,13 +78,13 @@ def matchDescription(self, value): class GroupAttribute(Attribute): """ A macro Attribute composed of several Attributes """ - def __init__(self, groupDesc, name, label, description, group='allParams', joinChar=' '): + def __init__(self, groupDesc, name, label, description, group='allParams', advanced=False, joinChar=' '): """ :param groupDesc: the description of the Attributes composing this group """ self._groupDesc = groupDesc self._joinChar = joinChar - super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group, advanced=False) + super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group, advanced=advanced) groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) From a274546247f6329f2d8c8cc9c9fcc722d93f06c5 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 7 Mar 2019 14:42:05 +0100 Subject: [PATCH 176/293] [ui] add option --pipeline at CLI to directly load a project in GUI --- meshroom/ui/app.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 9e5131dc27..e93ca3aac9 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -1,7 +1,8 @@ import logging import os +import argparse -from PySide2.QtCore import Qt, Slot, QJsonValue, Property, qInstallMessageHandler, QtMsgType +from PySide2.QtCore import Qt, QUrl, Slot, QJsonValue, Property, qInstallMessageHandler, QtMsgType from PySide2.QtGui import QIcon from PySide2.QtWidgets import QApplication @@ -53,8 +54,8 @@ def handler(cls, messageType, context, message): class MeshroomApp(QApplication): """ Meshroom UI Application. """ def __init__(self, args): - args = [args[0], '-style', 'fusion'] + args[1:] # force Fusion style by default - super(MeshroomApp, self).__init__(args) + QtArgs = [args[0], '-style', 'fusion'] + args[1:] # force Fusion style by default + super(MeshroomApp, self).__init__(QtArgs) self.setOrganizationName('AliceVision') self.setApplicationName('Meshroom') @@ -95,6 +96,13 @@ def __init__(self, args): # Request any potential computation to stop on exit self.aboutToQuit.connect(r.stopExecution) + parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.') + parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, + help='Meshroom project file (e.g. myProject.mg).') + args = parser.parse_args(args[1:]) + if args.pipeline: + r.loadUrl(QUrl.fromLocalFile(args.pipeline)) + self.engine.load(os.path.normpath(url)) @Slot(str, result=str) From 4febfb3ffefcaa437badf5ac62527472aa455e0c Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 21 Mar 2019 13:41:33 +0100 Subject: [PATCH 177/293] [nodes] PrepareDenseScene enables to export list of undistorted images --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index 75f92a4a7e..d70232b8c7 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -73,5 +73,14 @@ class PrepareDenseScene(desc.CommandLineNode): description='''Output folder.''', value=desc.Node.internalFolder, uid=[], - ) + ), + desc.File( + name='outputUndistorted', + label='Undistorted images', + description='List of undistorted images.', + value=desc.Node.internalFolder + '*.{outputFileTypeValue}', + uid=[], + group='', + advanced=True + ), ] From 7f1943c0de6d1ffac333fd531f80f89078dd1019 Mon Sep 17 00:00:00 2001 From: Tuk Bredsdorff Date: Fri, 29 Mar 2019 17:17:10 +0100 Subject: [PATCH 178/293] Corrected project argument name Argument was `project` but was used as `pipeline` which prevented the ui from starting. --- meshroom/ui/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index e93ca3aac9..0c9032dbc5 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -100,8 +100,8 @@ def __init__(self, args): parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, help='Meshroom project file (e.g. myProject.mg).') args = parser.parse_args(args[1:]) - if args.pipeline: - r.loadUrl(QUrl.fromLocalFile(args.pipeline)) + if args.project: + r.loadUrl(QUrl.fromLocalFile(args.project)) self.engine.load(os.path.normpath(url)) From 1c98e2afdec13fd782871b09ce9f0419aa027172 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 4 Mar 2019 21:20:26 +0100 Subject: [PATCH 179/293] [cxFreeze] add import of encodings to embed them in packaging --- meshroom/core/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index 02888f67f7..d85f76ff15 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -13,6 +13,13 @@ import sys +try: + import encodings.ascii + import encodings.idna + import encodings.utf_8 +except: + pass + from meshroom.core.submitter import BaseSubmitter from . import desc From 007216e88bbdc46194ef0750adcfb2d9a00d1b2a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 4 Mar 2019 21:21:24 +0100 Subject: [PATCH 180/293] [submitter] simpleFarm: add support for submitting without rez --- meshroom/submitters/simpleFarmSubmitter.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/meshroom/submitters/simpleFarmSubmitter.py b/meshroom/submitters/simpleFarmSubmitter.py index de0b0d6151..a2b8041ea9 100644 --- a/meshroom/submitters/simpleFarmSubmitter.py +++ b/meshroom/submitters/simpleFarmSubmitter.py @@ -9,10 +9,13 @@ from meshroom.core.submitter import BaseSubmitter currentDir = os.path.dirname(os.path.realpath(__file__)) - +binDir = os.path.dirname(os.path.dirname(os.path.dirname(currentDir))) class SimpleFarmSubmitter(BaseSubmitter): - MESHROOM_PACKAGE = "meshroom-{}".format(os.environ.get('REZ_MESHROOM_VERSION', '')) + if 'REZ_MESHROOM_VERSION' in os.environ: + MESHROOM_PACKAGE = "meshroom-{}".format(os.environ.get('REZ_MESHROOM_VERSION', '')) + else: + MESHROOM_PACKAGE = None filepath = os.environ.get('SIMPLEFARMCONFIG', os.path.join(currentDir, 'simpleFarmConfig.json')) config = json.load(open(filepath)) @@ -46,10 +49,11 @@ def createTask(self, meshroomFile, node): task = simpleFarm.Task( name=node.nodeType, - command='meshroom_compute --node {nodeName} "{meshroomFile}" {parallelArgs} --extern'.format( + command='{exe} --node {nodeName} "{meshroomFile}" {parallelArgs} --extern'.format( + exe='meshroom_compute' if self.MESHROOM_PACKAGE else os.path.join(binDir, 'meshroom_compute'), nodeName=node.name, meshroomFile=meshroomFile, parallelArgs=parallelArgs), tags=tags, - rezPackages=[self.MESHROOM_PACKAGE], + rezPackages=[self.MESHROOM_PACKAGE] if self.MESHROOM_PACKAGE else None, requirements={'service': str(','.join(allRequirements))}, **arguments) return task @@ -64,9 +68,10 @@ def submit(self, nodes, edges, filepath): 'nbFrames': str(nbFrames), 'comment': comment, } + allRequirements = list(self.config.get('BASE', [])) # Create Job Graph - job = simpleFarm.Job(name, tags=mainTags) + job = simpleFarm.Job(name, tags=mainTags, requirements={'service': str(','.join(allRequirements))}) nodeNameToTask = {} From b1984bca97afb3f9dd005eeea786cf1ed5576f59 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 18 Mar 2019 14:51:01 +0100 Subject: [PATCH 181/293] [submitters] update submitter config --- meshroom/submitters/simpleFarmConfig.json | 9 +++++---- meshroom/submitters/simpleFarmSubmitter.py | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/meshroom/submitters/simpleFarmConfig.json b/meshroom/submitters/simpleFarmConfig.json index dd0e41d04c..2e788f78b1 100644 --- a/meshroom/submitters/simpleFarmConfig.json +++ b/meshroom/submitters/simpleFarmConfig.json @@ -1,9 +1,10 @@ { - "BASE": ["mikrosRender", "!RenderLow", "!Wkst_OS", "!\"vfxpc1*\"", "!\"vfxpc??\""], + "LIMITS": ["mikros"], + "BASE": ["mikrosRender"], "CPU": { "NONE": [], "NORMAL": [], - "INTENSIVE": ["\"RenderHigh*\"", "@.nCPUs>20"] + "INTENSIVE": ["@.nCPUs>30"] }, "RAM": { "NONE": [], @@ -12,7 +13,7 @@ }, "GPU": { "NONE": [], - "NORMAL": ["!\"*loc*\"", "Wkst"], - "INTENSIVE": ["!\"*loc*\"", "Wkst"] + "NORMAL": ["!\"*rnd*\""], + "INTENSIVE": ["!\"*rnd*\"", "@.nCPUs=12"] } } diff --git a/meshroom/submitters/simpleFarmSubmitter.py b/meshroom/submitters/simpleFarmSubmitter.py index a2b8041ea9..2340150f76 100644 --- a/meshroom/submitters/simpleFarmSubmitter.py +++ b/meshroom/submitters/simpleFarmSubmitter.py @@ -42,7 +42,7 @@ def createTask(self, meshroomFile, node): tags['nbFrames'] = nbFrames tags['prod'] = self.prod - allRequirements = list(self.config.get('BASE', [])) + allRequirements = list() allRequirements.extend(self.config['CPU'].get(node.nodeDesc.cpu.name, [])) allRequirements.extend(self.config['RAM'].get(node.nodeDesc.ram.name, [])) allRequirements.extend(self.config['GPU'].get(node.nodeDesc.gpu.name, [])) @@ -71,7 +71,10 @@ def submit(self, nodes, edges, filepath): allRequirements = list(self.config.get('BASE', [])) # Create Job Graph - job = simpleFarm.Job(name, tags=mainTags, requirements={'service': str(','.join(allRequirements))}) + job = simpleFarm.Job(name, + tags=mainTags, + requirements={'limits': self.config.get('LIMITS', ''), 'service': str(','.join(allRequirements))}, + ) nodeNameToTask = {} From 49f5052a09c0f56433829d37a6badc3e2402cdc4 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 8 Apr 2019 10:16:02 +0200 Subject: [PATCH 182/293] [submitters] simpleFarm: minor tractor adjustments --- meshroom/submitters/simpleFarmConfig.json | 3 +-- meshroom/submitters/simpleFarmSubmitter.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/meshroom/submitters/simpleFarmConfig.json b/meshroom/submitters/simpleFarmConfig.json index 2e788f78b1..d3f1136909 100644 --- a/meshroom/submitters/simpleFarmConfig.json +++ b/meshroom/submitters/simpleFarmConfig.json @@ -1,5 +1,4 @@ { - "LIMITS": ["mikros"], "BASE": ["mikrosRender"], "CPU": { "NONE": [], @@ -14,6 +13,6 @@ "GPU": { "NONE": [], "NORMAL": ["!\"*rnd*\""], - "INTENSIVE": ["!\"*rnd*\"", "@.nCPUs=12"] + "INTENSIVE": ["!\"*rnd*\"", "@.nCPUs>=12"] } } diff --git a/meshroom/submitters/simpleFarmSubmitter.py b/meshroom/submitters/simpleFarmSubmitter.py index 2340150f76..fc46bf8c7a 100644 --- a/meshroom/submitters/simpleFarmSubmitter.py +++ b/meshroom/submitters/simpleFarmSubmitter.py @@ -73,7 +73,7 @@ def submit(self, nodes, edges, filepath): # Create Job Graph job = simpleFarm.Job(name, tags=mainTags, - requirements={'limits': self.config.get('LIMITS', ''), 'service': str(','.join(allRequirements))}, + requirements={'service': str(','.join(allRequirements))}, ) nodeNameToTask = {} From c88c78b47642fd2911db052930df4516a065f21a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 11 Apr 2019 11:40:08 +0200 Subject: [PATCH 183/293] [cxFreeze] add comment --- meshroom/core/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index d85f76ff15..20b5f774c8 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -14,6 +14,7 @@ import sys try: + # for cx_freeze import encodings.ascii import encodings.idna import encodings.utf_8 From 00188c9aefa383505551e1ffed8553c3acdcd9ef Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 22 Mar 2019 17:32:42 +0100 Subject: [PATCH 184/293] [nodes] texturing: add support for multi-band blending --- meshroom/nodes/aliceVision/Texturing.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 7921af2ead..53fcee2e25 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -90,12 +90,19 @@ class Texturing(desc.CommandLineNode): range=(0, 100, 1), uid=[0], ), - desc.IntParam( - name='maxNbImagesForFusion', - label='Max Number of Images For Fusion', - description='''Max number of images to combine to create the final texture''', - value=3, - range=(0, 10, 1), + #desc.ListAttribute( + # name='multiBandNbContrib', + # elementDesc=desc.IntParam(name='levelContrib', label='', description='', value=1, uid=[0], range=(0, 50, 1)), + # label='Nb Contribution per Band', + # value=[1, 5, 10], # TODO: need support for default values on ListAttribute + # description='Number of images to combine per band of frequencies to create the final texture.', + #), + desc.FloatParam( + name='multiBandKernelSize', + label='MultiBand Blending Kernel Size', + description='''Kernel size for the lowest band of frequencies''', + value=40.0, + range=(0.0, 100.0, 1.0), uid=[0], ), desc.FloatParam( From d0c5a5ffb5a3b8e9d5dda916e25899292f344960 Mon Sep 17 00:00:00 2001 From: cclauss Date: Thu, 18 Apr 2019 10:30:57 +0200 Subject: [PATCH 185/293] Avoid issue with use ==/!= to compare str, bytes, and int literals [flake8](http://flake8.pycqa.org) testing of https://github.com/alicevision/meshroom on Python 3.7.1 $ __flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics__ ``` ./meshroom/core/node.py:461:34: F632 use ==/!= to compare str, bytes, and int literals if v is not None and v is not '': ^ ./meshroom/core/node.py:480:34: F632 use ==/!= to compare str, bytes, and int literals if v is not None and v is not '': ^ 2 F632 use ==/!= to compare str, bytes, and int literals 2 ``` --- 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 6fcf02e8f2..fab7d02f8d 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -458,7 +458,7 @@ def _buildCmdVars(self): self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) self._cmdVars[name + 'Value'] = str(v) - if v is not None and v is not '': + if v not in (None, ''): self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ ' ' + self._cmdVars[name] @@ -477,7 +477,7 @@ def _buildCmdVars(self): self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) self._cmdVars[name + 'Value'] = str(v) - if v is not None and v is not '': + if v not in (None, ''): self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ ' ' + self._cmdVars[name] From 275c4133fea90fa6a84400f5c6b538b9cf4ddf91 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 26 Apr 2019 10:42:41 +0200 Subject: [PATCH 186/293] [core] make range complete block size accessible to command line nodes * range 'blockSize' is now the complete block size (equal for all iterations) * command lines nodes can use this complete block size to compute the iteration number on software side; delegate range cropping for last iteration to software. * 'effectiveBlockSize' is still accessible through 'rangeEffectiveBlockSize' parameter --- meshroom/core/desc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 34674d7753..a9a96a1051 100755 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -269,7 +269,8 @@ def toDict(self): "rangeStart": self.start, "rangeEnd": self.end, "rangeLast": self.last, - "rangeBlockSize": self.effectiveBlockSize, + "rangeBlockSize": self.blockSize, + "rangeEffectiveBlockSize": self.effectiveBlockSize, "rangeFullSize": self.fullSize, } From 6d0732358070fd66fd08b03cfb0db990c576923d Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 7 May 2019 10:39:20 +0200 Subject: [PATCH 187/293] [ui] compatibility fixes for PySide2 5.12.x --- meshroom/ui/components/edge.py | 2 +- meshroom/ui/utils.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/components/edge.py b/meshroom/ui/components/edge.py index 0b173fc6b5..64a9f0e3c8 100755 --- a/meshroom/ui/components/edge.py +++ b/meshroom/ui/components/edge.py @@ -118,7 +118,7 @@ def setContainsMouse(self, value): containsMouse = Property(float, getContainsMouse, notify=containsMouseChanged) acceptedButtons = Property(int, lambda self: super(EdgeMouseArea, self).acceptedMouseButtons, - lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(value)) + lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(Qt.MouseButtons(value))) pressed = Signal(MouseEvent) released = Signal(MouseEvent) diff --git a/meshroom/ui/utils.py b/meshroom/ui/utils.py index f697f89dfe..cfdb3a55e5 100755 --- a/meshroom/ui/utils.py +++ b/meshroom/ui/utils.py @@ -3,7 +3,10 @@ from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property, QObject from PySide2.QtQml import QQmlApplicationEngine -from PySide2 import shiboken2 +try: + from PySide2 import shiboken2 +except: + import shiboken2 class QmlInstantEngine(QQmlApplicationEngine): From 9ce077778b3bcb9a3cea516550613c5e0e87afa8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 7 May 2019 11:03:54 +0200 Subject: [PATCH 188/293] [ui] add more explicit doc for QML helpers singleton --- meshroom/ui/app.py | 18 ++++++++++++------ meshroom/ui/qml/Utils/qmldir | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 0c9032dbc5..219a016365 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -84,14 +84,20 @@ def __init__(self, args): # expose available node types that can be instantiated self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys())) + + # instantiate Reconstruction object r = Reconstruction(parent=self) self.engine.rootContext().setContextProperty("_reconstruction", r) - pm = PaletteManager(self.engine, parent=self) - self.engine.rootContext().setContextProperty("_PaletteManager", pm) - fpHelper = FilepathHelper(parent=self) - self.engine.rootContext().setContextProperty("Filepath", fpHelper) - scene3DHelper = Scene3DHelper(parent=self) - self.engine.rootContext().setContextProperty("Scene3DHelper", scene3DHelper) + + # those helpers should be available from QML Utils module as singletons, but: + # - qmlRegisterUncreatableType is not yet available in PySide2 + # - declaring them as singleton in qmldir file causes random crash at exit + # => expose them as context properties instead + self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self)) + self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self)) + + # additional context properties + self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self)) self.engine.rootContext().setContextProperty("MeshroomApp", self) # Request any potential computation to stop on exit self.aboutToQuit.connect(r.stopExecution) diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index a81d6c9639..230e41e404 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -4,6 +4,6 @@ singleton Colors 1.0 Colors.qml SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml Request 1.0 request.js Format 1.0 format.js -# causes random crash at application exit +# using singleton here causes random crash at application exit # singleton Filepath 1.0 Filepath.qml # singleton Scene3DHelper 1.0 Scene3DHelper.qml From 438622a14bbba08d110fe51bdda29cec4c98dbdd Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 7 May 2019 11:46:46 +0200 Subject: [PATCH 189/293] [ui] Introduce ClipboardHelper for copying to clipboard from QML * add ClipboardHelper class that contains a QClipboard and exposes its method as Slots * use Clipboard instead of hidden TextEdit where meaningful --- meshroom/ui/app.py | 2 ++ meshroom/ui/components/__init__.py | 2 ++ meshroom/ui/components/clipboard.py | 20 ++++++++++++++++++++ meshroom/ui/qml/Utils/Clipboard.qml | 9 +++++++++ meshroom/ui/qml/Utils/qmldir | 1 + meshroom/ui/qml/Viewer3D/Inspector3D.qml | 4 +--- 6 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 meshroom/ui/components/clipboard.py create mode 100644 meshroom/ui/qml/Utils/Clipboard.qml diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 219a016365..ea5be436d2 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -9,6 +9,7 @@ import meshroom from meshroom.core import nodesDesc from meshroom.ui import components +from meshroom.ui.components.clipboard import ClipboardHelper from meshroom.ui.components.filepath import FilepathHelper from meshroom.ui.components.scene3D import Scene3DHelper from meshroom.ui.palette import PaletteManager @@ -95,6 +96,7 @@ def __init__(self, args): # => expose them as context properties instead self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self)) self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self)) + self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self)) # additional context properties self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self)) diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index ae258aa6d8..6c94a10ded 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -1,11 +1,13 @@ def registerTypes(): from PySide2.QtQml import qmlRegisterType + from meshroom.ui.components.clipboard import ClipboardHelper from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.filepath import FilepathHelper from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") + qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController") diff --git a/meshroom/ui/components/clipboard.py b/meshroom/ui/components/clipboard.py new file mode 100644 index 0000000000..2cca6e1a5a --- /dev/null +++ b/meshroom/ui/components/clipboard.py @@ -0,0 +1,20 @@ +from PySide2.QtCore import Slot, QObject +from PySide2.QtGui import QClipboard + + +class ClipboardHelper(QObject): + """ + Simple wrapper around a QClipboard with methods exposed as Slots for QML use. + """ + + def __init__(self, parent=None): + super(ClipboardHelper, self).__init__(parent) + self._clipboard = QClipboard(parent=self) + + @Slot(str) + def setText(self, value): + self._clipboard.setText(value) + + @Slot() + def clear(self): + self._clipboard.clear() diff --git a/meshroom/ui/qml/Utils/Clipboard.qml b/meshroom/ui/qml/Utils/Clipboard.qml new file mode 100644 index 0000000000..07898a8a23 --- /dev/null +++ b/meshroom/ui/qml/Utils/Clipboard.qml @@ -0,0 +1,9 @@ +pragma Singleton +import Meshroom.Helpers 1.0 + +/** + * Clipboard singleton object to copy values to paste buffer. + */ +ClipboardHelper { + +} diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index 230e41e404..c8767f30e7 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -5,5 +5,6 @@ SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml Request 1.0 request.js Format 1.0 format.js # using singleton here causes random crash at application exit +# singleton Clipboard 1.0 Clipboard.qml # singleton Filepath 1.0 Filepath.qml # singleton Scene3DHelper 1.0 Scene3DHelper.qml diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index fe6b80b066..471eac3f42 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -289,9 +289,7 @@ FloatingPane { } MenuItem { text: "Copy Path" - // hidden TextEdit to copy to clipboard - TextEdit { id: fullpath; visible: false; text: Filepath.normpath(model.source) } - onTriggered: { fullpath.selectAll(); fullpath.copy(); } + onTriggered: Clipboard.setText(Filepath.normpath(model.source)) } MenuSeparator {} MenuItem { From 55dba55d19b666cee0068ce44a4b0ea1a48cb3e2 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 7 May 2019 11:45:26 +0200 Subject: [PATCH 190/293] [ui] New TextFileViewer for displaying log files Introduce a new TextFileViewer component with auto-reload feature based on a ListView instead of a TextArea for performance reasons. Uses the text content split on line breaks as ListView's model. Features: * auto-scroll to bottom * display line numbers * customizable line delegates (e.g.: display a real progress bar) * color lines according to log level --- meshroom/ui/qml/Controls/TextFileViewer.qml | 331 ++++++++++++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + meshroom/ui/qml/GraphEditor/NodeEditor.qml | 44 +-- meshroom/ui/qml/GraphEditor/NodeLog.qml | 160 +--------- 4 files changed, 363 insertions(+), 173 deletions(-) create mode 100644 meshroom/ui/qml/Controls/TextFileViewer.qml diff --git a/meshroom/ui/qml/Controls/TextFileViewer.qml b/meshroom/ui/qml/Controls/TextFileViewer.qml new file mode 100644 index 0000000000..19f07c6d2d --- /dev/null +++ b/meshroom/ui/qml/Controls/TextFileViewer.qml @@ -0,0 +1,331 @@ +import QtQuick 2.11 +import QtQml.Models 2.11 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.11 +import MaterialIcons 2.2 + +import Utils 1.0 + +/** + * Text file viewer with auto-reload feature. + * Uses a ListView with one delegate by line instead of a TextArea for performance reasons. + */ +Item { + id: root + + /// Source text file to load + property url source + /// Whether to periodically reload the source file + property bool autoReload: false + /// Interval (in ms) at which source file should be reloaded if autoReload is enabled + property int autoReloadInterval: 2000 + /// Whether the source is currently being loaded + property bool loading: false + + onSourceChanged: loadSource() + onVisibleChanged: if(visible) loadSource() + + RowLayout { + anchors.fill: parent + spacing: 0 + + // Toolbar + Pane { + Layout.alignment: Qt.AlignTop + Layout.fillHeight: true + padding: 0 + background: Rectangle { color: Qt.darker(Colors.sysPalette.window, 1.2) } + Column { + height: parent.height + ToolButton { + text: MaterialIcons.refresh + ToolTip.text: "Reload" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: loadSource(false) + } + ToolButton { + text: MaterialIcons.vertical_align_top + ToolTip.text: "Scroll to Top" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: textView.positionViewAtBeginning() + } + ToolButton { + text: MaterialIcons.vertical_align_bottom + ToolTip.text: "Scroll to Bottom" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: textView.positionViewAtEnd() + checkable: false + checked: textView.atYEnd + } + ToolButton { + text: MaterialIcons.assignment + ToolTip.text: "Copy" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: copySubMenu.open() + Menu { + id: copySubMenu + x: parent.width + + MenuItem { + text: "Copy Visible Text" + onTriggered: { + var t = ""; + for(var i = textView.firstVisibleIndex(); i < textView.lastVisibleIndex(); ++i) + t += textView.model[i] + "\n"; + Clipboard.setText(t); + } + } + MenuItem { + text: "Copy All" + onTriggered: { + Clipboard.setText(textView.text); + } + } + } + } + ToolButton { + text: MaterialIcons.open_in_new + ToolTip.text: "Open Externally" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + enabled: root.source !== "" + onClicked: Qt.openUrlExternally(root.source) + } + } + } + + MouseArea { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 4 + + ListView { + id: textView + + property string text + + // model consists in text split by line + model: textView.text.split("\n") + visible: text != "" + + anchors.fill: parent + clip: true + focus: true + + // custom key navigation handling + keyNavigationEnabled: false + highlightFollowsCurrentItem: true + highlightMoveDuration: 0 + Keys.onPressed: { + switch(event.key) + { + case Qt.Key_Home: + textView.positionViewAtBeginning(); + break; + case Qt.Key_End: + textView.positionViewAtEnd(); + break; + case Qt.Key_Up: + currentIndex = firstVisibleIndex(); + decrementCurrentIndex(); + break; + case Qt.Key_Down: + currentIndex = lastVisibleIndex(); + incrementCurrentIndex(); + break; + case Qt.Key_PageUp: + textView.positionViewAtIndex(firstVisibleIndex(), ListView.End); + break; + case Qt.Key_PageDown: + textView.positionViewAtIndex(lastVisibleIndex(), ListView.Beginning); + break; + } + } + + function setText(value, keepPosition) { + // store cursor position and content position + var topIndex = firstVisibleIndex(); + var scrollToBottom = atYEnd; + // replace text + text = value; + + if(scrollToBottom) + positionViewAtEnd(); + else if(topIndex !== firstVisibleIndex() && keepPosition) + positionViewAtIndex(topIndex, ListView.Beginning); + } + + function firstVisibleIndex() { + return indexAt(contentX, contentY); + } + + function lastVisibleIndex() { + return indexAt(contentX, contentY + height - 2); + } + + ScrollBar.vertical: ScrollBar { + id: vScrollBar + minimumSize: 0.05 + } + + ScrollBar.horizontal: ScrollBar { minimumSize: 0.1 } + + // TextMetrics for line numbers column + TextMetrics { + id: lineMetrics + font.family: "Monospace, Consolas, Monaco" + text: textView.count * 10 + } + + // TextMetrics for textual progress bar + TextMetrics { + id: progressMetrics + // total number of character in textual progress bar + property int count: 51 + property string character: '*' + text: character.repeat(count) + } + + delegate: RowLayout { + width: textView.width + spacing: 6 + + // Line number + Label { + text: index + 1 + Layout.minimumWidth: lineMetrics.width + rightPadding: 2 + enabled: false + Layout.fillHeight: true + horizontalAlignment: Text.AlignRight + } + + Loader { + id: delegateLoader + Layout.fillWidth: true + // default line delegate + sourceComponent: line_component + + // line delegate selector based on content + StateGroup { + states: [ + State { + name: "progressBar" + // detect textual progressbar (non empty line with only progressbar character) + when: modelData.trim().length + && modelData.split(progressMetrics.character).length - 1 === modelData.trim().length + PropertyChanges { + target: delegateLoader + sourceComponent: progressBar_component + } + } + ] + } + + // ProgressBar delegate + Component { + id: progressBar_component + Item { + Layout.fillWidth: true + implicitHeight: progressMetrics.height + ProgressBar { + width: progressMetrics.width + height: parent.height - 2 + anchors.verticalCenter: parent.verticalCenter + from: 0 + to: progressMetrics.count + value: modelData.length + } + } + } + + // Default line delegate + Component { + id: line_component + TextInput { + wrapMode: Text.WrapAnywhere + text: modelData + font.family: "Monospace, Consolas, Monaco" + padding: 0 + selectByMouse: true + readOnly: true + selectionColor: Colors.sysPalette.highlight + persistentSelection: false + Keys.forwardTo: [textView] + + color: { + // color line according to log level + if(text.indexOf("[warning]") >= 0) + return Colors.orange; + else if(text.indexOf("[error]") >= 0) + return Colors.red; + return palette.text; + } + } + } + } + } + } + + RowLayout { + anchors.fill: parent + anchors.rightMargin: vScrollBar.width + z: -1 + + Item { + Layout.preferredWidth: lineMetrics.width + Layout.fillHeight: true + } + + // IBeamCursor shape overlay + MouseArea { + Layout.fillWidth: true + Layout.fillHeight: true + cursorShape: Qt.IBeamCursor + } + } + + // File loading indicator + BusyIndicator { + Component.onCompleted: running = Qt.binding(function() { return root.loading }) + padding: 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitWidth: 16 + implicitHeight: 16 + } + } + } + + // Auto-reload current file timer + Timer { + running: root.autoReload + interval: root.autoReloadInterval + repeat: true + // reload file on start and stop + onRunningChanged: loadSource(true) + onTriggered: loadSource(true) + } + + // Load current source file and update ListView's model + function loadSource(keepPosition = false) + { + if(!visible) + return; + loading = true; + var xhr = new XMLHttpRequest; + xhr.open("GET", root.source); + xhr.onload = function() { + // - can't rely on 'Last-Modified' header response to verify + // that file has changed on disk (not always up-to-date) + // - instead, let QML engine evaluate whether 'text' property value has changed + textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition); + loading = false; + }; + xhr.send(); + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 2efd461a28..295947a2a4 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -5,3 +5,4 @@ Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml Panel 1.0 Panel.qml SearchBar 1.0 SearchBar.qml +TextFileViewer 1.0 TextFileViewer.qml diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 5cb63f869e..3ce454ff76 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -96,7 +96,7 @@ Panel { } Label { color: Qt.lighter(palette.mid, 1.2) - text: "Select a Node to edit its Attributes" + text: "Select a Node to access its Details" } } } @@ -128,28 +128,30 @@ Panel { node: root.node } } - TabBar { - id: tabBar - - Layout.fillWidth: true - width: childrenRect.width - position: TabBar.Footer - TabButton { - text: "Attributes" - width: implicitWidth - padding: 4 - leftPadding: 8 - rightPadding: leftPadding - } - TabButton { - text: "Log" - width: implicitWidth - leftPadding: 8 - rightPadding: leftPadding - } - } } } } + + TabBar { + id: tabBar + + Layout.fillWidth: true + width: childrenRect.width + position: TabBar.Footer + currentIndex: 1 + TabButton { + text: "Attributes" + width: implicitWidth + padding: 4 + leftPadding: 8 + rightPadding: leftPadding + } + TabButton { + text: "Log" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } + } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index fb2fb84e35..455b2b0c90 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -1,9 +1,9 @@ -import QtQuick 2.9 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 // SplitView import QtQuick.Layouts 1.3 import MaterialIcons 2.2 -import Utils 1.0 +import Controls 1.0 import "common.js" as Common @@ -85,17 +85,13 @@ FocusScope { id: fileSelector Layout.fillWidth: true property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : "" - property string lastLoadedFile - property date lastModTime - onCurrentFileChanged: if(visible) loadCurrentFile() - onVisibleChanged: if(visible) loadCurrentFile() - TabButton { property string fileProperty: "logFile" - text: "Log" + text: "Output" padding: 4 } + TabButton { property string fileProperty: "statisticsFile" text: "Statistics" @@ -108,152 +104,12 @@ FocusScope { } } - RowLayout { - Layout.fillHeight: true + TextFileViewer { Layout.fillWidth: true - spacing: 0 - Pane { - id: tb - Layout.alignment: Qt.AlignTop - Layout.fillHeight: true - padding: 0 - background: Rectangle { color: Qt.darker(activePalette.window, 1.2) } - Column { - height: parent.height - ToolButton { - text: MaterialIcons.refresh - ToolTip.text: "Refresh" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: loadCurrentFile(false) - } - ToolButton { - id: autoRefresh - text: MaterialIcons.timer - ToolTip.text: "Auto-Refresh when Running" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - checked: true - checkable: true - } - ToolButton { - text: MaterialIcons.vertical_align_top - ToolTip.text: "Scroll to Top" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: logArea.cursorPosition = 0 - } - ToolButton { - text: MaterialIcons.vertical_align_bottom - ToolTip.text: "Scroll to Bottom" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: logArea.cursorPosition = logArea.length - } - ToolButton { - id: autoScroll - text: MaterialIcons.system_update_alt - ToolTip.text: "Auto-Scroll to Bottom" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - checkable: true - checked: true - } - ToolButton { - text: MaterialIcons.open_in_new - ToolTip.text: "Open Externally" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - enabled: fileSelector.currentFile != "" - onClicked: Qt.openUrlExternally(Filepath.stringToUrl(fileSelector.currentFile)) - } - } - } - // Log display - ScrollView { - id: logScrollView - Layout.fillWidth: true - Layout.fillHeight: true - TextArea { - id: logArea - selectByMouse: true - selectByKeyboard: true - persistentSelection: true - font.family: "Monospace, Consolas, Monaco" - } - } + Layout.fillHeight: true + autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" + source: Filepath.stringToUrl(fileSelector.currentFile) } } } - - // Auto-reload current file if NodeChunk is being computed - Timer { - running: autoRefresh.checked && chunksLV.currentChunk != undefined && chunksLV.currentChunk.statusName === "RUNNING" - interval: 2000 - repeat: true - // reload file on start and stop - onRunningChanged: loadCurrentFile(true) - onTriggered: loadCurrentFile(true) - } - - function loadCurrentFile(keepCursorPosition) - { - if(keepCursorPosition == undefined) - keepCursorPosition = false - var xhr = new XMLHttpRequest; - xhr.open("GET", Filepath.stringToUrl(fileSelector.currentFile)); - xhr.onreadystatechange = function() { - if(xhr.readyState == XMLHttpRequest.HEADERS_RECEIVED) - { - // if the file is already open - // check last modification date - var lastMod = new Date(xhr.getResponseHeader("Last-Modified")); - if(fileSelector.lastLoadedFile == fileSelector.currentFile - && lastMod.getTime() == fileSelector.lastModTime.getTime() ) - { - // file has not changed, don't reload it - xhr.doLoad = false; - return - } - // file is different or last modification time has changed - fileSelector.lastModTime = lastMod - xhr.doLoad = true - } - if (xhr.readyState == XMLHttpRequest.DONE) { - // store lastLoadedFile url - fileSelector.lastLoadedFile = fileSelector.currentFile - // if responseText should not be loaded - if(!xhr.doLoad) - { - // file could not be opened, reset text and lastModTime - if(xhr.status == 0) - { - fileSelector.lastModTime = new Date() - logArea.text = '' - } - return; - } - // store cursor position and content position - var cursorPosition = logArea.cursorPosition; - var contentY = logScrollView.ScrollBar.vertical.position; - - // replace text - logArea.text = xhr.responseText; - - if(autoScroll.checked) - { - // Reset cursor position to trigger scroll to bottom - logArea.cursorPosition = 0; - logArea.cursorPosition = logArea.length; - } - else if(keepCursorPosition) - { - if(cursorPosition) - logArea.cursorPosition = cursorPosition; - logScrollView.ScrollBar.vertical.position = contentY - } - } - }; - xhr.send(); - } } From e40b0e57b88506ef7aa08d0fb67dbaae28ccf3b8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 7 May 2019 12:29:53 +0200 Subject: [PATCH 191/293] [ui] ChunksMonitor: asynchronous status files modification time polling * make last modification time check on status asynchronous using a dedicated thread + use a ThreadPool to run tasks in parallel * avoid UI freeze when checking for status updates + increase performances --- meshroom/ui/app.py | 5 +- meshroom/ui/graph.py | 133 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 109 insertions(+), 29 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index ea5be436d2..45819fc0a9 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -101,8 +101,9 @@ def __init__(self, args): # additional context properties self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self)) self.engine.rootContext().setContextProperty("MeshroomApp", self) - # Request any potential computation to stop on exit - self.aboutToQuit.connect(r.stopExecution) + + # request any potential computation to stop on exit + self.aboutToQuit.connect(r.stopChildThreads) parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.') parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index ea61d8a347..0f7b1cf2c0 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -2,8 +2,10 @@ # coding:utf-8 import logging import os +import time from enum import Enum -from threading import Thread +from threading import Thread, Event +from multiprocessing.pool import ThreadPool from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint @@ -16,6 +18,61 @@ from meshroom.ui.utils import makeProperty +class FilesModTimePollerThread(QObject): + """ + Thread responsible for non-blocking polling of last modification times of a list of files. + Uses a Python ThreadPool internally to split tasks on multiple threads. + """ + timesAvailable = Signal(list) + + def __init__(self, parent=None): + super(FilesModTimePollerThread, self).__init__(parent) + self._thread = None + self._threadPool = ThreadPool(4) + self._stopFlag = Event() + self._refreshInterval = 5 # refresh interval in seconds + self._files = [] + + def start(self, files): + """ Start polling thread. + + Args: + files: the list of files to monitor + """ + if self._thread: + # thread already running, return + return + if not files: + # file list is empty + return + self._stopFlag.clear() + self._files = files or [] + self._thread = Thread(target=self.run) + self._thread.start() + + def stop(self): + """ Request polling thread to stop. """ + if not self._thread: + return + self._stopFlag.set() + self._thread.join() + self._thread = None + + @staticmethod + def getFileLastModTime(f): + """ Return 'mtime' of the file if it exists, -1 otherwise. """ + try: + return os.path.getmtime(f) + except OSError: + return -1 + + def run(self): + """ Poll watched files for last modification time. """ + while not self._stopFlag.wait(self._refreshInterval): + times = self._threadPool.map(FilesModTimePollerThread.getFileLastModTime, self._files) + self.timesAvailable.emit(times) + + class ChunksMonitor(QObject): """ ChunksMonitor regularly check NodeChunks' status files for modification and trigger their update on change. @@ -29,52 +86,69 @@ class ChunksMonitor(QObject): def __init__(self, chunks=(), parent=None): super(ChunksMonitor, self).__init__(parent) self.lastModificationRecords = dict() + self._filesTimePoller = FilesModTimePollerThread(parent=self) + self._filesTimePoller.timesAvailable.connect(self.compareFilesTimes) + self._pollerOutdated = False self.setChunks(chunks) - # Check status files every x seconds - # TODO: adapt frequency according to graph compute status - self.startTimer(5000) def setChunks(self, chunks): """ Set the list of chunks to monitor. """ + self._filesTimePoller.stop() self.clear() for chunk in chunks: - f = chunk.statusFile - # Store a record of {chunk: status file last modification} - self.lastModificationRecords[chunk] = self.getFileLastModTime(f) + # initialize last modification times to current time for all chunks + self.lastModificationRecords[chunk] = time.time() # For local use, handle statusChanged emitted directly from the node chunk chunk.statusChanged.connect(self.onChunkStatusChanged) + self._pollerOutdated = True self.chunkStatusChanged.emit(None, -1) + self._filesTimePoller.start(self.statusFiles) + self._pollerOutdated = False + + def stop(self): + """ Stop the status files monitoring. """ + self._filesTimePoller.stop() def clear(self): - """ Clear the list of monitored chunks """ - for ch in self.lastModificationRecords: - ch.statusChanged.disconnect(self.onChunkStatusChanged) + """ Clear the list of monitored chunks. """ + for chunk in self.lastModificationRecords: + chunk.statusChanged.disconnect(self.onChunkStatusChanged) self.lastModificationRecords.clear() - def timerEvent(self, evt): - self.checkFileTimes() - def onChunkStatusChanged(self): """ React to change of status coming from the NodeChunk itself. """ chunk = self.sender() assert chunk in self.lastModificationRecords - # Update record entry for this file so that it's up-to-date on next timerEvent - self.lastModificationRecords[chunk] = self.getFileLastModTime(chunk.statusFile) + # update record entry for this file so that it's up-to-date on next timerEvent + # use current time instead of actual file's mtime to limit filesystem requests + self.lastModificationRecords[chunk] = time.time() self.chunkStatusChanged.emit(chunk, chunk.status.status) - @staticmethod - def getFileLastModTime(f): - """ Return 'mtime' of the file if it exists, -1 otherwise. """ - return os.path.getmtime(f) if os.path.exists(f) else -1 - - def checkFileTimes(self): - """ Check status files last modification time and compare with stored value """ - for chunk, t in self.lastModificationRecords.items(): - lastMod = self.getFileLastModTime(chunk.statusFile) - if lastMod != t: - self.lastModificationRecords[chunk] = lastMod + @property + def statusFiles(self): + """ Get status file paths from current chunks. """ + return [c.statusFile for c in self.lastModificationRecords.keys()] + + def compareFilesTimes(self, times): + """ + Compare previous file modification times with results from last poll. + Trigger chunk status update if file was modified since. + + Args: + times: the last modification times for currently monitored files. + """ + if self._pollerOutdated: + return + + newRecords = dict(zip(self.lastModificationRecords.keys(), times)) + for chunk, previousTime in self.lastModificationRecords.items(): + lastModTime = newRecords.get(chunk, -1) + # update chunk status if: + # - last modification time is more recent than previous record + # - file is no more available (-1) + if lastModTime > previousTime or (lastModTime == -1 != previousTime): + self.lastModificationRecords[chunk] = lastModTime chunk.updateStatusFromCache() - logging.debug("Status for node {} changed: {}".format(chunk.node, chunk.status.status)) chunkStatusChanged = Signal(NodeChunk, int) @@ -244,6 +318,11 @@ def clear(self): self._sortedDFSChunks.clear() self._undoStack.clear() + def stopChildThreads(self): + """ Stop all child threads. """ + self.stopExecution() + self._chunksMonitor.stop() + def load(self, filepath): g = Graph('') g.load(filepath) From 17556427fdd08803f52a0ad30a99c574b9597321 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 9 May 2019 18:18:48 +0200 Subject: [PATCH 192/293] [ui] NodeLog: PySide2 5.11 compatibility fixes * downgrade imports to work with versions prior to 5.12 * remove ES7 syntax --- meshroom/ui/qml/Controls/TextFileViewer.qml | 14 ++++++++------ meshroom/ui/qml/GraphEditor/NodeLog.qml | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/meshroom/ui/qml/Controls/TextFileViewer.qml b/meshroom/ui/qml/Controls/TextFileViewer.qml index 19f07c6d2d..b2483ae515 100644 --- a/meshroom/ui/qml/Controls/TextFileViewer.qml +++ b/meshroom/ui/qml/Controls/TextFileViewer.qml @@ -1,6 +1,5 @@ import QtQuick 2.11 -import QtQml.Models 2.11 -import QtQuick.Controls 2.5 +import QtQuick.Controls 2.4 import QtQuick.Layouts 1.11 import MaterialIcons 2.2 @@ -312,19 +311,22 @@ Item { } // Load current source file and update ListView's model - function loadSource(keepPosition = false) + function loadSource(keepPosition) { if(!visible) return; loading = true; var xhr = new XMLHttpRequest; + xhr.open("GET", root.source); - xhr.onload = function() { + xhr.onreadystatechange = function() { // - can't rely on 'Last-Modified' header response to verify // that file has changed on disk (not always up-to-date) // - instead, let QML engine evaluate whether 'text' property value has changed - textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition); - loading = false; + if(xhr.readyState === XMLHttpRequest.DONE) { + textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition); + loading = false; + } }; xhr.send(); } diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 455b2b0c90..7b15150b9d 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -1,4 +1,4 @@ -import QtQuick 2.12 +import QtQuick 2.11 import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 // SplitView import QtQuick.Layouts 1.3 From 9f928e1ea0d48166d2af2be888d54c8971df4d4e Mon Sep 17 00:00:00 2001 From: Clara Date: Tue, 7 May 2019 10:48:01 +0200 Subject: [PATCH 193/293] [mesh] Texturing: set parameters for multi-band blending Use only 3 parameters : useScore, multiBandDownscale, multiBandNbContrib --- meshroom/core/attribute.py | 4 ++- meshroom/nodes/aliceVision/Texturing.py | 43 +++++++++++++++++-------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 5c341886b2..1bc105a20b 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -414,7 +414,9 @@ def getPrimitiveValue(self, exportDefault=True): return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items() if not attr.isDefault} def getValueStr(self): - return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value.objects.values()]) + # sort values based on child attributes group description order + sortedSubValues = [self._value.get(attr.name).getValueStr() for attr in self.attributeDesc.groupDesc] + return self.attributeDesc.joinChar.join(sortedSubValues) # Override value property value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 53fcee2e25..ef34e12b78 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -86,24 +86,39 @@ class Texturing(desc.CommandLineNode): name='padding', label='Padding', description='''Texture edge padding size in pixel''', - value=15, + value=5, range=(0, 100, 1), uid=[0], + advanced=True, ), - #desc.ListAttribute( - # name='multiBandNbContrib', - # elementDesc=desc.IntParam(name='levelContrib', label='', description='', value=1, uid=[0], range=(0, 50, 1)), - # label='Nb Contribution per Band', - # value=[1, 5, 10], # TODO: need support for default values on ListAttribute - # description='Number of images to combine per band of frequencies to create the final texture.', - #), - desc.FloatParam( - name='multiBandKernelSize', - label='MultiBand Blending Kernel Size', - description='''Kernel size for the lowest band of frequencies''', - value=40.0, - range=(0.0, 100.0, 1.0), + desc.BoolParam( + name='useScore', + label='Use Score', + description='Use triangles scores for multiband blending.', + value=True, + uid=[0], + advanced=True, + ), + desc.IntParam( + name='multiBandDownscale', + label='Multi Band Downscale', + description='''Width of frequency bands for multiband blending''', + value=2, + range=(1, 10, 1), uid=[0], + advanced=True, + ), + desc.GroupAttribute( + name="multiBandNbContrib", + label="MultiBand contributions", + groupDesc=[ + desc.IntParam(name="high", label="High Freq", description="High Frequency Band", value=1, uid=[0], range=None), + desc.IntParam(name="midHigh", label="Mid-High Freq", description="Mid-High Frequency Band", value=5, uid=[0], range=None), + desc.IntParam(name="midLow", label="Mid-Low Freq", description="Mid-Low Frequency Band", value=10, uid=[0], range=None), + desc.IntParam(name="low", label="Low Freq", description="Low Frequency Band", value=0, uid=[0], range=None), + ], + description='''Number of contributions per frequency band for multiband blending''', + advanced=True, ), desc.FloatParam( name='bestScoreThreshold', From 09b28bbc2aff779b6b023e38be4915ca4874e300 Mon Sep 17 00:00:00 2001 From: cclauss Date: Tue, 14 May 2019 12:30:37 +0200 Subject: [PATCH 194/293] if v: --- 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 fab7d02f8d..95f19b5e9e 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -458,7 +458,7 @@ def _buildCmdVars(self): self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) self._cmdVars[name + 'Value'] = str(v) - if v not in (None, ''): + if v: self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ ' ' + self._cmdVars[name] @@ -477,7 +477,7 @@ def _buildCmdVars(self): self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) self._cmdVars[name + 'Value'] = str(v) - if v not in (None, ''): + if v: self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ ' ' + self._cmdVars[name] From fefd6a72c8ec346d90f382250d54191fc7010cb2 Mon Sep 17 00:00:00 2001 From: cclauss Date: Tue, 14 May 2019 12:32:38 +0200 Subject: [PATCH 195/293] Travis CI: Add Python 3.7 to the testing --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 966a0d3446..3ba8cefcae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ language: python + +dist: xenial # required for Python >= 3.7 + python: - "2.7" - "3.6" + - "3.7" install: - "pip install -r requirements.txt -r dev_requirements.txt --timeout 45" From d7f8311dcf4ca4cfe9088691e0cc7f042f462571 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 15 May 2019 20:12:55 +0200 Subject: [PATCH 196/293] [ui] NodeLog: set TextFileViewer source when chunks ListView is ready Avoid going through an empty source when switching from one node to another (currentChunk being invalid the time the ListView is being constructed). --- meshroom/ui/qml/GraphEditor/NodeLog.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 7b15150b9d..872a1ef13a 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -85,6 +85,13 @@ FocusScope { id: fileSelector Layout.fillWidth: true property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : "" + onCurrentFileChanged: { + // only set text file viewer source when ListView is fully ready + // (either empty or fully populated with a valid currentChunk) + // to avoid going through an empty url when switching between two nodes + if(!chunksLV.count || chunksLV.currentChunk) + textFileViewer.source = Filepath.stringToUrl(currentFile); + } TabButton { property string fileProperty: "logFile" @@ -105,10 +112,11 @@ FocusScope { } TextFileViewer { + id: textFileViewer Layout.fillWidth: true Layout.fillHeight: true autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" - source: Filepath.stringToUrl(fileSelector.currentFile) + // source is set in fileSelector } } } From 55c9e3063d9875a3ed5788d8c702d3e49b27be1b Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 15 May 2019 20:16:45 +0200 Subject: [PATCH 197/293] [ui] TextFileViewer: simplify position restoring when source changes Remove 'keepPosition' parameter: always try to reset position as close as possible to the previous state. --- meshroom/ui/qml/Controls/TextFileViewer.qml | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/meshroom/ui/qml/Controls/TextFileViewer.qml b/meshroom/ui/qml/Controls/TextFileViewer.qml index b2483ae515..525fd9e340 100644 --- a/meshroom/ui/qml/Controls/TextFileViewer.qml +++ b/meshroom/ui/qml/Controls/TextFileViewer.qml @@ -41,7 +41,7 @@ Item { ToolTip.text: "Reload" ToolTip.visible: hovered font.family: MaterialIcons.fontFamily - onClicked: loadSource(false) + onClicked: loadSource() } ToolButton { text: MaterialIcons.vertical_align_top @@ -51,6 +51,7 @@ Item { onClicked: textView.positionViewAtBeginning() } ToolButton { + id: autoscroll text: MaterialIcons.vertical_align_bottom ToolTip.text: "Scroll to Bottom" ToolTip.visible: hovered @@ -145,17 +146,21 @@ Item { } } - function setText(value, keepPosition) { - // store cursor position and content position + function setText(value) { + // store current first index var topIndex = firstVisibleIndex(); - var scrollToBottom = atYEnd; + // store whether autoscroll to bottom is active + var scrollToBottom = atYEnd && autoscroll.checked; // replace text text = value; + // restore content position by either: + // - autoscrolling to bottom if(scrollToBottom) positionViewAtEnd(); - else if(topIndex !== firstVisibleIndex() && keepPosition) - positionViewAtIndex(topIndex, ListView.Beginning); + // - setting first visible index back (when possible) + else if(topIndex !== firstVisibleIndex()) + positionViewAtIndex(Math.min(topIndex, count-1), ListView.Beginning); } function firstVisibleIndex() { @@ -306,15 +311,17 @@ Item { interval: root.autoReloadInterval repeat: true // reload file on start and stop - onRunningChanged: loadSource(true) - onTriggered: loadSource(true) + onRunningChanged: loadSource() + onTriggered: loadSource() } + // Load current source file and update ListView's model - function loadSource(keepPosition) + function loadSource() { if(!visible) return; + loading = true; var xhr = new XMLHttpRequest; @@ -324,7 +331,7 @@ Item { // that file has changed on disk (not always up-to-date) // - instead, let QML engine evaluate whether 'text' property value has changed if(xhr.readyState === XMLHttpRequest.DONE) { - textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition); + textView.setText(xhr.status === 200 ? xhr.responseText : ""); loading = false; } }; From c36c175b672f17fac09e260af748b5548df7fbe9 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 20 May 2019 06:40:00 +0200 Subject: [PATCH 198/293] [readme] Add links to tutorials --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 333cf627ac..de4225d468 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,27 @@ The goal of photogrammetry is to reverse this process. See the [presentation of the pipeline steps](http://alicevision.github.io/#photogrammetry). +## Tutorials + +* [Meshroom: Open Source 3D Reconstruction Software](https://www.youtube.com/watch?v=v_O6tYKQEBA) by [Mikros Image](http://www.mikrosimage.com) + +Overall presentation of the Meshroom software. + +* [Meshroom Tutorial on Sketchfab](https://sketchfab.com/blogs/community/tutorial-meshroom-for-beginners) by [Mikros Image](http://www.mikrosimage.com) + +Detailed tutorial with a focus on the features of the 2019.1 release. + +* [Photogrammetry 2 – 3D scanning with just PHONE/CAMERA simpler, better than ever!](https://www.youtube.com/watch?v=1D0EhSi-vvc) by [Prusa 3D Printer](https://blog.prusaprinters.org) + +Overall presentation of the protogrammetry practice with Meshroom. + +* [How to 3D Photoscan Easy and Free! by ](https://www.youtube.com/watch?v=k4NTf0hMjtY) by [CG Geek](https://www.youtube.com/channel/UCG8AxMVa6eutIGxrdnDxWpQ) + +Overall presentation of the protogrammetry practice with Meshroom and detailed presentation how to do the retolopogy in Blender. + +* [Meshroom Survival Guide](https://www.youtube.com/watch?v=eiEaHLNJJ94) by [Moviola](https://moviola.com) + +Presentation of the Meshroom software with a focus on using it for Match Moving. ## License From 6fc1f09aed23f230da2b889efe015773fd57eaee Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 6 Jun 2019 13:04:03 +0200 Subject: [PATCH 199/293] [core] Add git branch name in version name During development the git branch will be added to the version name. In releases, as there is no git repository included, the __version__ will not be modified. --- meshroom/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index 8ee42b3cf4..d91e3c17c6 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -1,6 +1,17 @@ __version__ = "2019.1.0" import os +scriptPath = os.path.dirname(os.path.abspath( __file__ )) +headFilepath = os.path.join(scriptPath, "../.git/HEAD") +# If we are in a release, the git history will not exist. +# If we are in development, we declare the name of the git development branch in the version name +if os.path.exists(headFilepath): + with open (headFilepath, "r") as headFile: + data = headFile.readlines() + branchName = data[0].split('/')[-1] + # Add git branch name to the Meshroom version name + __version__ = __version__ + "-" + branchName + # Allow override from env variable __version__ = os.environ.get("REZ_MESHROOM_VERSION", __version__) From 5c29e29ef34a91b787266c32c6495b4af1378b0f Mon Sep 17 00:00:00 2001 From: Clara Date: Wed, 12 Jun 2019 17:26:24 +0200 Subject: [PATCH 200/293] [Texturing] change some default parameters --- meshroom/nodes/aliceVision/Texturing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index ef34e12b78..da3f411fd7 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -87,7 +87,7 @@ class Texturing(desc.CommandLineNode): label='Padding', description='''Texture edge padding size in pixel''', value=5, - range=(0, 100, 1), + range=(0, 20, 1), uid=[0], advanced=True, ), @@ -103,8 +103,8 @@ class Texturing(desc.CommandLineNode): name='multiBandDownscale', label='Multi Band Downscale', description='''Width of frequency bands for multiband blending''', - value=2, - range=(1, 10, 1), + value=4, + range=(0, 8, 2), uid=[0], advanced=True, ), @@ -117,7 +117,7 @@ class Texturing(desc.CommandLineNode): desc.IntParam(name="midLow", label="Mid-Low Freq", description="Mid-Low Frequency Band", value=10, uid=[0], range=None), desc.IntParam(name="low", label="Low Freq", description="Low Frequency Band", value=0, uid=[0], range=None), ], - description='''Number of contributions per frequency band for multiband blending''', + description='''Number of contributions per frequency band for multiband blending (each frequency band also contributes to lower bands)''', advanced=True, ), desc.FloatParam( From cb84dd46626d95c9d7d44be6f6577b7a1483594f Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 13 Jun 2019 15:55:35 +0200 Subject: [PATCH 201/293] [Texturing] set bestScoreThreshold default value to 0.1 keep only triangles with sufficient reprojection area --- meshroom/nodes/aliceVision/Texturing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index da3f411fd7..7b2a1e21a9 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -124,7 +124,7 @@ class Texturing(desc.CommandLineNode): name='bestScoreThreshold', label='Best Score Threshold', description='''(0.0 to disable filtering based on threshold to relative best score)''', - value=0.0, + value=0.1, range=(0.0, 1.0, 0.01), uid=[0], advanced=True, From da8189bb30c545b0bb390acdc0f9133940f64856 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 14 Jun 2019 17:55:05 +0200 Subject: [PATCH 202/293] [nodes][aliceVision] add LDRToHDR node --- meshroom/nodes/aliceVision/LDRToHDR.py | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 meshroom/nodes/aliceVision/LDRToHDR.py diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py new file mode 100644 index 0000000000..5952986546 --- /dev/null +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -0,0 +1,106 @@ +__version__ = "1.0" + +from meshroom.core import desc + + +class LDRToHDR(desc.CommandLineNode): + commandLine = 'aliceVision_convertLDRToHDR {allParams}' + + inputs = [ + desc.File( + name='input', + label='Input', + description="List of LDR images or a folder containing them ", + value='', + uid=[0], + ), + desc.ChoiceParam( + name='calibrationMethod', + label='Calibration Method', + description="Method used for camera calibration \n" + " * linear \n" + " * robertson \n" + " * debevec \n" + " * beta: grossberg", + values=['linear', 'robertson', 'debevec', 'grossberg'], + value='linear', + exclusive=True, + uid=[0], + ), + desc.File( + name='inputResponse', + label='Input Response', + description="external camera response file path to fuse all LDR images together.", + value='', + uid=[0], + ), + desc.StringParam( + name='targetExposureImage', + label='Target Exposure Image', + description="LDR image at the target exposure for the output HDR image to be centered.", + value='', + uid=[0], + ), + desc.ChoiceParam( + name='calibrationWeight', + label='Calibration Weight', + description="Weight function type (default, gaussian, triangle, 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 (gaussian, triangle, plateau).", + value='gaussian', + values=['gaussian', 'triangle', 'plateau'], + exclusive=True, + uid=[0], + ), + desc.FloatParam( + name='oversaturatedCorrection', + label='Oversaturated Correction', + description="Oversaturated correction for pixels oversaturated in all images: \n" + " - use 0 for no correction \n" + " - use 0.5 for interior lighting \n" + " - use 1 for outdoor lighting", + value=1, + range=(0, 1, 0.1), + uid=[0], + ), + desc.File( + name='recoverPath', + label='Recover Path', + description="Path to write recovered LDR image at the target exposure by applying inverse response on HDR image.", + 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='output', + label='Output', + description="Output HDR image path.", + value=desc.Node.internalFolder + 'hdr.exr', + uid=[], + ), + desc.File( + name='outputResponse', + label='Output Response', + description="Output response function path.", + value=desc.Node.internalFolder + 'response.ods', + uid=[], + ), + ] From a85b4660ce1ebf4a32a0bfbc8073f06970888ecc Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 17 Jun 2019 12:46:26 +0200 Subject: [PATCH 203/293] [packaging] dissociate meshroom.__version__ / __version_name__ Introduce meshroom.__version_name__ variable in order not to modify meshroom.__version__ and use this as displayed application version. meshroom.__version__ is stored in scene files and used when making a package and should not contain anything other than major.minor.micro information. --- meshroom/__init__.py | 36 +++++++++++++++++------------------- meshroom/ui/app.py | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index d91e3c17c6..bb76539cb4 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -1,23 +1,26 @@ __version__ = "2019.1.0" +__version_name__ = __version__ import os -scriptPath = os.path.dirname(os.path.abspath( __file__ )) -headFilepath = os.path.join(scriptPath, "../.git/HEAD") -# If we are in a release, the git history will not exist. -# If we are in development, we declare the name of the git development branch in the version name -if os.path.exists(headFilepath): - with open (headFilepath, "r") as headFile: - data = headFile.readlines() - branchName = data[0].split('/')[-1] - # Add git branch name to the Meshroom version name - __version__ = __version__ + "-" + branchName - -# Allow override from env variable -__version__ = os.environ.get("REZ_MESHROOM_VERSION", __version__) - +import sys import logging from enum import Enum +# sys.frozen is initialized by cx_Freeze and identifies a release package +isFrozen = getattr(sys, "frozen", False) +if not isFrozen: + # development mode: add git branch name (if any) to __version_name__ + scriptPath = os.path.dirname(os.path.abspath(__file__)) + headFilepath = os.path.join(scriptPath, "../.git/HEAD") + if os.path.exists(headFilepath): + with open(headFilepath, "r") as headFile: + data = headFile.readlines() + branchName = data[0].split('/')[-1] + __version_name__ += "-" + branchName + +# Allow override from env variable +__version_name__ = os.environ.get("REZ_MESHROOM_VERSION", __version_name__) + class Backend(Enum): STANDALONE = 1 @@ -55,9 +58,6 @@ def setupEnvironment(): COPYING.md # Meshroom COPYING file """ - import os - import sys - def addToEnvPath(var, val, index=-1): """ Add paths to the given environment variable. @@ -75,8 +75,6 @@ def addToEnvPath(var, val, index=-1): paths[index:index] = val os.environ[var] = os.pathsep.join(paths) - # sys.frozen is initialized by cx_Freeze - isFrozen = getattr(sys, "frozen", False) # setup root directory (override possible by setting "MESHROOM_INSTALL_DIR" environment variable) rootDir = os.path.dirname(sys.executable) if isFrozen else os.environ.get("MESHROOM_INSTALL_DIR", None) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 45819fc0a9..6ad13d3171 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -61,7 +61,7 @@ def __init__(self, args): self.setOrganizationName('AliceVision') self.setApplicationName('Meshroom') self.setAttribute(Qt.AA_EnableHighDpiScaling) - self.setApplicationVersion(meshroom.__version__) + self.setApplicationVersion(meshroom.__version_name__) font = self.font() font.setPointSize(9) From 81e372a6c0ccea1f9a49c0a9803be6d805cac13a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 17 Jun 2019 13:07:32 +0200 Subject: [PATCH 204/293] [packaging] avoid trailing characters in version_name --- meshroom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index bb76539cb4..8b6101a98b 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -15,7 +15,7 @@ if os.path.exists(headFilepath): with open(headFilepath, "r") as headFile: data = headFile.readlines() - branchName = data[0].split('/')[-1] + branchName = data[0].split('/')[-1].strip() __version_name__ += "-" + branchName # Allow override from env variable From 919f5e32aa404614ffbbfbee56a3adfbf5b353b2 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 17 Jun 2019 20:03:05 +0200 Subject: [PATCH 205/293] [nodes][aliceVision] Texturing: fix outputTextures files extension --- meshroom/nodes/aliceVision/Texturing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 7921af2ead..7256495cf8 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -182,7 +182,7 @@ class Texturing(desc.CommandLineNode): name='outputTextures', label='Output Textures', description='Folder for output mesh: OBJ, material and texture files.', - value=desc.Node.internalFolder + 'texture_*.png', + value=desc.Node.internalFolder + 'texture_*.{outputTextureFileTypeValue}', uid=[], group='', ), From d173e540ce7dcc75f03b3983551bd0853c51fd11 Mon Sep 17 00:00:00 2001 From: Clara Date: Wed, 19 Jun 2019 11:58:11 +0200 Subject: [PATCH 206/293] [PrepareDenseScene] option to correct images exposure value --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index d70232b8c7..0bb4976352 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -55,6 +55,14 @@ class PrepareDenseScene(desc.CommandLineNode): uid=[0], advanced=True ), + desc.BoolParam( + name='evCorrection', + label='Correct images exposure', + description='Apply a correction on images Exposure Value', + value=False, + uid=[0], + advanced=True + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', From 0cf69bacc6be93a8c573514bdf2120fb4af94925 Mon Sep 17 00:00:00 2001 From: Clara Date: Tue, 25 Jun 2019 12:22:42 +0200 Subject: [PATCH 207/293] [Texturing] option to choose process colorspace Colorspace for the texturing internal computation (does not impact the output file colorspace). Choose between sRGB (default), LAB and XYZ --- meshroom/nodes/aliceVision/Texturing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 4bc94fcc8c..2066135eb9 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -99,6 +99,16 @@ class Texturing(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.ChoiceParam( + name='processColorspace', + label='Process Colorspace', + description="Colorspace for the texturing internal computation (does not impact the output file colorspace).", + value='sRGB', + values=('sRGB', 'LAB', 'XYZ'), + exclusive=True, + uid=[0], + advanced=True, + ), desc.IntParam( name='multiBandDownscale', label='Multi Band Downscale', From 2b1f2a83346c8f7a3ff5df9182caf638768df3fa Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 7 Dec 2018 18:15:38 +0100 Subject: [PATCH 208/293] [ui] update MaterialIcons to 3.0 --- meshroom/ui/qml/AboutDialog.qml | 2 +- .../MaterialIcons/MaterialIcons-Regular.ttf | Bin 128180 -> 161588 bytes .../ui/qml/MaterialIcons/MaterialIcons.qml | 112 ++++++++++++++++-- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/meshroom/ui/qml/AboutDialog.qml b/meshroom/ui/qml/AboutDialog.qml index b9c4cde1a5..64ffdad35d 100644 --- a/meshroom/ui/qml/AboutDialog.qml +++ b/meshroom/ui/qml/AboutDialog.qml @@ -63,7 +63,7 @@ Dialog { spacing: 4 Layout.alignment: Qt.AlignHCenter MaterialToolButton { - text: MaterialIcons.public_ + text: MaterialIcons._public font.pointSize: 21 palette.buttonText: root.palette.link ToolTip.text: "AliceVision Website" diff --git a/meshroom/ui/qml/MaterialIcons/MaterialIcons-Regular.ttf b/meshroom/ui/qml/MaterialIcons/MaterialIcons-Regular.ttf index 7015564ad166a3e9d88c82f17829f0cc01ebe29a..84a44b855ba1e178812f4c5b47724c8ef23f13d2 100644 GIT binary patch delta 65337 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

7r9}GwS#TRRi#8d)8PxQK( zu>Od@8}+p^Qd=+@D589^M)U_sD*D~E4Tt^Hwuo0|^jIEFj0Ik$+yY2Xm6ON_q*QX0 zwGTajC%qL4!SMKa$5)qTqL#7v-)JEamc#F(++qG;0}M1Kk|g8eQXnmuKctpoy+4ev zNC``zG+w4LUntj}`;_;SEMT$x=jbohmawIOMT|e?{gjt!@Zmql{#|Xc;86<1E=zid z!e*pzBj1;LC@!BapdX3wPcH7@`O(opc;CM8wu?Jh{hEdOOT@&AT4lxj>C>ntEg!hz z^>^=^En_7@`Mk$Az5n%h?>$gP`y_zpkiV1zbU%&}BJriio!~jQP>%S||Kd3$W1(Ck zm&y!kA;gAMc)$;3&_L5@k!^_t9(*yZ4~shdv_zF`5zL~}3*@(;ejta!fgF|ErS(|=v_+eTJ zLiv)htrb5qYt$`TEVep+lCGy>C9FNn)cB9Z1+{E76RbL&7SG)@mu1yR)P78A5_33N7a*v~!7q~$IWpg~AoWjDbq-Aro zY9(Iu&R-qa!3zJ_rX5JE{k*=ce~qrJyIEh*U5D3A%;{RunoFb%j~P;Ou61Bufn=z} zKw+_5Jy`n%YI*GkQQn3}n|YvLAqWZ4IqL7EzMDTY0Lba-GHvkai27a;xm(keT~?l+ zTHGbtRdUQ}qIP1CN<=Py?7L%v>Qxu+Vtcgl1pemX4$}(&G zQDP1oMjJd%m(UD zb;Q7RwLt!Eo}5uGld%_r*S&~bg=QDe@OPQVyK>NjdoBo%`I)lHjPpIUNxyasY%zN{EW2T#@ z8N-8rixW)sRB>MLiRW>_x!vi3F+ceG!xSU z*y4Kg;7x&|>bnu#M4yxj9RwBnn zcWEgjSZFzk%rc!LT%wM_;Jj?tUnlL_gg&TvciwvGzP^)+yRb$GHGbq3dPmc`2!JS) zFH(YF02D#0k79K5wSpipvsn8G#zf-@Ic10AwV6TKp9L?T%~|+PmW7<<{(~0;WhF%! zXMwy13w}J4vmkro#e38};ir^!6(P_9LV!KTo`Zib4)BJWHlib*L|w{dsosq@8^?PL zQo0VAoh;k+rhZ1SwyV=@>??*p|v0#5`y zo?e5cvx|Os4}>rYnpucyvQL*>pO8UG=?3owd+ow?m1O~WvsW?i!sQy~KU%}0Uc{~> zVrLhFj{f6=r5-?C{w9q{?j+X8e_2T@lh;zv-W-?NubMbJYPzsA!fXU`97GY*5 zAYA4P1ym5{aXui0ry*)!ALi@lNoCXgeGdSZBm_OR1hNevHbSeB(+mRK#4&Z5jz%F5 zX%DnG{h^7$NBCn^3!UDhtUQ}%LIAZCCA-D>#aUmpIKi~^Jk>(k6IH>+b4~IU?jM+q z`pT3<(6Esy?>mZ|kbYqoIs)d!y+!nr3kl2MHL|k8ttwH6MI?~k?2;PRDp{#0pwx2X zR2OkU87jpf$Qp)r<7Jg3=4Dy0(dyKqCYBjY`n}9(0oqZib#j2+$5nITF`w#?YlU(SWU zD9=B~4r&QdF;v4zvmX&*MIxlx=N3K>R|t9 zdCiA>TUWG^Ux^OCwTnwE#rr}_c#;iG_CN+02GSaWhE|8`t|6oPr25IR)(Qd5gKnM|Od7|spTNFh(bo*snoh^$}LB4F$ zgtO!lhe)*U0+zyv@fMZ(7dQu`)Pj4UJd!EkiR_Se^2o$n`9wcIF_JHg=1BlXG9ye` zlosNqVOw`%cFB}h!~u8{f`Gd@i5(*u;2HwHr5td<5?dMrCX;~;^vi&niZ%}x>*tmq zJ(&$voz&`5tU!f*Lo{ySMe;K&i7t|BmI#@E1D&YLr8;sbEeRB7Gz!CUn9Z4ul)?g4IC2@}+Oanep$rvX{Cd}TlXU{Dq zHa}Y4SHY7h_LU(}7iob&6p>D7Vt$=j2|$=ZJfm^t~? zV}WD;vq*VA;U{riIeYstoCa=NpuiF)o(O>q;3=R|B0o$N)*sTNJf7Fbqb37r0!j%; zLLcM)Syu@=k@3lJrzsBAJzKUXfAK%&CQ_>p=$w@gUA%vI!hTb6{O+-YR zG-)CtAR;OvA}1gs(xnXoA_5}PL69P1L_}1;sED8lp+y3ST(8&b^&2K;&&PaN^4uBh(ei#gBA_U0VNMJv;y~6^YL#4f4W3o%wM9KWGD$YN*rxRwihKi`1fC zFb6h$syPs3{@o`j(NzT`sezy+;3~!*CMls#FNMvH zA1MZpsGAT+YdKIE%?00pgBocNhp|g6L2Sw%Q8T97@NO|RM`R~HIOf6VXfr1KmGICJ zF|JU@-8d32#&QpiQ`r*I(h`s>B_re39jEOv;nAUw#<-#{Ma0EL6qtEHQ~iktnj~l; z^D3-ZaJ8*>LDwCjxf^nI`F9O0;Q$+%>)F31LI3Bxlz2hF6WnmwaOVDJi~$^u$`#VO#R4d>pr+ zW zROq-Z59UOCH0M@vr--xq0;9ImBYwH+Li)5Lm30^K- zJXmQF32{CglAz6?$N8ARg*`{+7cAzsLr{1U6S5Z;6snB_z<%uMRs;YJESR8c6y_!r z)nk=q$7V( zq{Hkpc~K+`p`4kzWlppW7Vql%`vg@v3PFNJ;-niV2HlgJpBrBYv5b};&w8}0CuFYt zk%jm`t~LPObB}U$>t1V%nD*A8*Lvf0P;_+q0}0|m^)b6H6@IVsQ#|d38=u~(*M7CS zL1Tkcle=`Vsu_oFVI#IuQ|mz>;S|ZBBuC{EA11&M!J`5q6*%A*Uv7X%>P>LQdvX%6 z`+5UD5YTf&;YdAkVSvdU2|2df%mlBQ9zT6%Xy{CKb0l2my>dp-u@;!bP$Pm4f88^h zSJ9kc{{qylez2dxduO1#!F%D?{T`;&w_~dJe~)WOxRrTTcoaNbidMa%prRjON|w*0 zaL9^jG$+uo3QQ%4;04hRwg&Z}u7HwYhE{JCq%Q(A3aWGxcJ#u+iG9iIRUi_WhO;Wy zum1+}Hg!;%vG}8IcMYB-QiLBo)(A2KN=tx0+=;L^;Y1?x7t~S}G?YioB*3l&oA-l* z?z$^a#N8Q{>`O{$o|Dr&zC&E|bd^};&dB8S#Ii*3WZMC4t3)QI#3$r5&xvmt+aUe+ zgk9JDVu5h9!wIQ9<+RB-_Y6`H7(M>O%mq6aDWm*&vS{hah))>Uq(ftGRD3lXE`@6G zF{usbH{`8Mo+5BILbmhHR&|myoY@KPa5wI<67%v>({U^NOc80;9Y$?AVT#DbC*tH< zykN#JmPe-uubtQeU%kIRMQp(lS-W0;LDaS&!X?4V$1h;?jTqdz_239MKDU$@8*+44 z$u!YHB$o?P7${6W5E|iX1PuUZ1GSzms`wY3U6_LfgjUUzyKep1c1(Kun6?8u$0Wta zcOED*GU`>$$f#OlV9H&Ow5eCGO^<2GPKV`Ao)*-0i$oEa4UP*EP)h5?fNHtuWYw$; zdpR44+?_i|U@1jHZc)n1S95zMi?E&XxBiv8b7W3@QSyv|xvhTfnF30f53*7Svceq_ zMjVQ0L}Cc*Hm_RvAzaV+?r6?PI{kXa^A`soO`R^zIbiFzH!T<-k{3*l>g!K3 zTbnb?AIyJQUaOAP#u{$zvHr4a+kNas_Sg0wjyjH!jrtW*)FS-}F4~8U$v=4bCWNyg1kli7_hgJ*i8agXI(}Rr*!g7@rZ}A%1N9%=kkI z?u0ftTxLna_JnT|ey^HYbz0Rg5~C9zNSvRzGx6KRKax_CIwy@w+LUzM6XxmenVlS$ z?9Wf0ko;zHQA$)wW=iLjnJIfxeof6y?U%YZ^>A89+TCf3(+bm$r~Ts{wP z?!E3y^^Nwe^L^qgO*hjM;B=jtz9ao|Mnp#4j4m1dGR9^s%vhIkD5JDmm1-|lTT*Rn zwPV$;WF}_bllgq+w#=*5J=Hr@A6k7~^%K=^W;O9=4b7UBb)?4QH5Szq+|ymiNqIbk_1a~{oEol}%+@gOO9B;U{d31_qpulZ`t%e5NS z8eVHot!1^g)%vw|v)X-XudMy!ogMF-d*_}zf6KG-Qu12mb_gvm*c|X;uTBm!R zSL&Rr8(p_W-4S(Pt?S=j_e#CYdUw~`Qa`7Dm-^cqxEqXV@J2&t!{!ZFHwtM~yHSru z6B@nI=t!fV8{g5mOXE?E7d8H%@$tt0ZPKvG*e0(x+1=#pCO4YKHm%>ZN7Io_Wz&zF zUTT)!?EYpiHha6-!DeTh-D+N?c~e*^Wt5vPuYxPrpWPX$Uhw^9T7qt#;UBC4`tp~UEw_elw+csvK z=4~doS=;7|HhqdF-Ca_bl&RxAVvMuI?hctm|^1%jx^V@2hv;1NY6iZ})v? zyH@Youj|sTU*4a5f1mqT-T!qrU$>6kp6#}}TXA>My?Xaf-A8qw-F;j4i#?n@>h|c- zV|lqe{od=J+J9pI?;gy7uDI#JZyx+}z|#X>9q`pa z*Fev}2L^5&xPRck2c-;ZJLu^_TLyhS*fqGp;PHcBAG~|;H$%dPqztJwWWtaYL*5&5 z{UPr|-5whE&>IhZJJdb&p`ou0^?&-X^>B-a$36V|!$rdqhP55`e4`#4wPn;l9?N;G z&trv;{bO{K(E~?M9lh=Gh{t<8E*~!)lRf5vF^k81Hs;zB(NENVV&D^#pZIjFHP$!w z?y-BuUVbwE$&vmi-+l7;r_!Hl_tdPXHavCl>CC6QJpI_yi=O`Q>5_52aqY%EHSUdZ z-#!!nO#f%*KC|J6ABg=Y%4faaJt}^$*#$1lN(HKJ-PGb2Pco7 zym|8FDX~+UO?hO>`YC5$NPVIE3zJ`1{X+57)TyJVuAlnLi=AGa`Qm3U{yNPwt?jfC z(_Wo+;idGK?tN*-OGl?Wr{_$cJAME3KVJ5{-1+5aUta(6Z!_xqXFNV*^^7lP{AXs; z%)T?H&Mcf2GAn}j(1z#?-7T&Y)g@uO~ zg)C~fX#AoLi@sd+uf?&8n=Zb8@z}+$N{94k$(1BPz5c@MZ@yl-GHT^rD<`h}?u}}1G=1a#H(q>W&l{zy zJgaK2>b`3Ds#U8BS6%k6j$hqq_5G`#T>Zlu_nNwEMy+{!P3fDtZ{GLj^KY(y^P9Ei z+U9GAt$lm#g|`O0HS?_x-umadn(I2PdurV)>-Md?`gZKwE#CIOUHta%>+e{9|N8Ok zS8s4^n7m>8hLVkm8~bjYw(-EGj7?KEZQN{cZn?Q&^PbH&w=~`|WXsYmUv7=tdf(Qu zTNiHi@7{W1TiCYDZ7sG9-ZpRB&h4Jw|5HOIkLmG!@DDIN5>rl zcTC#x+K!_;e%xv7^z3Z2bI8tFJKxz^wDZEdHQw$1?z&weyBh6!X4k4+-|o7yyV~vn zyC?15y}NWz=AJHl#_d_T=Yu^z?~U18ckhtBYu|hFJ^9}8_x{{>@4ngoeJA(*@_xvzxF};2ctjO^uh0i^$WWc&MN%m!@3_n`{9lcuO3J{(Eh+< z2j(Bxa$w(qPY+!9$oWyHkH&p8_oFo*9XptKu<5~m2mJ@v9Q^oT$;Vwk9{jQV_|rqN zhjI^fI`r6~_YNKZB;=F4PkMYZ?vvG@{Cv2_;kP~w{nS6=(~m!^{@I+*Nv3%Hu}fdH{-WTEBSqDVMimtn{q<$%FW>y~+E=Z=TJ_bbuhYJM z{OeuEBaT0Q{M~QDz8Uq+%x^wD5qhHWiKkAif-?Baw<+Jg^zHlK{^MlllXFk*IC<(+ z?5Vb=MxI)JYX5hGzkBw(t>0bz-ur!B|M&NPKj!<@-yi<|uhR`r_c^`d^r_Q-{m|=& zr9T`$6MLrBnek`dIa~c~&$BO_U4QmWadL6H;%AGG7N0+tdamEODd&pMXP$rP{Ot2Z z7ve6|ztHo-`U|HnHoG|RV!_4r7r(k>T}r#u`O;IDc9)2f<|WUU9Q-ll$7Vl1^W*22 z@4h_u@`snNl*X3cGr4rGEI5giZ+k9iXg-JE?Phfc1T`af!3ncxb)43PKRC_t z4~jZsBM~w!AKP1@x}c1a3z}Np<3{3+sdzK+J95Ev>#>l09FW!4@FCZ)mOqM5p`P+udmAfF%nS@q#ay}Hta!0Dxzyzej1?!znT!r`q zn_)b^;JmZ7$X$@>%5^oxQ?zA47gvXOUU5wmem0q}8pgv2SrD6{8_P~@V0KiGn z?THJGM9N6S@=>X7Jm18TgIHY65*itIT1b@mvft(-_lbF{b;$_No%n zRKOb+#f-VI05GO9Ea(WUf*p2eqjPy^A&!j-Ax)+pQ&`7#lP;?XF>wmRbe z80vBwU0@g=XiO-!i<64ei!+OB7S}0mQrx}xsp5Ub9~2)fzH#o^bBoR`KljnO!{>_5 zeShx!dGq|J^9Rp=dj7}-_l3j@O)nH*xPCG2V(p9hml|I3KY!_5iLd1Dl8;M%C^=hl z?#I&0;&SBWs+XHyzW?%*m*2g7tkfuVmPVD{Robrf`BH!Bl+u?=XO_+_onN}JbaCmD z(q*Num98#*vvghQ`qE9M+e>$p?kau1^rO;`OTQ>RUV5tZhthMU7fVY@OG|$${kim? zrN5Q_r}Sp&tt;l0=&PNt-nSq-w7z-n)`I?_ZT+aBgQ{VAr5cu^hBwM;*oqq7E#0eX zI9&Q==?T5Uz9J>7vT%{ zg?Yd97I}|)k9fy>hj{yY{k^>1yj{E|O`O;J5`NDI| zbHwwB=aATf`&j+(e2FRXi1IFQ1yFY*`v9$By^ zEEkqLgQovttbF1{J9}|5u5`q)k)Rm9_hKSi1BA>6p9LnJQOsv`5O0pK9xixafWbyF zjKam;5$}KyQ@Hpk#0MeVf@>e*1qk=!`T+4cupAbDgqwr7nTPNYt{aHYH;i*Qy6D`q zh`)~z&(1AE{1=4F^=15ya5XNxZ~O=0VSNdx-{+3vDgre|N6wwZ^*!QnOPnjlbsq6* z2yYq2c@vg1fh7&+d5yvY(Ax88<#{wnj75kBo&OZ^=MWyzm%tRf;KJobdI3UAxeHty zKLsHwzt9x%R}j923l$1b{R^n{!ga)dL--HFxCk?=X(5b9VHW{P6AieShbtd(T$eBm zml`5|KSF@^(({P-Lx|p9I*0g5!zhWwatH|DT5OW2wGOfaFKC^0L5#zYvDuLM0YjdpQOdtPmEi%k^=gmlh15m+!@e zMq1SoV%#r3iFgjer*XZD_&o@B<2r`;Y=jsVRA*Rg5SkeOQYUV{!41G!8in|G2vJ$- zU5Ep0rN9GTc058LRVg50Cm;mmc-d$`DL~K5?t*YSu9p$N58+IG*=S(tTz%Qy5YE?^ z-5uj!x)3+G>>dag>&xzmaEZR`UI>@sT82CiAbd?&phx zmLA6S1=1fw_$996hy(nkCvcrY{8@xQ=*xZ{VKFX@y*&w`{~~^H*#!to^kq**h(Y3I z6SUWG{e<`n2!Ga>{UX9&aQzeM(-8g(*Kddu{6IWj_6&qTHeNP|0{F$to@E$U3|uB0 zNcL<*LU2Xn!92sb+6-4`!~y=R7|*L1M|-^y{nwxBGVF~;jA$!|zAWn1=wZw-!1`=1 zPE0Wd1>z1^;LZf%P6!3R%l$8lWYJiD_p;cQTL&ju)xx<5wXkP84_@QOIIHJjW32I{ z@u=~*G1~AUt+sm7NbZ;+a^<-hA}-h*R-*i!-Ke=9pd0aS@B-Zlz7HJ}UcSz26%wZ=BVvO7o(2$2x88QsutzzAO7p z9i08dq-Pe7Z$5E~f0lnQd?!39qM?zmzI!vk7>>40G^T+(F0pGGF03AjfO$U&D*}CX z4*DIA-lp1j&;=8wqS;8K5i-@Tfmn!&Wg+HPvFeDqR4fxQr;1fWj6h4ZGZ3>?EFGbx zVm^d8c*D*`mlIJ+EUGSVV1K)wZj`^QRdw*Ht|AXHUB#V<=_+a?rmLuhP*+hu5Y|;y zIM62^z|h=i3#vj-Pz2o5DS&PyE*|idY&X0O5{-D?EP7z_?933uvKy$PEVMTffNFeO z_9nVQd#j<< zI^344V<1<1m1}^p-}poL>x)rhjrh)tHrtqE%oXNm=C4+o)!rIyEw=Vs7wu3x&+cK5 zx0l<6_C<%&k>zOb80MG;GwmTqiPP!KbhdF0f|+)y^Ihjx&g-sFSC*@dtG{cUYo2Sp z>!9n5>vwmoJICF|-Pb+VJ;S}qz1LmjE^*%si4DmPY1uraN63hfNg?w?)`sj2ITmt0 z&L?`*gy&dZgv5&wRchz^Uz0=;x#MWlI&sST`RBT*Of zkJ%!@T5TjOo+naejX5I1wz7=`IcT0pi3K8M0*R9F7mL?_GvcLh9xC?F5m7bmu2^g6 z!(Rd};u?KZ5%`USZ#MzY%COIBBInH!*|za-BTg6a=Nu8%0~{yre-sddm%XSj8pSE( zHM<~Rj6&;Fi2FqB6ja1^r6ZqLRf(9@7{wRPg+F5CToJ}o_+kw1Eyb&{=v(2vFH7Ix zU0njp@*ph&t%z22#$u{CkP?kQ9{9}8asMRqaK*D%!NY6BnEO#^?N>x%IG(VTKJzS6 zI4oh@V1~Y1jgU~*9Mj5`d2>ZZw)H0(RJr^FypVu4WudHO)RhTHM-x!V)#Fg@cGZMR zT}4JNPvdHyB>Pp_a-K-D?O1$xyWl-hh5lp(9I;cLnJdyvtD)Sl!Wwb_a+=1!Wg0_s zmAr*?+iC!s%$B+mP=AcMTNa@hxpkfh@xG6uy!hje!w57a5^X0*ReiQ{w##Uig%9#X z{GVX@rx793=HqiU0Q*i^@QO%Et_dQTjz6|99Fr8%J-)C`W-~p<|t6U!vofquB8aJ~W6W z56(Pib7x0q59a{mKk~*xkt=S=ltpky3q!VDBx;Cpa?~PGUHm2IFA|xyut9?7NpOTO z7Kw0CN1k3JGEA%G;>E&;9de+Te0Wc!Mhtto0s?71(SwM=sio#SM^iLb-~{#O9i9=? zL={e3Aet}(1&-u6R59XBe%_fhkjyg$f9Z-eu{u48ZsIxVo~G@jcpRZbTqL*2R!dc3 z3Q|bxy$JP)2AUu&&~5g>MY@$KYD=Gl;J!;XN6_eZWUgFz*#r@FLA^K;f<0l6*=Mi# zP)08iRU3thyxX#n#;_W%xbtsMIPcItQK(+FLc|9;;uiZENfPhtd#Bi^BM$MNiZrsJ zXosWMQTSsi1bAR(ivxJVX_AQaWIW-diUeF^?PYQ!N=v%<)YXwQfr#z>}<2 zNXhP)(WcRaM2?^FeWlW$9LP6=RxRq!1u*N}Vf7d#0{bEOBQAKCall3yi=Dr_qOs5dpAK=2aZPuv zbnSK>b6s@(ft7+;?&j{U?xF7S?pf}Y?j7#K?$hq;Ax#Vj@tHWxxMN7)kWrB0=Y^~c z*@{m?9}77has}%#oT0Ih<#R$CLYnUq+BNX?WSRG=NVKh6Misg8b&(*`=8726S}s{8?hsq$fn_2)$6TRgQD|=fZ;(n6(3DlR zAf==iL&|H*L=Edxh{ao%Lu|iuxd_WKcH(Uc>!fVtXyiqtcACzSVyL&xA7%69A}brF z9p;0Ci%<(2k$5MND~K-^EDXy*ct4YymxII#znp~Hdqm6 zl>(8(Dw2m$WQ%8zNn?6YwcsoWg2<0&y49p+1UW0y0z#y5r|gP~{5I#x?T}*26^yLU z@Kz8q#2AQUsuaKq`@Xck&khA3f&NlANH>-ttun;41PWp=z9@T!Qgp-kI%j;kF-?B9 zLU>H`ZP}8cI3+i)0B`w3_Qh>i*!ajmG}B;DHEN_A)x;q@DeoQCCiWUdil^|T9F)%O3PWSD%v&iEOmTSe zN|5nSK*lS!hNuoWS{>v(<}y3TqSr;$!JjK?Pf}Gu?=*bkd!)1Z1g#IAstmpi3VK8p zs}WU8!juJ)j2Bh9mfbw)AZI5)BgV@?sLo>?QuPqNdbSZ2zfe=Wf z#B5D6TJbSbD|Uitl5R{@slmE9O@Q>`X{43|nfOY+{!)d!=PYwCNFYvi$G%ATuw?AH{Z1grIP7G zu~uvKi~sF&!M2p5^keCK_;4AUFnPTRfL(OUC zIvClmSfN&q)yW!R&9K&6hpiIZwlnN}dw@OBZ!d)&aNNG`a65e9+Fc#Pp#>~Ug;JAr7&gIVa&fU%f&Lhs_&eL{3ARiHmbCHCH0tRD*7|l|I zDaj$huJy54I3oBJEd35t-SU@cK+pwMexVOuAiGRNA$pdO){~iPR!pSyp)xJc#@PK8 z1uSnAQ8g%blXr2lVpiC_@%f#rT z%>#1M8j&t~%asgzF{m05r``who86)D;JvCL3B&`lufC11qpgOC;zh;4HEkrYSg$4) zs()gtdJbU9yh2gc>J=!i1N)zDE>TbQv`NRPaU3hWZM>%-r5OPggxf%G_LOJPHW#_A z7vD%Od=sXs9!yIW)77I83ZBX4M^K~CUr$T;%-Aj}8QO%!68X9kA*{a;BN?PR_cu(k zZc5Kuk?P;7GL?h7l17Iw3(|5Id`dZAYlQ^NMNry`#rr^ODR8ISMu7YMz0%SELm^6& zMS0|;wZd7~x?Cw;n<>a1UO;+f>?Qc3#SnRHE%>3;Nv=TX7aPGa%G*gviaOdXq$VIg zLBN3|?xk8kD(0d=Lf|RsOE?0lw0aSRmF8x4QzdX9%jF}Vc@X>{I0*?KKd{pL5Q&^h zG+e}?2(TZl(jYUbG)LfKWSSQFNVZU!b+01IUk4M&Ia#s}bJrn?8A%r(OZVHt3kN~= z+afK)T#9idS`%N0*rX#=95m1K!E4YF*}cVhSRFH#G$J_j2v5!o8+8nS8&a}4Ll(X* z8g>yEk$QV>f(GMZZUoBlnU=J*stW`#H{6y^qyh=G?vv5$AuGWZtl!hpkVbu42LCPd z@!W5n04kBPkgF3F6<0Lps#M-9%7(5%0y00Tl$YiepsRVPQYt~@;9c8F;KNV8p>r zWh1VwuwJbEpf^wca}RP#k~_E&)t;ggM{bYn}(o z%>%AXv{r%PUW1NY-|lTsgw?IkE{2BebF_5ycT8|BalGp|4pUnsxNLjq$3frLHSB2d zxth9qxJE-eUgg^7I_`Sln%j1#xa+$+Lph%4Ug%!$E_9!8Ukfp@Gof}!8*KV6_ih~t zITLaNhg($%%?fQC+9|Yu=;+Xb(D|WjLU)872rUXd6M7}|w=g3tRNO0XZh#xBGlN_? za3i?UC~$|gO(Khu9Ib6h;x0LAqo^g?Fi4fVHbUBHFH<&Q10u(eVydRlG2$Ni%SI^9 z9r*MPn1HLu0h>iqRM7HCNj6KglVdhPO>P1;nNQ`W>#!C6Llz=YjFOib3LlSF1vw!elMOe6 zOcQNU7A0O{AtxLuh@KA7VuC!jRd~|GB$cOJz9FI3Es3%E$iCa4K>ky<+{UaA-Bz5= z$_Dt1v+~MTk(~Oos-?WwbhUBbkihqr8@Iv0*HSUY@C^bFRF9cs7#&Nm#-=%WT^5HPM_UY0UHN|GWURvQkf@m5gv-w z8$)E*cc9Z6gYmQi9FPOc7{ssYo$|qrNBLgyAEdII#0DZanNo_m38{KsSM<|LRf4Ad zW!_sX|De>M%|@K0e%cZqLSp46xQ(kSqaH%qwxNxR`;bO?t%7V~JYA{4R0WDhE2WVN zWQsqKR^D_)>qV*I_Tz=}Ub@7OU!qCxWj5Fa2mUh?cmF+p?yZ{cu-~+H=9s_$nMw;&;T^FW{ZH2~1(GRO53M172C( z5L2@h>9w)sX9zq{^WcNp3oq1p<5wK-lOVE0L(v`v<004<95Mk~=zOtUgv#|hg~wyB z0upn+!~wyy`<93sYyB?I?4;xy3ACa#M>`q0IOWkatG`O6#70w4rfP1Y2*H@sJ*M@> z>Kq^o2N&S|U{2aQVz6LM%eN#o6fub}e*uDoIMXz?{D~Alt0q5Jat+pd(XkiIlx+1? zQYvwt4NNtTs(TKVlF`IVxYv5FmU6>zuN$a^kA8rzt|1mHKxiY-x>F9Iy)_ z><~F=7j(A)a``Ti?5Fof6UT@OvKc%;)|YrfVVy(8fhg1Vji1!u)AUS@j)S3ig8h(6 z*Y<$`Yvy2zZAd5cq=lF=MR2HIHp2Y3J)n9XZCnGM=%o_60+P=vSh-5yyVjT}Z?m^m z#86;`?m*d8u;(F-S_f%$0L!rZfifiDralNxZVkk+Vq$aL3BW9SH4@kp_6v%dn2!63 z?QRgbC$Leq=?L7jV%`hf>naqUm?q|D@IKk>4+okireL z+jsuM0eS8FPF`1lxcf` zhWE<>dm%reCsbyF7I)GKtqq_h)+1PxM2;$h$ZKsoty~AcmpPa z^&_Se@j%7aP>fYF>AmTe9p8h@IvcWb;d>CTXTB#w#Voo1J>ix2p26|5VkU2Dh`F-l zJyG471L2k#9UzTjp3K_^z-^U1_F>x2m*e&cZ?lEy92VlJT9qh-26N(ZzQkhXHz8N~ zDJd_;Tr7}9`vAKcm;jZs*lU2VWLDbtnQS}|Fgi9DL$L;7GmMVEnI0h25Ob=z);t_A zI(EeRgQ?Jg3avBNA9jLW*X{yO;WT@dz2E-M{?!rb$i~`({*G~udGHk$!sd9x>2~^h zI2*&{I2`KGLYNy1ohO{vU@fQuhhcM9SJzP2c-Jhr40pH=yS{T>bKQc|(C5x`=fiC{ z$UVkA368_1?sae-7P^l>Ln?M(asO&QEH}O{YS*=^0>5c1?l_h3Qvl03A0m^dM!HZlEHt>0dnhy&@jx$RSwz@*>7!7CTI7Fhvlsgv4CMg zS%#pmFu-7}$K^z37+;owC2%ykfQJ-&q?~sE20Np<9E7m03GEprQ_a)bOu&3?j!~Q~ z*ak)2kiECKhq6Z7eK%tzyfP&8K7@Uh6lk?6Gc_<|~QjbBg^1 zYomf#-AE9W)V0r;Vm2WG3C^UasvIk{_LXH_kv9&Ac>m~15BUPGORxnLFJm7)Q9(dxi$atcTd6>$SeX?6 zv{kFje~g_tWa^~AG@5Eg_^`?soaCCrKC8+LHkyVAh}MW}l1=4G(H3te?S)dJ@50M-v$ax5T_M-r&revgiR9iR(6j6yH#q!7@&^6Ip(Hu5X zD@bJvOU+I)fbz!U1JBpRSnD1pK*1=`WF%O_YjH3@`+)SQHv z{SQzVbE}%!Yzn81CZ#pR14t#xXwis^yI>$Pt0FbX3%Fofn|1C`B?JZmrD#(3B~o>l zRaXN-yQRf+Eg?mtSYLf@Y}Z(6;4IHWsTyjS&P9UvGlhO3tQx18Qswq$uJE&?z&KMJ<4!q`pYX5M!I_ zEi!~a9(*Z*ViC*Hy5ZcR zi9&3hixhQ55AhU?*4sry21EdIsAv+?EGE;s z|A)Y2xn=E7MYd^$FidSkI?o}+(3w(AQ_55nmmvZ$Rozv>MGHV-`N<&x1DfTMlRgz0 z_?80}Dixqqe=;^$aq5wk{ZDi^xpJxEJ_zD982||d0rWYZa+(Bmys}nstz!)KGErb+ zOF50S)tBR_T5R5szR$QMCsxiw*xaO@v~pQmECv-c=@z}c9UMwHI4peHNUb(A%_O<; zGm+e&m72ZOj)~hejgd-FUWryuDmFK;3`9i9o1f8E8mA-zzRM>u3bE7SM)Ru)NUW-` zAq18h&J224v{D-j!ai0ZIJ?LubG7kIg}$7=8qiCCuLL^9K#!TIa9LYE8pMhx6q@pN zA522z2Z#%PYb6ddA_UdfH@acJ+6-e2IQ((rvh}2V_jBR(m!dYUVp7b|_TX|dU4r^5 zP&kD!dV@z5>?I`4V}zScES?L6_`KQtpvCVhgq|% z?bb=_mYr#LvPawV?RTN_+;U_%{B5zTX}V**<1@zB9i79RQ=O}z=$vuhba`Bj zUAlQX3Y(&_&u!67|_T%U? z2Z6JMhOA0^CIDVq%6>j6z-#EW#TcB8+MmMOhqGP^jwiv(;lr-{{OV0ja+;TYM7ZW_an@)$6#RD zIv?7vXP#b#PqslC99R=#w3nmRdtO0??mpN4Zdaii~7JJ0y%b zCaTJNzYt;Bu%B>ZYN~d-`=Okp&XPC2KpCb_Rw)u0S@x4aT?(d}u23}p+lp&rscekA zrZqtx`2rG2n(SYMpq(rzf;RXZsNcS$+%EJiVxD7&nQ4d(C=y97s^_uBTQd7g`mM%G zH$(HN?D!?bmGyEsf_A^7PV#m=RRYqFqLZLh_EUJO)d&)W+aP9 zSzY%38eXttm9ntO)MqGkCt9cEHB^yikFAu46eKvhF~$g)k763FsF0-`21-mYX)i4w zn2Id3NN)ZbZniHfWze#l;#7j`GRt43sXlOLX3Cp*HMs*a(4NnA`Q>U1srx-L?>LM* zn*xl3!>9S3vQ(MXkj}ES9hLp1D(1oIEZRjxFcM=3u1s?xQY#{Wrl?^7=}!Zlf(`^Z z>-j)lZZ`7B<;R8FZ$;hqzGguG0p+DYUC~^}y+u^}b$hjt_!`oJPzV6miUB)QzDm5U z2H32j7#v;Lq-_)=xyYU)iiKyiwgSD)R3ud*fYxt;6m7Ex53$Cn6pD^YS_6xC2JC<^ zrZ@tD!J3SFavPoEHB;1ca&+2mV+H*PT+pM(B;Id7rZSLP=}8f;L_=!Cc*%-XcLBPF z(+!+uFGm}%g6{_UZoPnbU=EvC6+c#RGBW~sIF_vu4^S^FRK=?pt-!R+$+#h=&;xs2 z$&~3(0%H{`hw^W~sbcIEH#0SdDS0Ybd7*LASSE*`5V_WJIPgcD5UC+#8`Q;~ko!)6 z{JbbnGnguGo`Bnc`|$!B|2Y1LYpYU6R3A)lgfEV*fV=ZI?62(v*>tJcFV16sdY0K9 z-ixW`DzgyQ>EEp?SXR-=8fs0#_JhsVA($0^#kOG|mREFyx;O^P;u3qkz0W>opRuol z+eSK45*;~M+1MUC5{6(;!bEW1`HtllnjmkU6iMEIQjCG34og|^8$eeJ=} zf*C8voCMv!As3#c*bCzZ%Sx^Rsl^`8@d4O^iz5S*N};qW)qFvIcalsXUH-w)%$AK$ zfu#OJdQORy-qv&s4xR060LdwhXH;T|oF2$!lx53{udqtoo}ivl5+aui%&I_mkDPak zl<;ZX>%q@NsA&XCMqOsEEItK`yuDU#JO#q@2PjizSOknh_A@|{ij4@YqBe6BsUq>! zQYs6^qAzm_(t~LfO#-vhcn#@lI-yZyyd(#hq2?SC8~9I93}*tV0j2!QiUyTKM(c3* z22v~a8b?%cC@aH)W+tX-avOnJYGIO8PU8szSZR%X*w;t;KyYz(jZ=wq$OZNN7?a)v z?q}rmrs0Ur&uySRR49o##HAQ}B2oxP(iUP-HDj1cDQ}3|+>SX*SP?qFFl!n=YDyfZ z#SxoHtgloC;sjM~ul2IJFGE!87xacySA+9wMUMhlB3@U|D5Uevm((+s!XfOe?t*aY z61XFT{8^wJ)PI8jKLG-u!IuTUGKmz4VTv^)KzBUGSBfeD8*m&6_dli^(?zTpAy$dw zaB()qYOCeu5vHW8LrK)uUx;mJ?@t7A?|5dPj$?F1G|jsVU`Aos6V3Z=*Z~A=#1!^(G8>X zqdP`-70E`f^cBNf^!{nlz_dK_*VCe*iFH*qe-L%xeVF%yxFdu#7vp6u;%%O3CCdYN zYVCp;pM6GDm&s?~b$VB3oq_y5SN1w1YTL#hqpG}qPGoW&kTyEL0kYY-7)Aei1_SZB zj6N%B+SWoIU+{xSjndPK91#MM@X3~ZEFfK}_B~1oKcGz7HrhjqLGv`xom{NkD@|)` zMM}iwE}RUcE1c}gzJNShCZ`-zA-7dkjyel{V~#v@7OtC>@|UwB#}po!R*c(KvQx3h zv8^RITY-(1d(Mh56T9!nA=xxvlt+qb&R8!yox?&lwEP^p8+A@ZnOLojn`~R8g4Pq@ zuvh3+vPtsVIgw?HG%!T2-n)cWMs{k%T>OdKt7j(s`Zv^dR}Cj);w8- zq4A__d;#oWgY18SPA0D$be>AW0=Z6wiSh_TV~d=69@$=%H*lL}PevuAZ$Un$tyUT~ zdR17u^N8mseDT4Do$iAV@>g`17f7L3J*1j zBFiU;h?*p4KJn(?p7FcatR~eMD92oaA;?}QXE8J$mp4^-zbr=RGw;Q_T)3@dK2@$> z;s>g2y#eD-O$Z20zD1@iR7k7g?ON9>NYf&trn@Bjq}lYv+$mF3Wh-RY5|M13LU{mx zVq=>xX{jRyb+gL^3}<1Cc@jJ~SXm{;Z|2ESCD{0AY&OE>yz}_5Po`Xn1iyF;uMmdi zogiZYc&tpYS7I0eM&n9=ci3-%W0E%zm|DOAFA%F$DsyA;f|}#dL(O?erQkzuLDofS zKhxwsUM&t)3;KcB{_l`nW^zE$igSuYIp#-rtXO^!LxZ}l4+pF8;{pm|0ie(-_YG2E z4pptw(h3Xq4xg89t#+oSsh-Mk?;8Mf5|OHa@YD%1vcqogF{- zqDO=C8!N2LsYug;5imv-8Y@;oIH)u;tk;pk#_`08uMu9 z>V`W``V?@TSgQ)|*ni$9S!*DSXvq4^+A6L#x!VPZvvT6j3x3>?XdwZ|Us5GwMX}bK z>IF`YSTP!Lz2qv^S^-%`1HC(9gh8s6p<;N?Jb{>=r{5!{=B{xHF%9t3h_S?U`)$ZU zzy$NSK2FMq)Dh45Gn;d2{dO~IiM z+pW*6^H`P zIHs>0rz0IDjvLt9jgKfhW6g!K#}!f6Z%3$+*LG%7O@cAta#CnR2MqB%u&>vqfhuU1 z{(t2M^|iy5_H}#NM8_b5!V)b-3G8VolxRh-0I4izGwnE9!qYu)N3Ig$gdrg>cO}3pkNL7&PQN02ZS0p6vIUz*&T?DREH3-|Umef_ z!#2783K=K0puabb5J8#ZEqV9~7K3a;CR(So^O1~!1HcuxWJ~YK=2v0#+KINp@2%bc<5}=jrd05g@~-I4s&1Iy$$3}7af~0pSAyNp z{u&w-(#0c{(gSlGPOTY8ryqx|45E+jk=La9M{1?P$(VM#jX-+PJ{Rm9CX=;C<;T$q z6efaQLIbTWir5FoCy1-lkxGBN#^At+337m7;T&}~J=&}XLQJB|rcuZR*sXVwrd3a$ zN~JiCB@%uN7LU~8q)_%avyP(Tbmnpg0kPC)T|^p%5K<}PFBYTPFRQeQ4N5kfAT3x) z0G4JWr0K?xb8FdjGe|dK2fd7lz*PcURZ_6mgu_3P66^)x6KnFD(VP|>K*bi!`ek`o zIMiK}Yah$2bDDJ(4d=X7Yi40XB2DK#w$B+pzX|13HG%C$H6|C;GETmX8EyUs>P)p* zwOr^8y4rq`ZoLB%8`!fi9z~pu0)CqfRh$FhHRmH9EPDZ($hgOBiWp@_f2_4b(O0lc z@wSR(0m)tjr3H_&wj+eQ(%Pu*qr`d;+-m4^EDmmLWAw%0htrIO#tM5TkVy+l)MGhx zYITCK3+bc@WJ`KAy;?1A?ojC%B3bg2i1NP^yhZEzx)ug}#yN}~!L3(RYT5i(z#|p8 zG^gi=BqDH%sD>u`P)#T*kkMD%haZ4eNvFU@gWWabDaq6TuZaVWHjx_kVsa{bSsP#@q$B`hM;(gdf2B4^eCIp=5LX>9GoGZv&a%p*oX2316^vIs5( z;rmn-6hfKJ<*lDVhO9yiL3s^9>rpZ`STmh+8%YkkqE;4J2P@>)RyieoTkls$*HR9L zCla`#7s7Lfkw()L%DLA^r9T!hR^dUAs`BuHV1sCxq14QV_fv$ZJiqFOt@nyTnnnqn ztfUfNgA2sU8?T|8WbKzXZeXj3byz(m!_wR)EJxXR0_RG0ZY7j1}UiuJlHD1bqI;r%s`=%5Ti_)g|@cE zTBbQT2kV3YYEr+)sYf>HW=$S*cfO*IuIGZ2|*r{_-G>fOk zpp`U^tXjQj9g(+w5gxzUK9Gl-lk`B8Rd4)^XSzU+K8c1l(xNK63MQw9R%HmqTHIsB zPHVo$$*0(r;2K2IajnBBb|S#Zss6Bs)Rri!dyS`%hJzYW8P`^jUZk5Xk;W9RLgIkL zU@KB4(t^DYINam|S2$$-9;3@En delta 32477 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 Z8G^oeM8U&zmZFC$V4fI;e!cgl{{o!+SZ@FT diff --git a/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml b/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml index e311b9d345..8e853584e2 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml @@ -6,14 +6,17 @@ QtObject { source: "./MaterialIcons-Regular.ttf" } readonly property string fontFamily: fl.name - - readonly property string rotation3D: "\ue84d" + readonly property string _360: "\ue577" + readonly property string _3d_rotation: "\ue84d" + readonly property string _4k: "\ue072" readonly property string ac_unit: "\ueb3b" readonly property string access_alarm: "\ue190" readonly property string access_alarms: "\ue191" readonly property string access_time: "\ue192" readonly property string accessibility: "\ue84e" + readonly property string accessibility_new: "\ue92c" readonly property string accessible: "\ue914" + readonly property string accessible_forward: "\ue934" readonly property string account_balance: "\ue84f" readonly property string account_balance_wallet: "\ue850" readonly property string account_box: "\ue851" @@ -24,10 +27,15 @@ QtObject { readonly property string add_alarm: "\ue193" readonly property string add_alert: "\ue003" readonly property string add_box: "\ue146" + readonly property string add_call: "\ue0e8" readonly property string add_circle: "\ue147" readonly property string add_circle_outline: "\ue148" + readonly property string add_comment: "\ue266" + readonly property string add_link: "\ue178" readonly property string add_location: "\ue567" + readonly property string add_photo_alternate: "\ue43e" readonly property string add_shopping_cart: "\ue854" + readonly property string add_to_home_screen: "\ue1fe" readonly property string add_to_photos: "\ue39d" readonly property string add_to_queue: "\ue05c" readonly property string adjust: "\ue39e" @@ -50,16 +58,22 @@ QtObject { readonly property string album: "\ue019" readonly property string all_inclusive: "\ueb3d" readonly property string all_out: "\ue90b" + readonly property string alternate_email: "\ue0e6" readonly property string android: "\ue859" readonly property string announcement: "\ue85a" readonly property string apps: "\ue5c3" readonly property string archive: "\ue149" readonly property string arrow_back: "\ue5c4" + readonly property string arrow_back_ios: "\ue5e0" readonly property string arrow_downward: "\ue5db" readonly property string arrow_drop_down: "\ue5c5" readonly property string arrow_drop_down_circle: "\ue5c6" readonly property string arrow_drop_up: "\ue5c7" readonly property string arrow_forward: "\ue5c8" + readonly property string arrow_forward_ios: "\ue5e1" + readonly property string arrow_left: "\ue5de" + readonly property string arrow_right: "\ue5df" + readonly property string arrow_right_alt: "\ue941" readonly property string arrow_upward: "\ue5d8" readonly property string art_track: "\ue060" readonly property string aspect_ratio: "\ue85b" @@ -72,6 +86,7 @@ QtObject { readonly property string assignment_turned_in: "\ue862" readonly property string assistant: "\ue39f" readonly property string assistant_photo: "\ue3a0" + readonly property string atm: "\ue573" readonly property string attach_file: "\ue226" readonly property string attach_money: "\ue227" readonly property string attachment: "\ue2bc" @@ -80,6 +95,8 @@ QtObject { readonly property string av_timer: "\ue01b" readonly property string backspace: "\ue14a" readonly property string backup: "\ue864" + readonly property string ballot: "\ue172" + readonly property string bar_chart: "\ue26b" readonly property string battery_alert: "\ue19c" readonly property string battery_charging_full: "\ue1a3" readonly property string battery_full: "\ue1a4" @@ -134,6 +151,8 @@ QtObject { readonly property string business_center: "\ueb3f" readonly property string cached: "\ue86a" readonly property string cake: "\ue7e9" + readonly property string calendar_today: "\ue935" + readonly property string calendar_view_day: "\ue936" readonly property string call: "\ue0b0" readonly property string call_end: "\ue0b1" readonly property string call_made: "\ue0b2" @@ -150,12 +169,15 @@ QtObject { readonly property string camera_rear: "\ue3b2" readonly property string camera_roll: "\ue3b3" readonly property string cancel: "\ue5c9" + readonly property string cancel_presentation: "\ue0e9" readonly property string card_giftcard: "\ue8f6" readonly property string card_membership: "\ue8f7" readonly property string card_travel: "\ue8f8" readonly property string casino: "\ueb40" readonly property string cast: "\ue307" readonly property string cast_connected: "\ue308" + readonly property string category: "\ue574" + readonly property string cell_wifi: "\ue0ec" readonly property string center_focus_strong: "\ue3b4" readonly property string center_focus_weak: "\ue3b5" readonly property string change_history: "\ue86b" @@ -166,12 +188,13 @@ QtObject { readonly property string check_box: "\ue834" readonly property string check_box_outline_blank: "\ue835" readonly property string check_circle: "\ue86c" + readonly property string check_circle_outline: "\ue92d" readonly property string chevron_left: "\ue5cb" readonly property string chevron_right: "\ue5cc" readonly property string child_care: "\ueb41" readonly property string child_friendly: "\ueb42" readonly property string chrome_reader_mode: "\ue86d" - readonly property string class_: "\ue86e" + readonly property string _class: "\ue86e" readonly property string clear: "\ue14c" readonly property string clear_all: "\ue0b8" readonly property string close: "\ue5cd" @@ -189,16 +212,20 @@ QtObject { readonly property string color_lens: "\ue3b7" readonly property string colorize: "\ue3b8" readonly property string comment: "\ue0b9" + readonly property string commute: "\ue940" readonly property string compare: "\ue3b9" readonly property string compare_arrows: "\ue915" + readonly property string compass_calibration: "\ue57c" readonly property string computer: "\ue30a" readonly property string confirmation_number: "\ue638" readonly property string contact_mail: "\ue0d0" readonly property string contact_phone: "\ue0cf" + readonly property string contact_support: "\ue94c" readonly property string contacts: "\ue0ba" readonly property string content_copy: "\ue14d" readonly property string content_cut: "\ue14e" readonly property string content_paste: "\ue14f" + readonly property string control_camera: "\ue074" readonly property string control_point: "\ue3ba" readonly property string control_point_duplicate: "\ue3bb" readonly property string copyright: "\ue90c" @@ -221,9 +248,11 @@ QtObject { readonly property string data_usage: "\ue1af" readonly property string date_range: "\ue916" readonly property string dehaze: "\ue3c7" - readonly property string delete_: "\ue872" + readonly property string _delete: "\ue872" readonly property string delete_forever: "\ue92b" + readonly property string delete_outline: "\ue92e" readonly property string delete_sweep: "\ue16c" + readonly property string departure_board: "\ue576" readonly property string description: "\ue873" readonly property string desktop_mac: "\ue30b" readonly property string desktop_windows: "\ue30c" @@ -231,6 +260,7 @@ QtObject { readonly property string developer_board: "\ue30d" readonly property string developer_mode: "\ue1b0" readonly property string device_hub: "\ue335" + readonly property string device_unknown: "\ue339" readonly property string devices: "\ue1b1" readonly property string devices_other: "\ue337" readonly property string dialer_sip: "\ue0bb" @@ -253,16 +283,21 @@ QtObject { readonly property string do_not_disturb_on: "\ue644" readonly property string dock: "\ue30e" readonly property string domain: "\ue7ee" + readonly property string domain_disabled: "\ue0ef" readonly property string done: "\ue876" readonly property string done_all: "\ue877" + readonly property string done_outline: "\ue92f" readonly property string donut_large: "\ue917" readonly property string donut_small: "\ue918" readonly property string drafts: "\ue151" readonly property string drag_handle: "\ue25d" + readonly property string drag_indicator: "\ue945" readonly property string drive_eta: "\ue613" readonly property string dvr: "\ue1b2" readonly property string edit: "\ue3c9" + readonly property string edit_attributes: "\ue578" readonly property string edit_location: "\ue568" + readonly property string edit_off: "\ue950" readonly property string eject: "\ue8fb" readonly property string email: "\ue0be" readonly property string enhanced_encryption: "\ue63f" @@ -277,6 +312,7 @@ QtObject { readonly property string event_note: "\ue616" readonly property string event_seat: "\ue903" readonly property string exit_to_app: "\ue879" + readonly property string expand: "\ue94f" readonly property string expand_less: "\ue5ce" readonly property string expand_more: "\ue5cf" readonly property string explicit: "\ue01e" @@ -291,6 +327,7 @@ QtObject { readonly property string face: "\ue87c" readonly property string fast_forward: "\ue01f" readonly property string fast_rewind: "\ue020" + readonly property string fastfood: "\ue57a" readonly property string favorite: "\ue87d" readonly property string favorite_border: "\ue87e" readonly property string featured_play_list: "\ue06d" @@ -301,6 +338,7 @@ QtObject { readonly property string fiber_new: "\ue05e" readonly property string fiber_pin: "\ue06a" readonly property string fiber_smart_record: "\ue062" + readonly property string file_copy: "\ue173" readonly property string file_download: "\ue2c4" readonly property string file_upload: "\ue2c6" readonly property string filter: "\ue3d3" @@ -359,6 +397,7 @@ QtObject { readonly property string format_line_spacing: "\ue240" readonly property string format_list_bulleted: "\ue241" readonly property string format_list_numbered: "\ue242" + readonly property string format_list_numbered_rtl: "\ue267" readonly property string format_paint: "\ue243" readonly property string format_quote: "\ue244" readonly property string format_shapes: "\ue25e" @@ -403,6 +442,7 @@ QtObject { readonly property string hdr_weak: "\ue3f2" readonly property string headset: "\ue310" readonly property string headset_mic: "\ue311" + readonly property string headset_off: "\ue33a" readonly property string healing: "\ue3f3" readonly property string hearing: "\ue023" readonly property string help: "\ue887" @@ -412,14 +452,18 @@ QtObject { readonly property string highlight_off: "\ue888" readonly property string history: "\ue889" readonly property string home: "\ue88a" + readonly property string horizontal_split: "\ue947" readonly property string hot_tub: "\ueb46" readonly property string hotel: "\ue53a" readonly property string hourglass_empty: "\ue88b" readonly property string hourglass_full: "\ue88c" + readonly property string how_to_reg: "\ue174" + readonly property string how_to_vote: "\ue175" readonly property string http: "\ue902" readonly property string https: "\ue88d" readonly property string image: "\ue3f4" readonly property string image_aspect_ratio: "\ue3f5" + readonly property string image_search: "\ue43f" readonly property string import_contacts: "\ue0e0" readonly property string import_export: "\ue0c3" readonly property string important_devices: "\ue912" @@ -429,6 +473,7 @@ QtObject { readonly property string info_outline: "\ue88f" readonly property string input: "\ue890" readonly property string insert_chart: "\ue24b" + readonly property string insert_chart_outlined: "\ue26a" readonly property string insert_comment: "\ue24c" readonly property string insert_drive_file: "\ue24d" readonly property string insert_emoticon: "\ue24e" @@ -451,6 +496,8 @@ QtObject { readonly property string keyboard_voice: "\ue31d" readonly property string kitchen: "\ueb47" readonly property string label: "\ue892" + readonly property string label_important: "\ue937" + readonly property string label_important_outline: "\ue948" readonly property string label_outline: "\ue893" readonly property string landscape: "\ue3f7" readonly property string language: "\ue894" @@ -468,13 +515,16 @@ QtObject { readonly property string library_add: "\ue02e" readonly property string library_books: "\ue02f" readonly property string library_music: "\ue030" + readonly property string lightbulb: "\ue0f0" readonly property string lightbulb_outline: "\ue90f" readonly property string line_style: "\ue919" readonly property string line_weight: "\ue91a" readonly property string linear_scale: "\ue260" readonly property string link: "\ue157" + readonly property string link_off: "\ue16f" readonly property string linked_camera: "\ue438" readonly property string list: "\ue896" + readonly property string list_alt: "\ue0ee" readonly property string live_help: "\ue0c6" readonly property string live_tv: "\ue639" readonly property string local_activity: "\ue53f" @@ -530,6 +580,8 @@ QtObject { readonly property string map: "\ue55b" readonly property string markunread: "\ue159" readonly property string markunread_mailbox: "\ue89b" + readonly property string maximize: "\ue930" + readonly property string meeting_room: "\ueb4f" readonly property string memory: "\ue322" readonly property string menu: "\ue5d2" readonly property string merge_type: "\ue252" @@ -537,10 +589,16 @@ QtObject { readonly property string mic: "\ue029" readonly property string mic_none: "\ue02a" readonly property string mic_off: "\ue02b" + readonly property string minimize: "\ue931" + readonly property string missed_video_call: "\ue073" readonly property string mms: "\ue618" + readonly property string mobile_friendly: "\ue200" + readonly property string mobile_off: "\ue201" + readonly property string mobile_screen_share: "\ue0e7" readonly property string mode_comment: "\ue253" readonly property string mode_edit: "\ue254" readonly property string monetization_on: "\ue263" + readonly property string money: "\ue57d" readonly property string money_off: "\ue25c" readonly property string monochrome_photos: "\ue403" readonly property string mood: "\ue7f2" @@ -556,6 +614,7 @@ QtObject { readonly property string movie_filter: "\ue43a" readonly property string multiline_chart: "\ue6df" readonly property string music_note: "\ue405" + readonly property string music_off: "\ue440" readonly property string music_video: "\ue063" readonly property string my_location: "\ue55c" readonly property string nature: "\ue406" @@ -572,21 +631,27 @@ QtObject { readonly property string next_week: "\ue16a" readonly property string nfc: "\ue1bb" readonly property string no_encryption: "\ue641" + readonly property string no_meeting_room: "\ueb4e" readonly property string no_sim: "\ue0cc" readonly property string not_interested: "\ue033" + readonly property string not_listed_location: "\ue575" readonly property string note: "\ue06f" readonly property string note_add: "\ue89c" + readonly property string notes: "\ue26c" + readonly property string notification_important: "\ue004" readonly property string notifications: "\ue7f4" readonly property string notifications_active: "\ue7f7" readonly property string notifications_none: "\ue7f5" readonly property string notifications_off: "\ue7f6" readonly property string notifications_paused: "\ue7f8" + readonly property string offline_bolt: "\ue932" readonly property string offline_pin: "\ue90a" readonly property string ondemand_video: "\ue63a" - readonly property string opacity_: "\ue91c" + readonly property string opacity: "\ue91c" readonly property string open_in_browser: "\ue89d" readonly property string open_in_new: "\ue89e" readonly property string open_with: "\ue89f" + readonly property string outlined_flag: "\ue16e" readonly property string pages: "\ue7f9" readonly property string pageview: "\ue8a0" readonly property string palette: "\ue40a" @@ -600,6 +665,7 @@ QtObject { readonly property string pause: "\ue034" readonly property string pause_circle_filled: "\ue035" readonly property string pause_circle_outline: "\ue036" + readonly property string pause_presentation: "\ue0ea" readonly property string payment: "\ue8a1" readonly property string people: "\ue7fb" readonly property string people_outline: "\ue7fc" @@ -621,6 +687,7 @@ QtObject { readonly property string phone: "\ue0cd" readonly property string phone_android: "\ue324" readonly property string phone_bluetooth_speaker: "\ue61b" + readonly property string phone_callback: "\ue649" readonly property string phone_forwarded: "\ue61c" readonly property string phone_in_talk: "\ue61d" readonly property string phone_iphone: "\ue325" @@ -663,12 +730,13 @@ QtObject { readonly property string portrait: "\ue416" readonly property string power: "\ue63c" readonly property string power_input: "\ue336" + readonly property string power_off: "\ue646" readonly property string power_settings_new: "\ue8ac" readonly property string pregnant_woman: "\ue91e" readonly property string present_to_all: "\ue0df" - readonly property string print_: "\ue8ad" + readonly property string _print: "\ue8ad" readonly property string priority_high: "\ue645" - readonly property string public_: "\ue80b" + readonly property string _public: "\ue80b" readonly property string publish: "\ue255" readonly property string query_builder: "\ue8ae" readonly property string question_answer: "\ue8af" @@ -701,10 +769,12 @@ QtObject { readonly property string reply: "\ue15e" readonly property string reply_all: "\ue15f" readonly property string report: "\ue160" + readonly property string report_off: "\ue170" readonly property string report_problem: "\ue8b2" readonly property string restaurant: "\ue56c" readonly property string restaurant_menu: "\ue561" readonly property string restore: "\ue8b3" + readonly property string restore_from_trash: "\ue938" readonly property string restore_page: "\ue929" readonly property string ring_volume: "\ue0d1" readonly property string room: "\ue8b4" @@ -719,9 +789,12 @@ QtObject { readonly property string rv_hookup: "\ue642" readonly property string satellite: "\ue562" readonly property string save: "\ue161" + readonly property string save_alt: "\ue171" readonly property string scanner: "\ue329" + readonly property string scatter_plot: "\ue268" readonly property string schedule: "\ue8b5" readonly property string school: "\ue80c" + readonly property string score: "\ue269" readonly property string screen_lock_landscape: "\ue1be" readonly property string screen_lock_portrait: "\ue1bf" readonly property string screen_lock_rotation: "\ue1c0" @@ -736,6 +809,7 @@ QtObject { readonly property string sentiment_dissatisfied: "\ue811" readonly property string sentiment_neutral: "\ue812" readonly property string sentiment_satisfied: "\ue813" + readonly property string sentiment_satisfied_alt: "\ue0ed" readonly property string sentiment_very_dissatisfied: "\ue814" readonly property string sentiment_very_satisfied: "\ue815" readonly property string settings: "\ue8b8" @@ -764,7 +838,9 @@ QtObject { readonly property string short_text: "\ue261" readonly property string show_chart: "\ue6e1" readonly property string shuffle: "\ue043" + readonly property string shutter_speed: "\ue43d" readonly property string signal_cellular_4_bar: "\ue1c8" + readonly property string signal_cellular_alt: "\ue202" readonly property string signal_cellular_connected_no_internet_4_bar: "\ue1cd" readonly property string signal_cellular_no_sim: "\ue1ce" readonly property string signal_cellular_null: "\ue1cf" @@ -817,10 +893,12 @@ QtObject { readonly property string subscriptions: "\ue064" readonly property string subtitles: "\ue048" readonly property string subway: "\ue56f" + readonly property string supervised_user_circle: "\ue939" readonly property string supervisor_account: "\ue8d3" readonly property string surround_sound: "\ue049" readonly property string swap_calls: "\ue0d7" readonly property string swap_horiz: "\ue8d4" + readonly property string swap_horizontal_circle: "\ue933" readonly property string swap_vert: "\ue8d5" readonly property string swap_vertical_circle: "\ue8d6" readonly property string switch_camera: "\ue41e" @@ -832,6 +910,7 @@ QtObject { readonly property string system_update_alt: "\ue8d7" readonly property string tab: "\ue8d8" readonly property string tab_unselected: "\ue8d9" + readonly property string table_chart: "\ue265" readonly property string tablet: "\ue32f" readonly property string tablet_android: "\ue330" readonly property string tablet_mac: "\ue331" @@ -840,11 +919,17 @@ QtObject { readonly property string terrain: "\ue564" readonly property string text_fields: "\ue262" readonly property string text_format: "\ue165" + readonly property string text_rotate_up: "\ue93a" + readonly property string text_rotate_vertical: "\ue93b" + readonly property string text_rotation_down: "\ue93e" + readonly property string text_rotation_none: "\ue93f" readonly property string textsms: "\ue0d8" readonly property string texture: "\ue421" readonly property string theaters: "\ue8da" readonly property string thumb_down: "\ue8db" + readonly property string thumb_down_alt: "\ue816" readonly property string thumb_up: "\ue8dc" + readonly property string thumb_up_alt: "\ue817" readonly property string thumbs_up_down: "\ue8dd" readonly property string time_to_leave: "\ue62c" readonly property string timelapse: "\ue422" @@ -865,25 +950,30 @@ QtObject { readonly property string train: "\ue570" readonly property string tram: "\ue571" readonly property string transfer_within_a_station: "\ue572" - readonly property string transform_: "\ue428" + readonly property string transform: "\ue428" + readonly property string transit_enterexit: "\ue579" readonly property string translate: "\ue8e2" readonly property string trending_down: "\ue8e3" readonly property string trending_flat: "\ue8e4" readonly property string trending_up: "\ue8e5" + readonly property string trip_origin: "\ue57b" readonly property string tune: "\ue429" readonly property string turned_in: "\ue8e6" readonly property string turned_in_not: "\ue8e7" readonly property string tv: "\ue333" + readonly property string tv_off: "\ue647" readonly property string unarchive: "\ue169" readonly property string undo: "\ue166" readonly property string unfold_less: "\ue5d6" readonly property string unfold_more: "\ue5d7" + readonly property string unsubscribe: "\ue0eb" readonly property string update: "\ue923" readonly property string usb: "\ue1e0" readonly property string verified_user: "\ue8e8" readonly property string vertical_align_bottom: "\ue258" readonly property string vertical_align_center: "\ue259" readonly property string vertical_align_top: "\ue25a" + readonly property string vertical_split: "\ue949" readonly property string vibration: "\ue62d" readonly property string video_call: "\ue070" readonly property string video_label: "\ue071" @@ -908,6 +998,7 @@ QtObject { readonly property string visibility: "\ue8f4" readonly property string visibility_off: "\ue8f5" readonly property string voice_chat: "\ue62e" + readonly property string voice_over_off: "\ue94a" readonly property string voicemail: "\ue0d9" readonly property string volume_down: "\ue04d" readonly property string volume_mute: "\ue04e" @@ -919,6 +1010,7 @@ QtObject { readonly property string warning: "\ue002" readonly property string watch: "\ue334" readonly property string watch_later: "\ue924" + readonly property string waves: "\ue176" readonly property string wb_auto: "\ue42c" readonly property string wb_cloudy: "\ue42d" readonly property string wb_incandescent: "\ue42e" @@ -929,11 +1021,15 @@ QtObject { readonly property string web_asset: "\ue069" readonly property string weekend: "\ue16b" readonly property string whatshot: "\ue80e" + readonly property string where_to_vote: "\ue177" readonly property string widgets: "\ue1bd" readonly property string wifi: "\ue63e" readonly property string wifi_lock: "\ue1e1" + readonly property string wifi_off: "\ue648" readonly property string wifi_tethering: "\ue1e2" readonly property string work: "\ue8f9" + readonly property string work_off: "\ue942" + readonly property string work_outline: "\ue943" readonly property string wrap_text: "\ue25b" readonly property string youtube_searched_for: "\ue8fa" readonly property string zoom_in: "\ue8ff" From 2b07699f65e2c203d9c4bde084838199e7a63e8c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 17 Jun 2019 12:24:51 +0200 Subject: [PATCH 209/293] [ui] Viewer3D: improved trackball parameters Make trackball larger and adjust rotation speed to make 3D navigation easier. --- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index fc7006c182..c7b3b1b858 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -134,8 +134,8 @@ FocusScope { width: root.width height: root.height } - rotationSpeed: 10 - trackballSize: 0.4 + rotationSpeed: 16 + trackballSize: 0.9 camera: mainCamera focus: scene3D.activeFocus From eb8023e02ca34c11d473fb9c4fc12743064cc64d Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 17 Jun 2019 12:25:57 +0200 Subject: [PATCH 210/293] [ui] NodeEditor: display AttributeEditor by default --- meshroom/ui/qml/GraphEditor/NodeEditor.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 3ce454ff76..b625302415 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -138,7 +138,7 @@ Panel { Layout.fillWidth: true width: childrenRect.width position: TabBar.Footer - currentIndex: 1 + currentIndex: 0 TabButton { text: "Attributes" width: implicitWidth From ff70b0bcd34ebf1ba91a3b7f07c069e2b64bbf38 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 17 Jun 2019 13:06:18 +0200 Subject: [PATCH 211/293] [setup] add 'ALICEVISION_BIN_PATH' env. var to path Enables to add AliceVision binaries folder to path (without having to manipulate PATH) using ALICEVISION_BIN_PATH. --- meshroom/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index 8b6101a98b..2a2fc4bb67 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -67,6 +67,9 @@ def addToEnvPath(var, val, index=-1): val (str or list of str): the path(s) to add index (int): insertion index """ + if not val: + return + paths = os.environ.get(var, "").split(os.pathsep) if not isinstance(val, (list, tuple)): @@ -108,3 +111,5 @@ def addToEnvPath(var, val, index=-1): if key not in os.environ and os.path.exists(value): logging.info("Set {}: {}".format(key, value)) os.environ[key] = value + else: + addToEnvPath("PATH", os.environ.get("ALICEVISION_BIN_PATH", "")) From 292b0fb5fe19ac258bef11e9da4908702f648689 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 17 Jun 2019 15:08:49 +0200 Subject: [PATCH 212/293] [ui] About: update copyright year --- meshroom/ui/qml/AboutDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/AboutDialog.qml b/meshroom/ui/qml/AboutDialog.qml index 64ffdad35d..8af761e406 100644 --- a/meshroom/ui/qml/AboutDialog.qml +++ b/meshroom/ui/qml/AboutDialog.qml @@ -111,7 +111,7 @@ Dialog { font.pointSize: 10 } Label { - text: "2018 AliceVision contributors" + text: "2010-2019 AliceVision contributors" } } From 00857a7a88a5a60d29f50181a9c05c403e70bc30 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:25:40 +0200 Subject: [PATCH 213/293] [ui] Reconstruction: rename 'endNode' to 'texturing' + use makeProperty helper --- meshroom/ui/qml/WorkspaceView.qml | 6 +++--- meshroom/ui/reconstruction.py | 23 +++++------------------ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index d0f2c12ed7..1a9dc3d3d0 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -133,8 +133,8 @@ Item { // Load reconstructed model Button { - readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null - readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS" + readonly property var outputAttribute: _reconstruction.texturing ? _reconstruction.texturing.attribute("outputMesh") : null + readonly property bool outputReady: outputAttribute && _reconstruction.texturing.globalStatus === "SUCCESS" readonly property int outputMediaIndex: viewer3D.library.find(outputAttribute) text: "Load Model" @@ -142,7 +142,7 @@ Item { anchors.bottomMargin: 10 anchors.horizontalCenter: parent.horizontalCenter visible: outputReady && outputMediaIndex == -1 - onClicked: viewer3D.view(_reconstruction.endNode.attribute("outputMesh")) + onClicked: viewer3D.view(_reconstruction.texturing.attribute("outputMesh")) } } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b16c623a3c..a11b6784e0 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -164,7 +164,7 @@ def __init__(self, graphFilepath='', parent=None): self._buildingIntrinsics = False self._cameraInit = None self._cameraInits = QObjectListModel(parent=self) - self._endNode = None + self._texturing = None self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) self.graphChanged.connect(self.onGraphChanged) self._liveSfmManager = LiveSfmManager(self) @@ -212,7 +212,7 @@ def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() self.sfm = None - self.endNode = None + self.texturing = None self.updateCameraInits() if not self._graph: return @@ -493,21 +493,8 @@ def setSfm(self, node): self._sfm.chunks[0].statusChanged.disconnect(self.updateViewsAndPoses) self._sfm.destroyed.disconnect(self._unsetSfm) self._setSfm(node) - self.setEndNode(self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS)) - def setEndNode(self, node=None): - if self._endNode == node: - return - if self._endNode: - try: - self._endNode.destroyed.disconnect(self.setEndNode) - except RuntimeError: - # self._endNode might have been destroyed at this point, causing PySide2 to throw a RuntimeError - pass - self._endNode = node - if self._endNode: - self._endNode.destroyed.connect(self.setEndNode) - self.endNodeChanged.emit() + self.texturing = self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS) @Slot(QObject, result=bool) def isInViews(self, viewpoint): @@ -570,8 +557,8 @@ def reconstructedCamerasCount(self): # 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) - endNodeChanged = Signal() - endNode = Property(QObject, lambda self: self._endNode, setEndNode, notify=endNodeChanged) + texturingChanged = Signal() + texturing = makeProperty(QObject, "_texturing", notify=texturingChanged) nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged) From a358c4674840b39a7e10035c85dac5b4abf3226c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:31:43 +0200 Subject: [PATCH 214/293] [ui] Reconstruction: improve constructor readability --- meshroom/ui/reconstruction.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index a11b6784e0..3bda8defa5 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -161,19 +161,27 @@ class Reconstruction(UIGraph): def __init__(self, graphFilepath='', parent=None): super(Reconstruction, self).__init__(graphFilepath, parent) + + # initialize member variables for key steps of the 3D reconstruction pipeline + + # - CameraInit + self._cameraInit = None # current CameraInit node + self._cameraInits = QObjectListModel(parent=self) # all CameraInit nodes self._buildingIntrinsics = False - self._cameraInit = None - self._cameraInits = QObjectListModel(parent=self) - self._texturing = None self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) - self.graphChanged.connect(self.onGraphChanged) - self._liveSfmManager = LiveSfmManager(self) - # SfM result + # - SfM self._sfm = None self._views = None self._poses = None self._selectedViewId = None + self._liveSfmManager = LiveSfmManager(self) + + # - Texturing + self._texturing = None + + # react to internal graph changes to update those variables + self.graphChanged.connect(self.onGraphChanged) if graphFilepath: self.onGraphChanged() From 4435ba9e1bdfea361b56e463d21d09d0ed3b8320 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 27 Jun 2019 18:36:10 +0200 Subject: [PATCH 215/293] [ui] Reconstruction: add 'featureExtraction' member * add featureExtraction property to keep track of current FeatureExtraction node * update it when current CameraInit node is changed * allow to set current CameraInit by double clicking on a CameraInit node from the Graph Editor --- meshroom/ui/qml/main.qml | 8 ++++++++ meshroom/ui/reconstruction.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 164dd224ec..989355aac0 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -590,6 +590,14 @@ ApplicationWindow { { _reconstruction.sfm = node; } + else if(node.nodeType === "FeatureExtraction") + { + _reconstruction.featureExtraction = node; + } + else if(node.nodeType === "CameraInit") + { + _reconstruction.cameraInit = node; + } for(var i=0; i < node.attributes.count; ++i) { var attr = node.attributes.at(i) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 3bda8defa5..9ef09dadf4 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -170,6 +170,10 @@ def __init__(self, graphFilepath='', parent=None): self._buildingIntrinsics = False self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable) + # - Feature Extraction + self._featureExtraction = None + self.cameraInitChanged.connect(self.updateFeatureExtraction) + # - SfM self._sfm = None self._views = None @@ -219,6 +223,7 @@ def load(self, filepath): def onGraphChanged(self): """ React to the change of the internal graph. """ self._liveSfmManager.reset() + self.featureExtraction = None self.sfm = None self.texturing = None self.updateCameraInits() @@ -257,6 +262,10 @@ 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 lastSfmNode(self): """ Retrieve the last SfM node from the initial CameraInit node. """ return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS) @@ -561,6 +570,10 @@ def reconstructedCamerasCount(self): sfmChanged = Signal() sfm = Property(QObject, getSfm, setSfm, notify=sfmChanged) + + featureExtractionChanged = Signal() + featureExtraction = makeProperty(QObject, "_featureExtraction", featureExtractionChanged, 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) From 48658c78cbb387ec08d9560ed39e350afe695166 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 1 Jul 2019 16:52:20 +0200 Subject: [PATCH 216/293] [node][alicevision] SfMTransform: update version to 1.1 A new "method" called "from_single_camera" has been added since the last release --- meshroom/nodes/aliceVision/SfMTransform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index 3dc4435eec..af1725055a 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "1.1" from meshroom.core import desc From 1e50e02b83870a165fcd2c289854f95844121edd Mon Sep 17 00:00:00 2001 From: Clara Date: Thu, 4 Jul 2019 11:03:49 +0200 Subject: [PATCH 217/293] [Texturing] add option to correct exposure values during Texturing --- meshroom/nodes/aliceVision/Texturing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 4bc94fcc8c..d9dfcb2a6e 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -91,6 +91,14 @@ class Texturing(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='correctEV', + label='Correct Exposure', + description='Uniformize images exposure values.', + value=False, + uid=[0], + advanced=True, + ), desc.BoolParam( name='useScore', label='Use Score', From 8dd0a4be22873c64e04ee49a81bfcf9e375661a6 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 4 Jul 2019 17:51:44 +0200 Subject: [PATCH 218/293] [ui] New FeaturesViewer component FeaturesViewer wraps QtAliceVision plugin's FeaturesViewer component to display the extracted feature points of a View. --- meshroom/ui/qml/Utils/Colors.qml | 3 ++ meshroom/ui/qml/Viewer/FeaturesViewer.qml | 40 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 meshroom/ui/qml/Viewer/FeaturesViewer.qml diff --git a/meshroom/ui/qml/Utils/Colors.qml b/meshroom/ui/qml/Utils/Colors.qml index 4dea34beb3..54f6fd6649 100644 --- a/meshroom/ui/qml/Utils/Colors.qml +++ b/meshroom/ui/qml/Utils/Colors.qml @@ -14,4 +14,7 @@ QtObject { readonly property color yellow: "#FFEB3B" readonly property color red: "#F44336" readonly property color blue: "#03A9F4" + readonly property color cyan: "#00BCD4" + readonly property color pink: "#E91E63" + readonly property color lime: "#CDDC39" } diff --git a/meshroom/ui/qml/Viewer/FeaturesViewer.qml b/meshroom/ui/qml/Viewer/FeaturesViewer.qml new file mode 100644 index 0000000000..6e21de14b2 --- /dev/null +++ b/meshroom/ui/qml/Viewer/FeaturesViewer.qml @@ -0,0 +1,40 @@ +import QtQuick 2.11 +import AliceVision 1.0 as AliceVision + +import Utils 1.0 + +/** + * FeaturesViewer displays the extracted feature points of a View. + * Requires QtAliceVision plugin. + */ +Repeater { + id: root + + /// ViewID to display the features of + property int viewId + /// Folder containing the features files + property string folder + /// The list of describer types to load + property alias describerTypes: root.model + /// List of available display modes + readonly property var displayModes: ['Points', 'Squares', 'Oriented Squares'] + /// Current display mode index + property int displayMode: 0 + /// The list of colors used for displaying several describers + property var colors: [Colors.blue, Colors.red, Colors.yellow, Colors.green, Colors.orange, Colors.cyan, Colors.pink, Colors.lime] + /// Offset the color list + property int colorOffset: 0 + + model: root.describerTypes + + // instantiate one FeaturesViewer by describer type + delegate: AliceVision.FeaturesViewer { + readonly property int colorIndex: (index+root.colorOffset)%root.colors.length + describerType: modelData + folder: root.folder + viewId: root.viewId + color: root.colors[colorIndex] + displayMode: root.displayMode + } + +} From 4563d32e3e3cad86604188f1566147aa9ccdd2d5 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 4 Jul 2019 17:53:30 +0200 Subject: [PATCH 219/293] [ui] Viewer2D: add FeaturesViewer + dedicated overlay Make FeaturesViewer available in Viewer2D to display the feature points of the current view, based on the current FeatureExtraction node. * FeaturesInfoOverlay: overlay that displays info and provides controls over a FeaturesViewer component. * ColorChart: color picker based on a set of predefined colors. --- meshroom/ui/qml/Controls/ColorChart.qml | 66 +++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + .../ui/qml/Viewer/FeaturesInfoOverlay.qml | 111 ++++++++++++++++++ meshroom/ui/qml/Viewer/Viewer2D.qml | 53 +++++++++ 4 files changed, 231 insertions(+) create mode 100644 meshroom/ui/qml/Controls/ColorChart.qml create mode 100644 meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml diff --git a/meshroom/ui/qml/Controls/ColorChart.qml b/meshroom/ui/qml/Controls/ColorChart.qml new file mode 100644 index 0000000000..82667f22d0 --- /dev/null +++ b/meshroom/ui/qml/Controls/ColorChart.qml @@ -0,0 +1,66 @@ +import QtQuick 2.10 +import QtQuick.Controls 2.10 + +import Utils 1.0 + +/** + * ColorChart is a color picker based on a set of predefined colors. + * It takes the form of a ToolButton that pops-up its palette when pressed. + */ +ToolButton { + id: root + + property var colors: ["red", "green", "blue"] + property int currentIndex: 0 + + signal colorPicked(var colorIndex) + + background: Rectangle { + color: root.colors[root.currentIndex] + border.width: hovered ? 1 : 0 + border.color: Colors.sysPalette.midlight + } + + onPressed: palettePopup.open() + + // Popup for the color palette + Popup { + id: palettePopup + + padding: 4 + // content width is missing side padding (hence the + padding*2) + implicitWidth: colorChart.contentItem.width + padding*2 + + // center the current color + y: - (root.height - padding) / 2 + x: - colorChart.currentItem.x - padding + + // Colors palette + ListView { + id: colorChart + implicitHeight: contentItem.childrenRect.height + implicitWidth: contentWidth + orientation: ListView.Horizontal + spacing: 2 + currentIndex: root.currentIndex + model: root.colors + // display each color as a ToolButton with a custom background + delegate: ToolButton { + padding: 0 + width: root.width + height: root.height + background: Rectangle { + color: modelData + // display border of current/selected item + border.width: hovered || index === colorChart.currentIndex ? 1 : 0 + border.color: Colors.sysPalette.midlight + } + + onClicked: { + colorPicked(index); + palettePopup.close(); + } + } + } + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 295947a2a4..00e8965556 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -1,5 +1,6 @@ module Controls +ColorChart 1.0 ColorChart.qml FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml new file mode 100644 index 0000000000..acc9415fca --- /dev/null +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -0,0 +1,111 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.11 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 + +import Utils 1.0 +import Controls 1.0 + +/** + * FeaturesInfoOverlay is an overlay that displays info and + * provides controls over a FeaturesViewer component. + */ +FloatingPane { + id: root + + property int pluginStatus: Loader.Null + property Item featuresViewer: null + property var featureExtractionNode: null + + ColumnLayout { + + // Header + RowLayout { + // FeatureExtraction node name + Label { + text: featureExtractionNode.label + Layout.fillWidth: true + } + // Settings menu + Loader { + active: root.pluginStatus === Loader.Ready + sourceComponent: MaterialToolButton { + text: MaterialIcons.settings + font.pointSize: 10 + onClicked: settingsMenu.popup(width, 0) + Menu { + id: settingsMenu + padding: 4 + implicitWidth: 210 + + RowLayout { + Label { + text: "Display Mode:" + } + ComboBox { + id: displayModeCB + flat: true + Layout.fillWidth: true + model: featuresViewer.displayModes + onActivated: featuresViewer.displayMode = currentIndex + } + } + } + } + } + } + + // Error message if AliceVision plugin is unavailable + Label { + visible: root.pluginStatus === Loader.Error + text: "AliceVision plugin is required to display Features" + color: Colors.red + } + + // Feature types + ListView { + implicitHeight: contentHeight + implicitWidth: contentItem.childrenRect.width + + model: featuresViewer !== null ? featuresViewer.model : 0 + + delegate: RowLayout { + id: featureType + + property var viewer: featuresViewer.itemAt(index) + spacing: 4 + + // Visibility toogle + MaterialToolButton { + text: featureType.viewer.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off + onClicked: featureType.viewer.visible = !featureType.viewer.visible + font.pointSize: 10 + opacity: featureType.viewer.visible ? 1.0 : 0.6 + } + // ColorChart picker + ColorChart { + implicitWidth: 12 + implicitHeight: implicitWidth + colors: featuresViewer.colors + currentIndex: featureType.viewer.colorIndex + // offset FeaturesViewer color set when changing the color of one feature type + onColorPicked: featuresViewer.colorOffset = colorIndex - index + } + // Feature type name + Label { + text: featureType.viewer.describerType + (featureType.viewer.loading ? "" : ": " + featureType.viewer.features.length) + } + // Feature loading status + Loader { + active: featureType.viewer.loading + sourceComponent: BusyIndicator { + padding: 0 + implicitWidth: 12 + implicitHeight: 12 + running: true + } + } + } + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index d7954b19dd..1a4eb8c4b7 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -77,8 +77,39 @@ FocusScope { visible: image.status === Image.Loading } + + // FeatureViewer: display view extracted feature points + // note: requires QtAliceVision plugin - use a Loader to evaluate plugin avaibility at runtime + Loader { + id: featuresViewerLoader + + active: displayFeatures.checked + + // handle rotation/position based on available metadata + rotation: { + var orientation = metadata ? metadata["Orientation"] : 0 + switch(orientation) { + case "6": return 90; + case "8": return -90; + default: return 0; + } + } + x: rotation === 90 ? image.paintedWidth : 0 + y: rotation === -90 ? image.paintedHeight : 0 + + Component.onCompleted: { + // instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource + setSource("FeaturesViewer.qml", { + 'active': Qt.binding(function() { return displayFeatures.checked; }), + 'viewId': Qt.binding(function() { return _reconstruction.selectedViewId; }), + 'model': Qt.binding(function() { return _reconstruction.featureExtraction.attribute("describerTypes").value; }), + 'folder': Qt.binding(function() { return Filepath.stringToUrl(_reconstruction.featureExtraction.attribute("output").value); }), + }) + } + } } + // Busy indicator BusyIndicator { anchors.centerIn: parent @@ -147,6 +178,21 @@ FocusScope { metadata: visible ? root.metadata : {} } + + Loader { + id: featuresOverlay + anchors.bottom: bottomToolbar.top + anchors.left: parent.left + anchors.margins: 2 + active: displayFeatures.checked + + sourceComponent: FeaturesInfoOverlay { + featureExtractionNode: _reconstruction.featureExtraction + pluginStatus: featuresViewerLoader.status + featuresViewer: featuresViewerLoader.item + } + } + FloatingPane { id: bottomToolbar anchors.bottom: parent.bottom @@ -163,6 +209,13 @@ FocusScope { text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x" state: "xsmall" } + MaterialToolButton { + id: displayFeatures + font.pointSize: 11 + ToolTip.text: "Display Features" + checkable: true + text: MaterialIcons.scatter_plot + } Item { Layout.fillWidth: true From 55408d6731eb71eea74840e4e2cf9715130062e8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 5 Jul 2019 23:08:36 +0200 Subject: [PATCH 220/293] [nodes][alicevision] PrepareDenseScene: Update version with new option "evCorrection" --- meshroom/nodes/aliceVision/PrepareDenseScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index 0bb4976352..5467d576ab 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -1,4 +1,4 @@ -__version__ = "2.0" +__version__ = "3.0" from meshroom.core import desc From 4e22ec5553702ccc430eb16e8a4ecd49d07d0880 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 5 Jul 2019 23:10:10 +0200 Subject: [PATCH 221/293] [nodes][alicevision] Texturing: update version with new param processColorspace --- meshroom/nodes/aliceVision/Texturing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 2066135eb9..ca5aeaf7e8 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -1,4 +1,4 @@ -__version__ = "4.0" +__version__ = "5.0" from meshroom.core import desc From 88b0297660fe41d3a9e10f465d57a6d41db53e33 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 8 Jul 2019 19:48:06 +0200 Subject: [PATCH 222/293] [ui] Graph: update ChunkMonitor after 'saveAs' Saving file on disk impacts cache folder location and therefore status files paths; force re-evaluation of monitored filepaths at the end of a "saveAs" operation. --- meshroom/ui/graph.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 0f7b1cf2c0..417b2ce9c4 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -306,8 +306,12 @@ def updateChunks(self): if self._sortedDFSChunks.objectList() == chunks: return self._sortedDFSChunks.setObjectList(chunks) - # Update the list of monitored chunks - self._chunksMonitor.setChunks(self._sortedDFSChunks) + # provide ChunkMonitor with the update list of chunks + self.updateChunkMonitor(self._sortedDFSChunks) + + def updateChunkMonitor(self, chunks): + """ Update the list of chunks for status files monitoring. """ + self._chunksMonitor.setChunks(chunks) def clear(self): if self._graph: @@ -342,6 +346,9 @@ def saveAs(self, url): localFile += ".mg" self._graph.save(localFile) self._undoStack.setClean() + # saving file on disk impacts cache folder location + # => force re-evaluation of monitored status files paths + self.updateChunkMonitor(self._sortedDFSChunks) @Slot() def save(self): From 52361d0b86fa22e5fac49d4b9ef5951223e22b45 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 8 Jul 2019 20:04:22 +0200 Subject: [PATCH 223/293] [ui] TextFileViewer: wait for request completion before sending a new one On auto-realod mode, only trigger a new request when the last one has been completed. Avoids requests loops on slow filesystems that can lead to UI freezes. --- meshroom/ui/qml/Controls/TextFileViewer.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/Controls/TextFileViewer.qml b/meshroom/ui/qml/Controls/TextFileViewer.qml index 525fd9e340..0f32cb232b 100644 --- a/meshroom/ui/qml/Controls/TextFileViewer.qml +++ b/meshroom/ui/qml/Controls/TextFileViewer.qml @@ -22,6 +22,7 @@ Item { property bool loading: false onSourceChanged: loadSource() + onAutoReloadChanged: loadSource() onVisibleChanged: if(visible) loadSource() RowLayout { @@ -307,11 +308,10 @@ Item { // Auto-reload current file timer Timer { + id: reloadTimer running: root.autoReload interval: root.autoReloadInterval - repeat: true - // reload file on start and stop - onRunningChanged: loadSource() + repeat: false // timer is restarted in request's callback (see loadSource) onTriggered: loadSource() } @@ -333,6 +333,9 @@ Item { if(xhr.readyState === XMLHttpRequest.DONE) { textView.setText(xhr.status === 200 ? xhr.responseText : ""); loading = false; + // re-trigger reload source file + if(autoReload) + reloadTimer.restart(); } }; xhr.send(); From cee8d1b6122371f2070fe044317d92d55533cf13 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 10 Jul 2019 16:35:44 +0200 Subject: [PATCH 224/293] [ui] Viewer3D: add support for vertex-colored meshes Use PerVertexColorMaterial if any vertex color data is available on a mesh without textures. --- meshroom/ui/components/scene3D.py | 7 +++++++ meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/components/scene3D.py b/meshroom/ui/components/scene3D.py index 971db7935a..bf5b44c5a9 100644 --- a/meshroom/ui/components/scene3D.py +++ b/meshroom/ui/components/scene3D.py @@ -45,6 +45,13 @@ def faceCount(self, entity): count += sum([attr.count() for attr in geo.attributes() if attr.name() == "vertexPosition"]) return count / 3 + @Slot(Qt3DCore.QEntity, result=int) + def vertexColorCount(self, entity): + count = 0 + for geo in entity.findChildren(Qt3DRender.QGeometry): + count += sum([attr.count() for attr in geo.attributes() if attr.name() == "vertexColor"]) + return count + class TrackballController(QObject): """ diff --git a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml index a93abeba40..75081e1861 100644 --- a/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml +++ b/meshroom/ui/qml/Viewer3D/MaterialSwitcher.qml @@ -62,7 +62,11 @@ Entity { }, State { name: "Textured" - PropertyChanges { target: m; material: diffuseMap ? textured : solid } + PropertyChanges { + target: m; + // "textured" material resolution order: diffuse map > vertex color data > no color info + material: diffuseMap ? textured : (Scene3DHelper.vertexColorCount(root.parent) ? colored : solid) + } } ] } @@ -80,6 +84,11 @@ Entity { diffuse: root.diffuseColor } + PerVertexColorMaterial { + id: colored + objectName: "VertexColorMaterial" + } + DiffuseSpecularMaterial { id: textured objectName: "TexturedMaterial" From fea7f89e9939e6bc42af64de85362582a2a606f3 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 10 Jul 2019 18:26:08 +0200 Subject: [PATCH 225/293] [nodes][aliceVision] Meshing: make outputMesh first output parameter ensures that the mesh is loaded in the 3D Viewer when double clicking on a Meshing node --- meshroom/nodes/aliceVision/Meshing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index cac485e35e..b20f386f95 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -230,17 +230,17 @@ class Meshing(desc.CommandLineNode): outputs = [ desc.File( - name="output", - label="Output Dense Point Cloud", - description="Output dense point cloud with visibilities (SfMData file format).", - value="{cache}/{nodeType}/{uid0}/densePointCloud.abc", - uid=[], - ), - desc.File( name="outputMesh", label="Output Mesh", description="Output mesh (OBJ file format).", value="{cache}/{nodeType}/{uid0}/mesh.obj", uid=[], ), + desc.File( + name="output", + label="Output Dense Point Cloud", + description="Output dense point cloud with visibilities (SfMData file format).", + value="{cache}/{nodeType}/{uid0}/densePointCloud.abc", + uid=[], + ), ] From 44fe4f765e4b19c5b9b00f52182f4e8bfaf95ea2 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 11 Jul 2019 12:45:11 +0200 Subject: [PATCH 226/293] [nodes][aliceVision] Meshing: add "colorizeOutput" option --- meshroom/nodes/aliceVision/Meshing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/Meshing.py b/meshroom/nodes/aliceVision/Meshing.py index b20f386f95..449ef008aa 100644 --- a/meshroom/nodes/aliceVision/Meshing.py +++ b/meshroom/nodes/aliceVision/Meshing.py @@ -209,6 +209,13 @@ class Meshing(desc.CommandLineNode): uid=[0], advanced=True, ), + desc.BoolParam( + name='colorizeOutput', + label='Colorize Output', + description='Whether to colorize output dense point cloud and mesh.', + value=False, + uid=[0], + ), desc.BoolParam( name='saveRawDensePointCloud', label='Save Raw Dense Point Cloud', From 02e2a5f04866214db717cd7b91f63db45fe0aefd Mon Sep 17 00:00:00 2001 From: Clara Date: Wed, 24 Jul 2019 15:15:07 +0200 Subject: [PATCH 227/293] [Texturing] set texturing downscale default value to 1 --- meshroom/nodes/aliceVision/Texturing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index 6cb3d580de..5996f76a4f 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -42,7 +42,7 @@ class Texturing(desc.CommandLineNode): name='downscale', label='Texture Downscale', description='''Texture downscale factor''', - value=2, + value=1, values=(1, 2, 4, 8), exclusive=True, uid=[0], From f6a42cb86e089cfaaa373e617f14e00059ed7b51 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 24 Jul 2019 17:53:34 +0200 Subject: [PATCH 228/293] [ui] Nodes status monitoring simplification * store status file last modification time on NodeChunk * ChunksMonitor: don't stop the underlying thread when changing chunks, only modify the list of monitored files + use a mutex for thread-safety --- meshroom/core/node.py | 3 ++ meshroom/ui/graph.py | 83 +++++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 95f19b5e9e..60c7573e1c 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -144,6 +144,7 @@ def __init__(self, node, range, parent=None): self.range = range self.status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion) self.statistics = stats.Statistics() + self.statusFileLastModTime = -1 self._subprocess = None # notify update in filepaths when node's internal folder changes self.node.internalFolderChanged.connect(self.nodeFolderChanged) @@ -175,11 +176,13 @@ def updateStatusFromCache(self): oldStatus = self.status.status # No status file => reset status to Status.None if not os.path.exists(statusFile): + self.statusFileLastModTime = -1 self.status.reset() else: with open(statusFile, 'r') as jsonFile: statusData = json.load(jsonFile) self.status.fromDict(statusData) + self.statusFileLastModTime = os.path.getmtime(statusFile) if oldStatus != self.status.status: self.statusChanged.emit() diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 417b2ce9c4..4d2b3a9bb8 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -4,7 +4,7 @@ import os import time from enum import Enum -from threading import Thread, Event +from threading import Thread, Event, Lock from multiprocessing.pool import ThreadPool from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint @@ -28,12 +28,13 @@ class FilesModTimePollerThread(QObject): def __init__(self, parent=None): super(FilesModTimePollerThread, self).__init__(parent) self._thread = None + self._mutex = Lock() self._threadPool = ThreadPool(4) self._stopFlag = Event() self._refreshInterval = 5 # refresh interval in seconds self._files = [] - def start(self, files): + def start(self, files=None): """ Start polling thread. Args: @@ -42,14 +43,20 @@ def start(self, files): if self._thread: # thread already running, return return - if not files: - # file list is empty - return self._stopFlag.clear() self._files = files or [] self._thread = Thread(target=self.run) self._thread.start() + def setFiles(self, files): + """ Set the list of files to monitor + + Args: + files: the list of files to monitor + """ + with self._mutex: + self._files = files + def stop(self): """ Request polling thread to stop. """ if not self._thread: @@ -69,8 +76,12 @@ def getFileLastModTime(f): def run(self): """ Poll watched files for last modification time. """ while not self._stopFlag.wait(self._refreshInterval): - times = self._threadPool.map(FilesModTimePollerThread.getFileLastModTime, self._files) - self.timesAvailable.emit(times) + with self._mutex: + files = list(self._files) + times = self._threadPool.map(FilesModTimePollerThread.getFileLastModTime, files) + with self._mutex: + if files == self._files: + self.timesAvailable.emit(times) class ChunksMonitor(QObject): @@ -85,49 +96,25 @@ class ChunksMonitor(QObject): """ def __init__(self, chunks=(), parent=None): super(ChunksMonitor, self).__init__(parent) - self.lastModificationRecords = dict() + self.chunks = [] self._filesTimePoller = FilesModTimePollerThread(parent=self) self._filesTimePoller.timesAvailable.connect(self.compareFilesTimes) - self._pollerOutdated = False + self._filesTimePoller.start() self.setChunks(chunks) def setChunks(self, chunks): """ Set the list of chunks to monitor. """ - self._filesTimePoller.stop() - self.clear() - for chunk in chunks: - # initialize last modification times to current time for all chunks - self.lastModificationRecords[chunk] = time.time() - # For local use, handle statusChanged emitted directly from the node chunk - chunk.statusChanged.connect(self.onChunkStatusChanged) - self._pollerOutdated = True - self.chunkStatusChanged.emit(None, -1) - self._filesTimePoller.start(self.statusFiles) - self._pollerOutdated = False + self.chunks = chunks + self._filesTimePoller.setFiles(self.statusFiles) def stop(self): """ Stop the status files monitoring. """ self._filesTimePoller.stop() - def clear(self): - """ Clear the list of monitored chunks. """ - for chunk in self.lastModificationRecords: - chunk.statusChanged.disconnect(self.onChunkStatusChanged) - self.lastModificationRecords.clear() - - def onChunkStatusChanged(self): - """ React to change of status coming from the NodeChunk itself. """ - chunk = self.sender() - assert chunk in self.lastModificationRecords - # update record entry for this file so that it's up-to-date on next timerEvent - # use current time instead of actual file's mtime to limit filesystem requests - self.lastModificationRecords[chunk] = time.time() - self.chunkStatusChanged.emit(chunk, chunk.status.status) - @property def statusFiles(self): """ Get status file paths from current chunks. """ - return [c.statusFile for c in self.lastModificationRecords.keys()] + return [c.statusFile for c in self.chunks] def compareFilesTimes(self, times): """ @@ -137,21 +124,12 @@ def compareFilesTimes(self, times): Args: times: the last modification times for currently monitored files. """ - if self._pollerOutdated: - return - - newRecords = dict(zip(self.lastModificationRecords.keys(), times)) - for chunk, previousTime in self.lastModificationRecords.items(): - lastModTime = newRecords.get(chunk, -1) - # update chunk status if: - # - last modification time is more recent than previous record - # - file is no more available (-1) - if lastModTime > previousTime or (lastModTime == -1 != previousTime): - self.lastModificationRecords[chunk] = lastModTime + newRecords = dict(zip(self.chunks, times)) + for chunk, fileModTime in newRecords.items(): + # update chunk status if last modification time has changed since previous record + if fileModTime != chunk.statusFileLastModTime: chunk.updateStatusFromCache() - chunkStatusChanged = Signal(NodeChunk, int) - class GraphLayout(QObject): """ @@ -270,7 +248,6 @@ def __init__(self, filepath='', parent=None): self._graph = Graph('', self) self._modificationCount = 0 self._chunksMonitor = ChunksMonitor(parent=self) - self._chunksMonitor.chunkStatusChanged.connect(self.onChunkStatusChanged) self._computeThread = Thread() self._running = self._submitted = False self._sortedDFSChunks = QObjectListModel(parent=self) @@ -305,7 +282,11 @@ def updateChunks(self): # Nothing has changed, return if self._sortedDFSChunks.objectList() == chunks: return + for chunk in self._sortedDFSChunks: + chunk.statusChanged.disconnect(self.onChunkStatusChanged) self._sortedDFSChunks.setObjectList(chunks) + for chunk in self._sortedDFSChunks: + chunk.statusChanged.connect(self.onChunkStatusChanged) # provide ChunkMonitor with the update list of chunks self.updateChunkMonitor(self._sortedDFSChunks) @@ -393,7 +374,7 @@ def submit(self, node=None): node = [node] if node else None submitGraph(self._graph, os.environ.get('MESHROOM_DEFAULT_SUBMITTER', ''), node) - def onChunkStatusChanged(self, chunk, status): + def onChunkStatusChanged(self): # update graph computing status running = any([ch.status.status == Status.RUNNING for ch in self._sortedDFSChunks]) submitted = any([ch.status.status == Status.SUBMITTED for ch in self._sortedDFSChunks]) From 36fc7ea069092a1500e83081ce659f59037c856d Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 24 Jul 2019 18:03:58 +0200 Subject: [PATCH 229/293] [ui] GraphEditor: add "Refresh Nodes Status" MenuItem Enables the user to force the re-evaluation of nodes status. --- meshroom/ui/graph.py | 5 +++++ meshroom/ui/qml/main.qml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 4d2b3a9bb8..f9e396b36b 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -538,6 +538,11 @@ def upgradeAllNodes(self): for node in nodes: self.upgradeNode(node) + @Slot() + def forceNodesStatusUpdate(self): + """ Force re-evaluation of graph's nodes status. """ + self._graph.updateStatusFromCache(force=True) + @Slot(Attribute, QJsonValue) def appendAttribute(self, attribute, value=QJsonValue()): if isinstance(value, QJsonValue): diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index 164dd224ec..15e18a2e2a 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -561,6 +561,11 @@ ApplicationWindow { enabled: !_reconstruction.computingLocally onTriggered: _reconstruction.graph.clearSubmittedNodes() } + MenuItem { + text: "Refresh Nodes Status" + enabled: !_reconstruction.computingLocally + onTriggered: _reconstruction.forceNodesStatusUpdate() + } Menu { title: "Advanced" MenuItem { From aff5a98a4b33001574788db33f2c2f818dbcd174 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 24 Jul 2019 17:55:44 +0200 Subject: [PATCH 230/293] [ui] Viewer3D: use RenderSettings.Always renderPolicy Since Qt-5.13, RenderSettings.OnDemand can lead to rendering issues due to non-triggered updates (e.g: when resizing the viewport or changing visibilities of objects displayed). --- meshroom/ui/qml/Viewer3D/Viewer3D.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Viewer3D/Viewer3D.qml b/meshroom/ui/qml/Viewer3D/Viewer3D.qml index c7b3b1b858..df2570c6a0 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3D.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3D.qml @@ -171,7 +171,7 @@ FocusScope { RenderSettings { pickingSettings.pickMethod: PickingSettings.PrimitivePicking // enables point/edge/triangle picking pickingSettings.pickResultMode: PickingSettings.NearestPick - renderPolicy: RenderSettings.OnDemand + renderPolicy: RenderSettings.Always activeFrameGraph: RenderSurfaceSelector { // Use the whole viewport From f5607cb1271138269f51cb7a350b990d357fea27 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Wed, 24 Jul 2019 18:13:23 +0200 Subject: [PATCH 231/293] [ui] downgrade QtQuick import version for retro-compatibility --- meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml index acc9415fca..37e930c31b 100644 --- a/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml +++ b/meshroom/ui/qml/Viewer/FeaturesInfoOverlay.qml @@ -1,5 +1,5 @@ -import QtQuick 2.11 -import QtQuick.Controls 2.11 +import QtQuick 2.9 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import MaterialIcons 2.2 From d229743c3d5b5d2a4f998b42e454328e01b4b563 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 25 Jul 2019 14:40:05 +0200 Subject: [PATCH 232/293] [ui] update graph computing status when NodeChunks change Ensure graph computing status is properly updated after deleting a node that was running --- meshroom/ui/graph.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index f9e396b36b..952713fe6c 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -283,12 +283,14 @@ def updateChunks(self): if self._sortedDFSChunks.objectList() == chunks: return for chunk in self._sortedDFSChunks: - chunk.statusChanged.disconnect(self.onChunkStatusChanged) + chunk.statusChanged.disconnect(self.updateGraphComputingStatus) self._sortedDFSChunks.setObjectList(chunks) for chunk in self._sortedDFSChunks: - chunk.statusChanged.connect(self.onChunkStatusChanged) + chunk.statusChanged.connect(self.updateGraphComputingStatus) # provide ChunkMonitor with the update list of chunks self.updateChunkMonitor(self._sortedDFSChunks) + # update graph computing status based on the new list of NodeChunks + self.updateGraphComputingStatus() def updateChunkMonitor(self, chunks): """ Update the list of chunks for status files monitoring. """ @@ -374,7 +376,7 @@ def submit(self, node=None): node = [node] if node else None submitGraph(self._graph, os.environ.get('MESHROOM_DEFAULT_SUBMITTER', ''), node) - def onChunkStatusChanged(self): + def updateGraphComputingStatus(self): # update graph computing status running = any([ch.status.status == Status.RUNNING for ch in self._sortedDFSChunks]) submitted = any([ch.status.status == Status.SUBMITTED for ch in self._sortedDFSChunks]) From 0a0d21d4a1b02c032512482a32f62ee84da9d87f Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:30:27 +0200 Subject: [PATCH 233/293] Add GPU stats --- meshroom/core/stats.py | 46 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index adc226253b..1a117d64cc 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -1,9 +1,13 @@ from collections import defaultdict +from distutils import spawn +from subprocess import Popen, PIPE +import xml.etree.ElementTree as ET import logging import psutil import time import threading - +import platform +import os def bytes2human(n): """ @@ -32,6 +36,31 @@ def __init__(self): self.vramAvailable = 0 # GB self.swapAvailable = 0 + if platform.system() == "Windows": + # If the platform is Windows and nvidia-smi + # could not be found from the environment path, + # try to find it from system drive with default installation path + self.nvidia_smi = spawn.find_executable('nvidia-smi') + if self.nvidia_smi is None: + self.nvidia_smi = "%s\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe" % os.environ['systemdrive'] + else: + self.nvidia_smi = "nvidia-smi" + + try: + p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) + xmlGpu, stdError = p.communicate() + + gpuTree = ET.fromstring(xmlGpu) + + gpuMemoryUsage = gpuTree[4].find('fb_memory_usage') + + self.gpuMemoryTotal = gpuMemoryUsage[0].text.split(" ")[0] + self.gpuName = gpuTree[4].find('product_name').text + + + except: + pass + self.curves = defaultdict(list) def _addKV(self, k, v): @@ -50,6 +79,21 @@ def update(self): self._addKV('swapUsage', psutil.swap_memory().percent) self._addKV('vramUsage', 0) self._addKV('ioCounters', psutil.disk_io_counters()) + self.updateGpu() + + def updateGpu(self): + try: + p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) + xmlGpu, stdError = p.communicate() + + gpuTree = ET.fromstring(xmlGpu) + + self._addKV('gpuMemoryUsed', gpuTree[4].find('fb_memory_usage')[1].text.split(" ")[0]) + self._addKV('gpuUsed', gpuTree[4].find('utilization')[0].text.split(" ")[0]) + self._addKV('gpuTemperature', gpuTree[4].find('temperature')[0].text.split(" ")[0]) + + except: + return def toDict(self): return self.__dict__ From edf5dc7570cbfd26727bb385d928ba1ede8684e4 Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:31:31 +0200 Subject: [PATCH 234/293] Set update interval variable and add it to statistics file --- meshroom/core/stats.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index 1a117d64cc..10928f0bc1 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -195,6 +195,7 @@ def __init__(self): self.computer = ComputerStatistics() self.process = ProcStatistics() self.times = [] + self.interval = 5 def update(self, proc): ''' @@ -213,6 +214,7 @@ def toDict(self): 'computer': self.computer.toDict(), 'process': self.process.toDict(), 'times': self.times, + 'interval': self.interval } def fromDict(self, d): @@ -248,7 +250,7 @@ def run(self): try: while True: self.updateStats() - if self._stopFlag.wait(60): + if self._stopFlag.wait(self.statistics.interval): # stopFlag has been set # update stats one last time and exit main loop if self.proc.is_running(): From 73f667d22653358a02fcf07653a6124cc5bbdcb1 Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:32:33 +0200 Subject: [PATCH 235/293] Add new CPU and RAM stats with new psutil version 5.6.3 --- meshroom/core/stats.py | 3 ++- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index 10928f0bc1..b8b270328e 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -31,7 +31,8 @@ class ComputerStatistics: def __init__(self): # TODO: init self.nbCores = 0 - self.cpuFreq = 0 + self.cpuFreq = psutil.cpu_freq().max + self.ramTotal = psutil.virtual_memory().total / 1024/1024/1024 self.ramAvailable = 0 # GB self.vramAvailable = 0 # GB self.swapAvailable = 0 diff --git a/requirements.txt b/requirements.txt index 20a6a62700..7ad4de1266 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # runtime -psutil +psutil>=5.6.3 enum34;python_version<"3.4" PySide2==5.11.1 markdown==2.6.11 From 54ff012a04138732ea26228e80ffafd05d09d9be Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:33:37 +0200 Subject: [PATCH 236/293] Clear statistics when node has finished computing --- meshroom/core/node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 60c7573e1c..f2e98a2de3 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -285,6 +285,7 @@ def process(self, forceCompute=False): # ask and wait for the stats thread to stop self.statThread.stopRequest() self.statThread.join() + self.statistics = stats.Statistics() del runningProcesses[self.name] self.upgradeStatusTo(Status.SUCCESS) From e48039bb2f2d320299a7deb38283e856dd959703 Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:34:43 +0200 Subject: [PATCH 237/293] Add StatViewer component to view statistics charts --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 634 +++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 meshroom/ui/qml/GraphEditor/StatViewer.qml diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml new file mode 100644 index 0000000000..5db15c9287 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -0,0 +1,634 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 +import QtCharts 2.2 +import QtQuick.Layouts 1.11 +import Utils 1.0 +import MaterialIcons 2.2 + +Item { + id: root + + implicitWidth: 500 + implicitHeight: 500 + + property url source + property var sourceModified: undefined + property var jsonObject + property int nbReads: 1 + property var deltaTime: 1 + + property var cpuLineSeries: [] + property int nbCores: 0 + property int cpuFrequency: 0 + + property int ramTotal + + property int gpuTotalMemory + property int gpuMaxAxis: 100 + property string gpuName + + property color textColor: Colors.sysPalette.text + + readonly property var colors: [ + "#f44336", + "#e91e63", + "#9c27b0", + "#673ab7", + "#3f51b5", + "#2196f3", + "#03a9f4", + "#00bcd4", + "#009688", + "#4caf50", + "#8bc34a", + "#cddc39", + "#ffeb3b", + "#ffc107", + "#ff9800", + "#ff5722", + "#b71c1c", + "#880E4F", + "#4A148C", + "#311B92", + "#1A237E", + "#0D47A1", + "#01579B", + "#006064", + "#004D40", + "#1B5E20", + "#33691E", + "#827717", + "#F57F17", + "#FF6F00", + "#E65100", + "#BF360C" + ] + + onSourceChanged: function() { + resetCharts() + readSourceFile() + } + + Timer { + interval: root.deltaTime * 60000; running: true; repeat: true + onTriggered: function() { + var xhr = new XMLHttpRequest; + xhr.open("GET", source); + + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + + if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { + var jsonString = xhr.responseText; + + jsonObject= JSON.parse(jsonString); + root.jsonObject = jsonObject; + resetCharts() + sourceModified = xhr.getResponseHeader('Last-Modified') + root.createCharts() + } + } + }; + xhr.send(); + } + } + + function readSourceFile() { + var xhr = new XMLHttpRequest; + xhr.open("GET", source); + + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + var jsonString = xhr.responseText; + + jsonObject= JSON.parse(jsonString); + root.jsonObject = jsonObject; + + root.createCharts() + } + }; + xhr.send(); + } + + function resetCharts() { + cpuLineSeries = [] + cpuChart.removeAllSeries() + cpuCheckboxModel.clear() + ramChart.removeAllSeries() + gpuChart.removeAllSeries() + } + + function createCharts() { + initCpuChart() + initRamChart() + initGpuChart() + + root.deltaTime = jsonObject.interval /60 + } + + + + +/************************** +*** CPU *** +**************************/ + + function initCpuChart() { + var categories = [] + var categoryCount = 0 + var category + do { + category = jsonObject.computer.curves["cpuUsage." + categoryCount] + if(category !== undefined) { + categories.push(category) + categoryCount++ + } + } while(category !== undefined) + + var nbCores = categories.length + root.nbCores = nbCores + + root.cpuFrequency = jsonObject.computer.cpuFreq + + root.nbReads = categories[0].length-1 + + for(var j = 0; j < nbCores; j++) { + cpuCheckboxModel.append({ name: "CPU" + j, index: j, indicColor: colors[j % colors.length] }) + var lineSerie = cpuChart.createSeries(ChartView.SeriesTypeLine, "CPU" + j, valueAxisX, valueAxisY) + + if(categories[j].length === 1) { + lineSerie.append(0, categories[j][0]) + lineSerie.append(root.deltaTime, categories[j][0]) + } else { + for(var k = 0; k < categories[j].length; k++) { + lineSerie.append(k * root.deltaTime, categories[j][k]) + } + } + lineSerie.color = colors[j % colors.length] + + root.cpuLineSeries.push(lineSerie) + } + + cpuCheckboxModel.append({ name: "AVERAGE", index: nbCores, indicColor: colors[0] }) + var averageLine = cpuChart.createSeries(ChartView.SeriesTypeLine, "AVERAGE", valueAxisX, valueAxisY) + var average = [] + + for(var l = 0; l < categories[0].length; l++) { + average.push(0) + } + + for(var m = 0; m < categories.length; m++) { + for(var n = 0; n < categories[m].length; n++) { + average[n] += categories[m][n] + } + } + + for(var q = 0; q < average.length; q++) { + average[q] = average[q] / (categories.length-1) + + averageLine.append(q * root.deltaTime, average[q]) + } + + averageLine.color = colors[0] + + root.cpuLineSeries.push(averageLine) + } + + function showCpu(index) { + let serie = cpuLineSeries[index] + if(!serie.visible) { + serie.visible = true + } + } + + function hideCpu(index) { + let serie = cpuLineSeries[index] + if(serie.visible) { + serie.visible = false + } + } + + function hideOtherCpu(index) { + for(var i = 0; i < cpuLineSeries.length; i++) { + cpuLineSeries[i].visible = false + } + + cpuLineSeries[i].visible = true + } + + function higlightCpu(index) { + for(var i = 0; i < cpuLineSeries.length; i++) { + if(i === index) { + cpuLineSeries[i].width = 5.0 + } else { + cpuLineSeries[i].width = 0.2 + } + } + } + + function stopHighlightCpu(index) { + for(var i = 0; i < cpuLineSeries.length; i++) { + cpuLineSeries[i].width = 2.0 + } + } + + + + +/************************** +*** RAM *** +**************************/ + + function initRamChart() { + root.ramTotal = jsonObject.computer.ramTotal + + var ram = jsonObject.computer.curves.ramUsage + + var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM", valueAxisX2, valueAxisRam) + + if(ram.length === 1) { + ramSerie.append(0, ram[0] / 100 * root.ramTotal) + ramSerie.append(root.deltaTime, ram[0] / 100 * root.ramTotal) + } else { + for(var i = 0; i < ram.length; i++) { + ramSerie.append(i * root.deltaTime, ram[i] / 100 * root.ramTotal) + } + } + + ramSerie.color = colors[10] + } + + + +/************************** +*** GPU *** +**************************/ + + function initGpuChart() { + root.gpuTotalMemory = jsonObject.computer.gpuMemoryTotal + root.gpuName = jsonObject.computer.gpuName + + var gpuUsedMemory = jsonObject.computer.curves.gpuMemoryUsed + var gpuUsed = jsonObject.computer.curves.gpuUsed + var gpuTemperature = jsonObject.computer.curves.gpuTemperature + + var gpuUsedSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "GPU", valueAxisX3, valueAxisY3) + var gpuUsedMemorySerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Memory", valueAxisX3, valueAxisY3) + var gpuTemperatureSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Temperature", valueAxisX3, valueAxisY3) + + if(gpuUsedMemory.length === 1) { + gpuUsedSerie.append(0, gpuUsed[0]) + gpuUsedSerie.append(1 * root.deltaTime, gpuUsed[0]) + + gpuUsedMemorySerie.append(0, gpuUsedMemory[0] / root.gpuTotalMemory * 100) + gpuUsedMemorySerie.append(1 * root.deltaTime, gpuUsedMemory[0] / root.gpuTotalMemory * 100) + + gpuTemperatureSerie.append(0, gpuTemperature[0]) + gpuTemperatureSerie.append(1 * root.deltaTime, gpuTemperature[0]) + root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[0]) + } else { + for(var i = 0; i < gpuUsedMemory.length; i++) { + gpuUsedSerie.append(i * root.deltaTime, gpuUsed[i]) + + gpuUsedMemorySerie.append(i * root.deltaTime, gpuUsedMemory[i] / root.gpuTotalMemory * 100) + + gpuTemperatureSerie.append(i * root.deltaTime, gpuTemperature[i]) + root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[i]) + } + } + } + + + +/************************** +*** UI *** +**************************/ + + ScrollView { + height: root.height + width: root.width + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + ColumnLayout { + width: root.width + + +/************************** +*** CPU UI *** +**************************/ + + ColumnLayout { + Layout.fillWidth: true + + Button { + id: toggleCpuBtn + Layout.fillWidth: true + height: 30 + text: "Toggle CPU's" + state: "closed" + + onClicked: state === "opened" ? state = "closed" : state = "opened" + + Text { + text: MaterialIcons.arrow_drop_down + font.pixelSize: 24 + color: "#eee" + anchors.right: parent.right + } + + states: [ + State { + name: "opened" + PropertyChanges { target: cpuBtnContainer; visible: true } + PropertyChanges { target: toggleCpuBtn; down: true } + }, + State { + name: "closed" + PropertyChanges { target: cpuBtnContainer; visible: false } + PropertyChanges { target: toggleCpuBtn; down: false } + } + ] + } + + Item { + id: cpuBtnContainer + + Layout.fillWidth: true + implicitHeight: childrenRect.height + Layout.leftMargin: 25 + + RowLayout { + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + + ButtonGroup { + id: cpuGroup + exclusive: false + checkState: allCPU.checkState + } + + CheckBox { + width: 80 + checked: true + id: allCPU + text: "ALL" + checkState: cpuGroup.checkState + + indicator: Rectangle { + width: 20 + height: 20 + border.color: textColor + border.width: 2 + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.centerIn: parent + width: 10 + height: allCPU.checkState === 1 ? 4 : 10 + color: allCPU.checkState === 0 ? "transparent" : textColor + } + } + + leftPadding: indicator.width + 5 + + contentItem: Label { + text: allCPU.text + font: allCPU.font + verticalAlignment: Text.AlignVCenter + } + + Layout.fillHeight: true + } + + ListModel { + id: cpuCheckboxModel + } + + Flow { + Layout.fillWidth: true + + Repeater { + model: cpuCheckboxModel + + CheckBox { + width: 80 + checked: true + text: name + ButtonGroup.group: cpuGroup + + indicator: Rectangle { + width: 20 + height: 20 + border.color: indicColor + border.width: 2 + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.centerIn: parent + width: 10 + height: parent.parent.checkState === 1 ? 4 : 10 + color: parent.parent.checkState === 0 ? "transparent" : indicColor + } + } + + leftPadding: indicator.width + 5 + + contentItem: Label { + text: name + verticalAlignment: Text.AlignVCenter + } + + onCheckStateChanged: function() { + if(checkState === 2) { + root.showCpu(index) + } else { + root.hideCpu(index) + } + } + + onHoveredChanged: function() { + if(hovered) { + root.higlightCpu(index) + } else { + root.stopHighlightCpu(index) + } + } + + onDoubleClicked: function() { + name.checked = false + root.hideOtherCpu(index) + } + } + } + } + } + + + } + + ChartView { + id: cpuChart + + Layout.fillWidth: true + Layout.preferredHeight: width/2 + antialiasing: true + legend.visible: false + theme: ChartView.ChartThemeLight + backgroundColor: "transparent" + plotAreaColor: "transparent" + titleColor: textColor + + title: "CPU: " + root.nbCores + " cores, " + root.cpuFrequency + "Hz" + + ValueAxis { + id: valueAxisY + min: 0 + max: 100 + titleText: "%" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + + ValueAxis { + id: valueAxisX + min: 0 + max: root.deltaTime * Math.max(1, root.nbReads) + titleText: "Minutes" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + + } + } + + + +/************************** +*** RAM UI *** +**************************/ + + ColumnLayout { + + + ChartView { + id: ramChart + + Layout.fillWidth: true + Layout.preferredHeight: width/2 + antialiasing: true + legend.color: textColor + legend.labelColor: textColor + theme: ChartView.ChartThemeLight + backgroundColor: "transparent" + plotAreaColor: "transparent" + titleColor: textColor + + title: "RAM: " + root.ramTotal + "GB" + + ValueAxis { + id: valueAxisY2 + min: 0 + max: 100 + titleText: "%" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + + ValueAxis { + id: valueAxisRam + min: 0 + max: root.ramTotal + titleText: "GB" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + + ValueAxis { + id: valueAxisX2 + min: 0 + max: root.deltaTime * Math.max(1, root.nbReads) + titleText: "Minutes" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + } + } + + + +/************************** +*** GPU UI *** +**************************/ + + ColumnLayout { + + + ChartView { + id: gpuChart + + Layout.fillWidth: true + Layout.preferredHeight: width/2 + antialiasing: true + legend.color: textColor + legend.labelColor: textColor + theme: ChartView.ChartThemeLight + backgroundColor: "transparent" + plotAreaColor: "transparent" + titleColor: textColor + + title: "GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB" + + ValueAxis { + id: valueAxisY3 + min: 0 + max: root.gpuMaxAxis + titleText: "%, °C" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + + ValueAxis { + id: valueAxisX3 + min: 0 + max: root.deltaTime * Math.max(1, root.nbReads) + titleText: "Minutes" + color: textColor + gridLineColor: textColor + minorGridLineColor: textColor + shadesColor: textColor + shadesBorderColor: textColor + labelsColor: textColor + } + } + } + + } + } + +} From 1822bbe13d474240d3ebd3c5232a2d6d0def14c2 Mon Sep 17 00:00:00 2001 From: Lee Geertsen Date: Fri, 26 Jul 2019 11:35:44 +0200 Subject: [PATCH 238/293] Dynamically load TextFileViewer or StatViewer depending on selected tab --- meshroom/ui/qml/GraphEditor/NodeLog.qml | 41 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 872a1ef13a..8254a08bb3 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -89,8 +89,15 @@ FocusScope { // only set text file viewer source when ListView is fully ready // (either empty or fully populated with a valid currentChunk) // to avoid going through an empty url when switching between two nodes + if(!chunksLV.count || chunksLV.currentChunk) - textFileViewer.source = Filepath.stringToUrl(currentFile); + logComponentLoader.source = Filepath.stringToUrl(currentFile); + + if(currentItem.fileProperty === "statisticsFile") { + logComponentLoader.componentNb = 1 + } else { + logComponentLoader.componentNb = 0 + } } TabButton { @@ -111,12 +118,36 @@ FocusScope { } } - TextFileViewer { - id: textFileViewer + Loader { + id: logComponentLoader + clip: true Layout.fillWidth: true Layout.fillHeight: true - autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" - // source is set in fileSelector + property int componentNb: 0 + property url source + sourceComponent: componentNb === 0 ? textFileViewerComponent : statViewerComponent + } + + Component { + id: textFileViewerComponent + TextFileViewer { + id: textFileViewer + source: logComponentLoader.source + Layout.fillWidth: true + Layout.fillHeight: true + autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" + // source is set in fileSelector + } + } + + Component { + id: statViewerComponent + StatViewer { + id: statViewer + Layout.fillWidth: true + Layout.fillHeight: true + source: logComponentLoader.source + } } } } From 78d16df4a7c864a0d655d98de0728b0514a43985 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Wed, 31 Jul 2019 15:20:45 +0200 Subject: [PATCH 239/293] [bin] add views and intrinsic when loading custom pipeline --- bin/meshroom_photogrammetry | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index ffbd10ee56..982e837939 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -89,6 +89,9 @@ if args.pipeline: # reset graph inputs cameraInit.viewpoints.resetValue() cameraInit.intrinsics.resetValue() + # add views and intrinsics (if any) read from args.input + cameraInit.viewpoints.extend(views) + cameraInit.intrinsics.extend(intrinsics) if not graph.canComputeLeaves: raise RuntimeError("Graph cannot be computed. Check for compatibility issues.") From ac5a509a11d36a638e07da0d6598c7ed9d147d40 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Wed, 31 Jul 2019 15:21:19 +0200 Subject: [PATCH 240/293] [bin] raise exception if input format is not supported --- bin/meshroom_photogrammetry | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 982e837939..1d27d3a3cf 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -80,6 +80,8 @@ elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json' # args.input is a sfmData file: setup pre-calibrated views and intrinsics from meshroom.nodes.aliceVision.CameraInit import readSfMData views, intrinsics = readSfMData(args.input) +else: + raise RuntimeError(args.input + ': format not supported') # initialize photogrammetry pipeline if args.pipeline: From bedda0c5b670c363d4df0bce4dd14a80d2da44c9 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 6 Aug 2019 10:22:40 +0200 Subject: [PATCH 241/293] [ui] simplify loading of statistics file * NodeLog: remove intermediate property for Loader * StatViewer * keep only one function to load source file * only send request only after the previous one fully completed * update deltaTime before initializing charts * reset "sourceModified" property when source changes --- meshroom/ui/qml/GraphEditor/NodeLog.qml | 8 +-- meshroom/ui/qml/GraphEditor/StatViewer.qml | 61 ++++++++++------------ 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 8254a08bb3..3d5923e66f 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -93,11 +93,6 @@ FocusScope { if(!chunksLV.count || chunksLV.currentChunk) logComponentLoader.source = Filepath.stringToUrl(currentFile); - if(currentItem.fileProperty === "statisticsFile") { - logComponentLoader.componentNb = 1 - } else { - logComponentLoader.componentNb = 0 - } } TabButton { @@ -123,9 +118,8 @@ FocusScope { clip: true Layout.fillWidth: true Layout.fillHeight: true - property int componentNb: 0 property url source - sourceComponent: componentNb === 0 ? textFileViewerComponent : statViewerComponent + sourceComponent: fileSelector.currentItem.fileProperty === "statisticsFile" ? statViewerComponent : textFileViewerComponent } Component { diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 5db15c9287..5cc1a09b43 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -15,7 +15,7 @@ Item { property var sourceModified: undefined property var jsonObject property int nbReads: 1 - property var deltaTime: 1 + property real deltaTime: 1 property var cpuLineSeries: [] property int nbCores: 0 @@ -64,47 +64,47 @@ Item { "#BF360C" ] - onSourceChanged: function() { + onSourceChanged: { + sourceModified = undefined; resetCharts() readSourceFile() } Timer { - interval: root.deltaTime * 60000; running: true; repeat: true - onTriggered: function() { - var xhr = new XMLHttpRequest; - xhr.open("GET", source); - - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - - if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { - var jsonString = xhr.responseText; - - jsonObject= JSON.parse(jsonString); - root.jsonObject = jsonObject; - resetCharts() - sourceModified = xhr.getResponseHeader('Last-Modified') - root.createCharts() - } - } - }; - xhr.send(); - } + id: reloadTimer + interval: root.deltaTime * 60000; running: true; repeat: false + onTriggered: readSourceFile() + } function readSourceFile() { + if(!Filepath.urlToString(source).endsWith("statistics")) + return; + var xhr = new XMLHttpRequest; xhr.open("GET", source); xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - var jsonString = xhr.responseText; + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) { - jsonObject= JSON.parse(jsonString); - root.jsonObject = jsonObject; + if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { + var jsonObject; - root.createCharts() + try { + jsonObject = JSON.parse(xhr.responseText); + } + catch(exc) + { + console.warning("Failed to parse statistics file: " + source) + root.jsonObject = {}; + return; + } + root.jsonObject = jsonObject; + resetCharts(); + sourceModified = xhr.getResponseHeader('Last-Modified') + root.createCharts(); + reloadTimer.restart(); + } } }; xhr.send(); @@ -119,16 +119,13 @@ Item { } function createCharts() { + root.deltaTime = jsonObject.interval / 60.0; initCpuChart() initRamChart() initGpuChart() - - root.deltaTime = jsonObject.interval /60 } - - /************************** *** CPU *** **************************/ From 001d9a3de8c7ff9999d232663a9212ef58f83ec8 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 6 Aug 2019 10:28:12 +0200 Subject: [PATCH 242/293] [ui] StatViewer: fix average computation --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 5cc1a09b43..c84bb50e40 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -181,7 +181,7 @@ Item { } for(var q = 0; q < average.length; q++) { - average[q] = average[q] / (categories.length-1) + average[q] = average[q] / (categories.length) averageLine.append(q * root.deltaTime, average[q]) } From aedf10c8386a7621b719f56dc228ab8217166921 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 6 Aug 2019 10:30:10 +0200 Subject: [PATCH 243/293] [ui] StatViewer: fix "Toggle CPU" button use MaterialLabel instead of Text --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index c84bb50e40..5aa24051df 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -320,16 +320,14 @@ Item { Button { id: toggleCpuBtn Layout.fillWidth: true - height: 30 text: "Toggle CPU's" state: "closed" onClicked: state === "opened" ? state = "closed" : state = "opened" - Text { + MaterialLabel { text: MaterialIcons.arrow_drop_down - font.pixelSize: 24 - color: "#eee" + font.pointSize: 14 anchors.right: parent.right } From 07ced073f3e5afcf41168eca9a9d91899dc29457 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Tue, 6 Aug 2019 10:39:39 +0200 Subject: [PATCH 244/293] [ui] StatViewer: introduce custom ChartViewLegend system * add a generic ChartViewLegend component that provides an interactive legend for ChartViews * rely more on data bindings to control Series visibility and display --- meshroom/ui/qml/Charts/ChartViewCheckBox.qml | 34 ++++ meshroom/ui/qml/Charts/ChartViewLegend.qml | 105 +++++++++++ meshroom/ui/qml/Charts/qmldir | 4 + meshroom/ui/qml/GraphEditor/StatViewer.qml | 180 ++++--------------- 4 files changed, 178 insertions(+), 145 deletions(-) create mode 100644 meshroom/ui/qml/Charts/ChartViewCheckBox.qml create mode 100644 meshroom/ui/qml/Charts/ChartViewLegend.qml create mode 100644 meshroom/ui/qml/Charts/qmldir diff --git a/meshroom/ui/qml/Charts/ChartViewCheckBox.qml b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml new file mode 100644 index 0000000000..0b395b72e7 --- /dev/null +++ b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml @@ -0,0 +1,34 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + + +/** + * A custom CheckBox designed to be used in ChartView's legend. + */ +CheckBox { + id: root + + property color color + + leftPadding: 0 + font.pointSize: 8 + + indicator: Rectangle { + width: 11 + height: width + border.width: 1 + border.color: root.color + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + anchors.margins: parent.border.width + 1 + visible: parent.parent.checkState != Qt.Unchecked + anchors.topMargin: parent.parent.checkState === Qt.PartiallyChecked ? 5 : 2 + anchors.bottomMargin: anchors.topMargin + color: parent.border.color + anchors.centerIn: parent + } + } +} diff --git a/meshroom/ui/qml/Charts/ChartViewLegend.qml b/meshroom/ui/qml/Charts/ChartViewLegend.qml new file mode 100644 index 0000000000..1244871559 --- /dev/null +++ b/meshroom/ui/qml/Charts/ChartViewLegend.qml @@ -0,0 +1,105 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.9 +import QtCharts 2.3 + + +/** + * ChartViewLegend is an interactive legend component for ChartViews. + * It provides a CheckBox for each series that can control its visibility, + * and highlight on hovering. + */ +Flow { + id: root + + // The ChartView to create the legend for + property ChartView chartView + // Currently hovered series + property var hoveredSeries: null + + readonly property ButtonGroup buttonGroup: ButtonGroup { + id: legendGroup + exclusive: false + } + + /// Shortcut function to clear legend + function clear() { + seriesModel.clear(); + } + + // Update internal ListModel when ChartView's series change + Connections { + target: chartView + onSeriesAdded: seriesModel.append({"series": series}) + onSeriesRemoved: { + for(var i = 0; i < seriesModel.count; ++i) + { + if(seriesModel.get(i)["series"] === series) + { + seriesModel.remove(i); + return; + } + } + } + } + + onChartViewChanged: { + clear(); + for(var i = 0; i < chartView.count; ++i) + seriesModel.append({"series": chartView.series(i)}); + } + + Repeater { + + // ChartView series can't be accessed directly as a model. + // Use an intermediate ListModel populated with those series. + model: ListModel { + id: seriesModel + } + + ChartViewCheckBox { + ButtonGroup.group: legendGroup + + checked: series.visible + text: series.name + color: series.color + + onHoveredChanged: { + if(hovered && series.visible) + root.hoveredSeries = series; + else + root.hoveredSeries = null; + } + + // hovered serie properties override + states: [ + State { + when: series && root.hoveredSeries === series + PropertyChanges { target: series; width: 5.0 } + }, + State { + when: series && root.hoveredSeries && root.hoveredSeries !== series + PropertyChanges { target: series; width: 0.2 } + } + ] + + MouseArea { + anchors.fill: parent + onClicked: { + if(mouse.modifiers & Qt.ControlModifier) + root.soloSeries(index); + else + series.visible = !series.visible; + } + } + } + } + + /// Hide all series but the one at index 'idx' + function soloSeries(idx) { + for(var i = 0; i < seriesModel.count; i++) { + chartView.series(i).visible = false; + } + chartView.series(idx).visible = true; + } + +} diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir new file mode 100644 index 0000000000..32ea2d3271 --- /dev/null +++ b/meshroom/ui/qml/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts + +ChartViewLegend 1.0 ChartViewLegend.qml +ChartViewCheckBox 1.0 ChartViewCheckBox.qml diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 5aa24051df..e322f5e73f 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.3 import QtCharts 2.2 import QtQuick.Layouts 1.11 import Utils 1.0 +import Charts 1.0 import MaterialIcons 2.2 Item { @@ -11,13 +12,14 @@ Item { implicitWidth: 500 implicitHeight: 500 + /// Statistics source file property url source + property var sourceModified: undefined property var jsonObject property int nbReads: 1 property real deltaTime: 1 - property var cpuLineSeries: [] property int nbCores: 0 property int cpuFrequency: 0 @@ -29,6 +31,7 @@ Item { property color textColor: Colors.sysPalette.text + readonly property var colors: [ "#f44336", "#e91e63", @@ -78,6 +81,7 @@ Item { } function readSourceFile() { + // make sure we are trying to load a statistics file if(!Filepath.urlToString(source).endsWith("statistics")) return; @@ -111,9 +115,8 @@ Item { } function resetCharts() { - cpuLineSeries = [] + cpuLegend.clear() cpuChart.removeAllSeries() - cpuCheckboxModel.clear() ramChart.removeAllSeries() gpuChart.removeAllSeries() } @@ -131,11 +134,12 @@ Item { **************************/ function initCpuChart() { + var categories = [] var categoryCount = 0 var category do { - category = jsonObject.computer.curves["cpuUsage." + categoryCount] + category = root.jsonObject.computer.curves["cpuUsage." + categoryCount] if(category !== undefined) { categories.push(category) categoryCount++ @@ -150,7 +154,6 @@ Item { root.nbReads = categories[0].length-1 for(var j = 0; j < nbCores; j++) { - cpuCheckboxModel.append({ name: "CPU" + j, index: j, indicColor: colors[j % colors.length] }) var lineSerie = cpuChart.createSeries(ChartView.SeriesTypeLine, "CPU" + j, valueAxisX, valueAxisY) if(categories[j].length === 1) { @@ -162,11 +165,8 @@ Item { } } lineSerie.color = colors[j % colors.length] - - root.cpuLineSeries.push(lineSerie) } - cpuCheckboxModel.append({ name: "AVERAGE", index: nbCores, indicColor: colors[0] }) var averageLine = cpuChart.createSeries(ChartView.SeriesTypeLine, "AVERAGE", valueAxisX, valueAxisY) var average = [] @@ -186,51 +186,16 @@ Item { averageLine.append(q * root.deltaTime, average[q]) } - averageLine.color = colors[0] - - root.cpuLineSeries.push(averageLine) - } - - function showCpu(index) { - let serie = cpuLineSeries[index] - if(!serie.visible) { - serie.visible = true - } - } - - function hideCpu(index) { - let serie = cpuLineSeries[index] - if(serie.visible) { - serie.visible = false - } + averageLine.color = colors[colors.length-1] } function hideOtherCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - cpuLineSeries[i].visible = false - } - - cpuLineSeries[i].visible = true - } - - function higlightCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - if(i === index) { - cpuLineSeries[i].width = 5.0 - } else { - cpuLineSeries[i].width = 0.2 - } + for(var i = 0; i < cpuChart.count; i++) { + cpuChart.series(i).visible = false; } + cpuChart.series(index).visible = true; } - function stopHighlightCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - cpuLineSeries[i].width = 2.0 - } - } - - - /************************** *** RAM *** @@ -241,7 +206,7 @@ Item { var ram = jsonObject.computer.curves.ramUsage - var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM", valueAxisX2, valueAxisRam) + var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM: " + root.ramTotal + "GB", valueAxisX2, valueAxisRam) if(ram.length === 1) { ramSerie.append(0, ram[0] / 100 * root.ramTotal) @@ -356,111 +321,30 @@ Item { width: parent.width anchors.horizontalCenter: parent.horizontalCenter - ButtonGroup { - id: cpuGroup - exclusive: false - checkState: allCPU.checkState - } - CheckBox { - width: 80 - checked: true + ChartViewCheckBox { id: allCPU text: "ALL" - checkState: cpuGroup.checkState - - indicator: Rectangle { - width: 20 - height: 20 - border.color: textColor - border.width: 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.centerIn: parent - width: 10 - height: allCPU.checkState === 1 ? 4 : 10 - color: allCPU.checkState === 0 ? "transparent" : textColor + color: textColor + checkState: cpuLegend.buttonGroup.checkState + leftPadding: 0 + onClicked: { + var _checked = checked; + for(var i = 0; i < cpuChart.count; ++i) + { + cpuChart.series(i).visible = _checked; } } - - leftPadding: indicator.width + 5 - - contentItem: Label { - text: allCPU.text - font: allCPU.font - verticalAlignment: Text.AlignVCenter - } - - Layout.fillHeight: true } - ListModel { - id: cpuCheckboxModel - } - - Flow { + ChartViewLegend { + id: cpuLegend Layout.fillWidth: true - - Repeater { - model: cpuCheckboxModel - - CheckBox { - width: 80 - checked: true - text: name - ButtonGroup.group: cpuGroup - - indicator: Rectangle { - width: 20 - height: 20 - border.color: indicColor - border.width: 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.centerIn: parent - width: 10 - height: parent.parent.checkState === 1 ? 4 : 10 - color: parent.parent.checkState === 0 ? "transparent" : indicColor - } - } - - leftPadding: indicator.width + 5 - - contentItem: Label { - text: name - verticalAlignment: Text.AlignVCenter - } - - onCheckStateChanged: function() { - if(checkState === 2) { - root.showCpu(index) - } else { - root.hideCpu(index) - } - } - - onHoveredChanged: function() { - if(hovered) { - root.higlightCpu(index) - } else { - root.stopHighlightCpu(index) - } - } - - onDoubleClicked: function() { - name.checked = false - root.hideOtherCpu(index) - } - } - } + Layout.fillHeight: true + chartView: cpuChart } - } - + } } ChartView { @@ -468,7 +352,10 @@ Item { Layout.fillWidth: true Layout.preferredHeight: width/2 + margins.top: 0 + margins.bottom: 0 antialiasing: true + legend.visible: false theme: ChartView.ChartThemeLight backgroundColor: "transparent" @@ -514,15 +401,16 @@ Item { ColumnLayout { - ChartView { id: ramChart - + margins.top: 0 + margins.bottom: 0 Layout.fillWidth: true Layout.preferredHeight: width/2 antialiasing: true legend.color: textColor legend.labelColor: textColor + legend.visible: false theme: ChartView.ChartThemeLight backgroundColor: "transparent" plotAreaColor: "transparent" @@ -585,6 +473,8 @@ Item { Layout.fillWidth: true Layout.preferredHeight: width/2 + margins.top: 0 + margins.bottom: 0 antialiasing: true legend.color: textColor legend.labelColor: textColor From aac10be31d4c7283d8c57155310d81a934d2ae35 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 10:24:45 +0200 Subject: [PATCH 245/293] [docker] update to cuda 8 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 93fb664b4b..6a74dd1a2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -ARG CUDA_TAG=7.0 +ARG CUDA_TAG=8.0 ARG OS_TAG=7 -FROM alicevision:centos${OS_TAG}-cuda${CUDA_TAG} +FROM alicevision/alicevision:centos${OS_TAG}-cuda${CUDA_TAG} LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" # Execute with nvidia docker (https://github.com/nvidia/nvidia-docker/wiki/Installation-(version-2.0)) From 94c8c15b01bd470ccc0df747e1fc87367d53c8b9 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 10:25:11 +0200 Subject: [PATCH 246/293] [docker] update to Qt 5.13 --- Dockerfile | 3 ++- docker/qt-installer-noninteractive.qs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6a74dd1a2f..f615f428d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" ENV MESHROOM_DEV=/opt/Meshroom \ MESHROOM_BUILD=/tmp/Meshroom_build \ MESHROOM_BUNDLE=/opt/Meshroom_bundle \ - QT_DIR=/opt/qt/5.11.1/gcc_64 \ + QT_DIR=/opt/qt/5.13.0/gcc_64 \ PATH="${PATH}:${MESHROOM_BUNDLE}" COPY . "${MESHROOM_DEV}" @@ -52,6 +52,7 @@ RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install # Install Qt (to build plugins) WORKDIR /tmp/qt +# Qt version in specified in docker/qt-installer-noninteractive.qs RUN curl -LO http://download.qt.io/official_releases/online_installers/qt-unified-linux-x64-online.run && \ chmod u+x qt-unified-linux-x64-online.run && \ ./qt-unified-linux-x64-online.run --verbose --platform minimal --script "${MESHROOM_DEV}/docker/qt-installer-noninteractive.qs" && \ diff --git a/docker/qt-installer-noninteractive.qs b/docker/qt-installer-noninteractive.qs index 18224cd17a..32d65cb1fa 100644 --- a/docker/qt-installer-noninteractive.qs +++ b/docker/qt-installer-noninteractive.qs @@ -46,14 +46,14 @@ Controller.prototype.ComponentSelectionPageCallback = function() { widget.deselectAll(); // widget.selectComponent("qt"); - // widget.selectComponent("qt.qt5.5111"); - widget.selectComponent("qt.qt5.5111.gcc_64"); - // widget.selectComponent("qt.qt5.5111.qtscript"); - // widget.selectComponent("qt.qt5.5111.qtscript.gcc_64"); - // widget.selectComponent("qt.qt5.5111.qtwebengine"); - // widget.selectComponent("qt.qt5.5111.qtwebengine.gcc_64"); - // widget.selectComponent("qt.qt5.5111.qtwebglplugin"); - // widget.selectComponent("qt.qt5.5111.qtwebglplugin.gcc_64"); + // widget.selectComponent("qt.qt5.5130"); + widget.selectComponent("qt.qt5.5130.gcc_64"); + // widget.selectComponent("qt.qt5.5130.qtscript"); + // widget.selectComponent("qt.qt5.5130.qtscript.gcc_64"); + // widget.selectComponent("qt.qt5.5130.qtwebengine"); + // widget.selectComponent("qt.qt5.5130.qtwebengine.gcc_64"); + // widget.selectComponent("qt.qt5.5130.qtwebglplugin"); + // widget.selectComponent("qt.qt5.5130.qtwebglplugin.gcc_64"); // widget.selectComponent("qt.tools"); gui.clickButton(buttons.NextButton); From bbba301f852e05ef174bb7d5d4f87fb16f06293f Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 10:25:48 +0200 Subject: [PATCH 247/293] [cmake] Add build of qtAliceVision --- CMakeLists.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index acae6127ad..09a813f21a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ set(QT_DIR "$ENV{QT_DIR}" CACHE STRING "Qt root directory") option(MR_BUILD_QTOIIO "Enable building of QtOIIO plugin" ON) option(MR_BUILD_QMLALEMBIC "Enable building of qmlAlembic plugin" ON) +option(MR_BUILD_QTALICEVISION "Enable building of qtAliceVision plugin" ON) if(CMAKE_BUILD_TYPE MATCHES Release) message(STATUS "Force CMAKE_INSTALL_DO_STRIP in Release") @@ -74,3 +75,18 @@ ExternalProject_Add(${QMLALEMBIC_TARGET} ) endif() +if(MR_BUILD_QTALICEVISION) +set(QTALICEVISION_TARGET qtAliceVision) +ExternalProject_Add(${QTALICEVISION_TARGET} + GIT_REPOSITORY https://github.com/alicevision/qtAliceVision + GIT_TAG develop + PREFIX ${BUILD_DIR} + BUILD_IN_SOURCE 0 + BUILD_ALWAYS 0 + SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtAliceVision + BINARY_DIR ${BUILD_DIR}/qtAliceVision_build + INSTALL_DIR ${CMAKE_INSTALL_PREFIX} + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} ${QT_CMAKE_FLAGS} -DCMAKE_INSTALL_PREFIX:PATH= + ) +endif() + From 2663ab472939afa6431197ee606145d64b8beb45 Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Wed, 7 Aug 2019 15:15:22 +0200 Subject: [PATCH 248/293] [nodes][aliceVision] hdr: change input to accept list of folders --- meshroom/nodes/aliceVision/LDRToHDR.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 5952986546..6fff763357 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -7,13 +7,18 @@ class LDRToHDR(desc.CommandLineNode): commandLine = 'aliceVision_convertLDRToHDR {allParams}' inputs = [ - desc.File( - name='input', - label='Input', - description="List of LDR images or a folder containing them ", + desc.ListAttribute( + elementDesc=desc.File( + name='inputFolder', + label='Input File/Folder', + description="Folder containing LDR images", value='', uid=[0], ), + name="input", + label="Input Files or Folders", + description='Folders containing LDR images.', + ), desc.ChoiceParam( name='calibrationMethod', label='Calibration Method', From 158f02601978c40e24808a254c2a735b5a43bb6b Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Wed, 7 Aug 2019 15:17:04 +0200 Subject: [PATCH 249/293] [nodes][aliceVision] hdr: add bool parameter for fisheye lenses --- meshroom/nodes/aliceVision/LDRToHDR.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 6fff763357..f1c0f07fe0 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -19,6 +19,13 @@ class LDRToHDR(desc.CommandLineNode): label="Input Files or Folders", description='Folders containing LDR images.', ), + desc.BoolParam( + name='fisheyeLens', + label='Fisheye Lens', + description="Check if fisheye lens", + value=True, + uid=[0], + ), desc.ChoiceParam( name='calibrationMethod', label='Calibration Method', From 6d377e1b7d07aa6dec7ccda20684ec8ce717d07c Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Wed, 7 Aug 2019 15:19:43 +0200 Subject: [PATCH 250/293] [nodes][aliceVision] hdr: change descriptions and label parameters --- meshroom/nodes/aliceVision/LDRToHDR.py | 62 +++++++++++++++----------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index f1c0f07fe0..2dc1e51fa3 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -12,9 +12,9 @@ class LDRToHDR(desc.CommandLineNode): name='inputFolder', label='Input File/Folder', description="Folder containing LDR images", - value='', - uid=[0], - ), + value='', + uid=[0], + ), name="input", label="Input Files or Folders", description='Folders containing LDR images.', @@ -33,62 +33,62 @@ class LDRToHDR(desc.CommandLineNode): " * linear \n" " * robertson \n" " * debevec \n" - " * beta: grossberg", + " * grossberg", values=['linear', 'robertson', 'debevec', 'grossberg'], value='linear', exclusive=True, uid=[0], - ), + ), desc.File( name='inputResponse', label='Input Response', description="external camera response file path to fuse all LDR images together.", value='', uid=[0], - ), + ), desc.StringParam( name='targetExposureImage', label='Target Exposure Image', - description="LDR image at the target exposure for the output HDR image to be centered.", + description="LDR image(s) name(s) at the target exposure for the output HDR image(s) to be centered.", value='', uid=[0], - ), + ), desc.ChoiceParam( name='calibrationWeight', label='Calibration Weight', - description="Weight function type (default, gaussian, triangle, plateau).", + description="Weight function used to calibrate camera response \n" + " * default \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 (gaussian, triangle, plateau).", + 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.FloatParam( - name='oversaturatedCorrection', - label='Oversaturated Correction', - description="Oversaturated correction for pixels oversaturated in all images: \n" + name='expandDynamicRange', + label='Expand Dynamic Range', + description="Correction of clamped high values in dynamic range: \n" " - use 0 for no correction \n" " - use 0.5 for interior lighting \n" " - use 1 for outdoor lighting", value=1, range=(0, 1, 0.1), uid=[0], - ), - desc.File( - name='recoverPath', - label='Recover Path', - description="Path to write recovered LDR image at the target exposure by applying inverse response on HDR image.", - value='', - uid=[0], - ), + ), desc.ChoiceParam( name='verboseLevel', label='Verbose Level', @@ -97,7 +97,15 @@ class LDRToHDR(desc.CommandLineNode): values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'], exclusive=True, uid=[], - ), + ), + desc.File( + name='recoverPath', + label='Output Recovered Files', + description="(debug) Folder for recovered LDR images at target exposures.", + advanced=True, + value='', + uid=[], + ), ] outputs = [ @@ -107,12 +115,12 @@ class LDRToHDR(desc.CommandLineNode): description="Output HDR image path.", value=desc.Node.internalFolder + 'hdr.exr', uid=[], - ), + ), desc.File( name='outputResponse', label='Output Response', description="Output response function path.", - value=desc.Node.internalFolder + 'response.ods', + value=desc.Node.internalFolder + 'response.csv', uid=[], - ), + ), ] From fe158917f102e0a0dea150c1e11e071a56a75045 Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Wed, 7 Aug 2019 15:21:57 +0200 Subject: [PATCH 251/293] [nodes][aliceVision] hdr: change output parameter from path to folder --- meshroom/nodes/aliceVision/LDRToHDR.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 2dc1e51fa3..2ea303eda2 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -111,9 +111,9 @@ class LDRToHDR(desc.CommandLineNode): outputs = [ desc.File( name='output', - label='Output', - description="Output HDR image path.", - value=desc.Node.internalFolder + 'hdr.exr', + label='Output Folder', + description="Output folder for HDR images", + value=desc.Node.internalFolder, uid=[], ), desc.File( From 6bb89397f309e141a218fddb39676321c4c31860 Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Wed, 7 Aug 2019 15:27:41 +0200 Subject: [PATCH 252/293] [nodes][aliceVision] hdr: delete forgotten quotes --- meshroom/nodes/aliceVision/LDRToHDR.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index 2ea303eda2..f859e30d02 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -59,7 +59,7 @@ class LDRToHDR(desc.CommandLineNode): description="Weight function used to calibrate camera response \n" " * default \n" " * gaussian \n" - " * triangle \n"" + " * triangle \n" " * plateau", value='default', values=['default', 'gaussian', 'triangle', 'plateau'], @@ -69,7 +69,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", From 5ef5efcb83929e3d37c96a917c965db958f6b642 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 19:20:43 +0200 Subject: [PATCH 253/293] [cmake] use CMAKE_PREFIX_PATH for Qt and AliceVision --- CMakeLists.txt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09a813f21a..45404a0f19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,6 @@ set(ALEMBIC_CMAKE_FLAGS -DAlembic_DIR:PATH=${ALICEVISION_ROOT}/lib64/cmake/Alembic -DILMBASE_ROOT=${ALICEVISION_ROOT} ) -set(QT_CMAKE_FLAGS -DCMAKE_PREFIX_PATH=${QT_DIR}) include(ExternalProject) @@ -45,7 +44,7 @@ include(GNUInstallDirs) # message(STATUS "QT_CMAKE_FLAGS: ${QT_CMAKE_FLAGS}") if(MR_BUILD_QTOIIO) -set(QTOIIO_TARGET qtoiio) +set(QTOIIO_TARGET qtOIIO) ExternalProject_Add(${QTOIIO_TARGET} GIT_REPOSITORY https://github.com/alicevision/QtOIIO GIT_TAG develop @@ -55,13 +54,13 @@ ExternalProject_Add(${QTOIIO_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtoiio BINARY_DIR ${BUILD_DIR}/qtoiio_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${OIIO_CMAKE_FLAGS} ${QT_CMAKE_FLAGS} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${OIIO_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() if(MR_BUILD_QMLALEMBIC) -set(QMLALEMBIC_TARGET qmlalembic) +set(QMLALEMBIC_TARGET qmlAlembic) ExternalProject_Add(${QMLALEMBIC_TARGET} GIT_REPOSITORY https://github.com/alicevision/qmlAlembic GIT_TAG develop @@ -71,7 +70,7 @@ ExternalProject_Add(${QMLALEMBIC_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qmlalembic BINARY_DIR ${BUILD_DIR}/qmlalembic_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} ${QT_CMAKE_FLAGS} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() @@ -86,7 +85,7 @@ ExternalProject_Add(${QTALICEVISION_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtAliceVision BINARY_DIR ${BUILD_DIR}/qtAliceVision_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} ${QT_CMAKE_FLAGS} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR}:${ALICEVISION_ROOT} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() From 30718fbccfe9130216b10381127dacae2d73ea1a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 19:22:54 +0200 Subject: [PATCH 254/293] [docker] need static libs from aliceVision "lib" folder to build qtAliceVision --- Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f615f428d7..093f788343 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,8 @@ ENV MESHROOM_DEV=/opt/Meshroom \ QT_DIR=/opt/qt/5.13.0/gcc_64 \ PATH="${PATH}:${MESHROOM_BUNDLE}" -COPY . "${MESHROOM_DEV}" +# Workaround for qmlAlembic/qtAliceVision builds: fuse lib/lib64 folders +RUN cp -rf "${AV_INSTALL}/lib/*" "${AV_INSTALL}/lib64" && rm -rf "${AV_INSTALL}/lib" && ln -s "${AV_INSTALL}/lib64" "${AV_INSTALL}/lib" # Install libs needed by Qt RUN yum install -y \ @@ -39,6 +40,8 @@ RUN yum install -y \ RUN yum install -y centos-release-scl RUN yum install -y rh-python36 +COPY . "${MESHROOM_DEV}" + # Install Meshroom requirements and freeze bundle RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ find ${MESHROOM_BUNDLE} -name "*Qt5Web*" -delete && \ @@ -59,11 +62,12 @@ RUN curl -LO http://download.qt.io/official_releases/online_installers/qt-unifie rm ./qt-unified-linux-x64-online.run WORKDIR ${MESHROOM_BUILD} -# Temporary workaround for qmlAlembic build -RUN rm -rf "${AV_INSTALL}/lib" && ln -s "${AV_INSTALL}/lib64" "${AV_INSTALL}/lib" # Build Meshroom plugins RUN cmake "${MESHROOM_DEV}" -DALICEVISION_ROOT="${AV_INSTALL}" -DQT_DIR="${QT_DIR}" -DCMAKE_INSTALL_PREFIX="${MESHROOM_BUNDLE}/qtPlugins" +# RUN make -j8 qtOIIO +# RUN make -j8 qmlAlembic +# RUN make -j8 qtAliceVision RUN make -j8 && cd /tmp && rm -rf ${MESHROOM_BUILD} RUN mv "${AV_BUNDLE}" "${MESHROOM_BUNDLE}/aliceVision" From ddc5b2909467c248ddb0124fb51e7e4f0681d657 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 20:05:38 +0200 Subject: [PATCH 255/293] [bin] meshroom_photogrammetry: args.input is optional --- bin/meshroom_photogrammetry | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 1d27d3a3cf..e61132e27b 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -73,15 +73,16 @@ views, intrinsics = [], [] # Build image files list from inputImages arguments images = [f for f in args.inputImages if multiview.isImageFile(f)] -if os.path.isdir(args.input): - # args.input is a folder: extend images list with images in that folder - images += multiview.findImageFiles(args.input) -elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json', '.sfm'): - # args.input is a sfmData file: setup pre-calibrated views and intrinsics - from meshroom.nodes.aliceVision.CameraInit import readSfMData - views, intrinsics = readSfMData(args.input) -else: - raise RuntimeError(args.input + ': format not supported') +if args.input: + if os.path.isdir(args.input): + # args.input is a folder: extend images list with images in that folder + images += multiview.findImageFiles(args.input) + elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json', '.sfm'): + # args.input is a sfmData file: setup pre-calibrated views and intrinsics + from meshroom.nodes.aliceVision.CameraInit import readSfMData + views, intrinsics = readSfMData(args.input) + else: + raise RuntimeError(args.input + ': format not supported.') # initialize photogrammetry pipeline if args.pipeline: From 177d47f95fafdebc2bb68314ccefae20561235ae Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 7 Aug 2019 20:06:48 +0200 Subject: [PATCH 256/293] [bin] meshroom_photogrammetry: rebuild intrinsics only if we have new input images --- bin/meshroom_photogrammetry | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index e61132e27b..dae5183b07 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -107,9 +107,10 @@ else: graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output) cameraInit = getOnlyNodeOfType(graph, 'CameraInit') -views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) -cameraInit.viewpoints.value = views -cameraInit.intrinsics.value = intrinsics +if images: + views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images) + cameraInit.viewpoints.value = views + cameraInit.intrinsics.value = intrinsics if args.overrides: import io From e5f6247d6bcaff5955c1e0ba26de85cc346eb93b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 11:05:01 +0200 Subject: [PATCH 257/293] [docker] fix paths --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 093f788343..3de3bb2788 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV MESHROOM_DEV=/opt/Meshroom \ PATH="${PATH}:${MESHROOM_BUNDLE}" # Workaround for qmlAlembic/qtAliceVision builds: fuse lib/lib64 folders -RUN cp -rf "${AV_INSTALL}/lib/*" "${AV_INSTALL}/lib64" && rm -rf "${AV_INSTALL}/lib" && ln -s "${AV_INSTALL}/lib64" "${AV_INSTALL}/lib" +RUN cp -rf ${AV_INSTALL}/lib/* ${AV_INSTALL}/lib64 && rm -rf ${AV_INSTALL}/lib && ln -s ${AV_INSTALL}/lib64 ${AV_INSTALL}/lib # Install libs needed by Qt RUN yum install -y \ From 2881b712e60085052f99070f6a0d63d4b7921ec4 Mon Sep 17 00:00:00 2001 From: Anouk Liberge Date: Thu, 8 Aug 2019 11:23:34 +0200 Subject: [PATCH 258/293] [nodes][aliceVision] hdr: clarify descriptions --- meshroom/nodes/aliceVision/LDRToHDR.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meshroom/nodes/aliceVision/LDRToHDR.py b/meshroom/nodes/aliceVision/LDRToHDR.py index f859e30d02..ad36c17d85 100644 --- a/meshroom/nodes/aliceVision/LDRToHDR.py +++ b/meshroom/nodes/aliceVision/LDRToHDR.py @@ -22,7 +22,9 @@ class LDRToHDR(desc.CommandLineNode): desc.BoolParam( name='fisheyeLens', label='Fisheye Lens', - description="Check if 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=True, uid=[0], ), @@ -57,7 +59,7 @@ class LDRToHDR(desc.CommandLineNode): name='calibrationWeight', label='Calibration Weight', description="Weight function used to calibrate camera response \n" - " * default \n" + " * default (automatically selected according to the calibrationMethod) \n" " * gaussian \n" " * triangle \n" " * plateau", @@ -70,7 +72,7 @@ class LDRToHDR(desc.CommandLineNode): name='fusionWeight', label='Fusion Weight', description="Weight function used to fuse all LDR images together \n" - " * gaussian \n" + " * gaussian \n" " * triangle \n" " * plateau", value='gaussian', From 15192e2aad3dc4d9a34413901d763a91dfd093c3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 13:07:39 +0200 Subject: [PATCH 259/293] [cmake] workaround for ExternalProject_Add list arguments --- CMakeLists.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 45404a0f19..7d6996d322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,7 @@ ExternalProject_Add(${QTOIIO_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtoiio BINARY_DIR ${BUILD_DIR}/qtoiio_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${OIIO_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${OIIO_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH:PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() @@ -70,7 +70,7 @@ ExternalProject_Add(${QMLALEMBIC_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qmlalembic BINARY_DIR ${BUILD_DIR}/qmlalembic_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH:PATH=${QT_DIR} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() @@ -85,7 +85,9 @@ ExternalProject_Add(${QTALICEVISION_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtAliceVision BINARY_DIR ${BUILD_DIR}/qtAliceVision_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH=${QT_DIR}:${ALICEVISION_ROOT} -DCMAKE_INSTALL_PREFIX:PATH= + LIST_SEPARATOR , # ExternalProject_Add preprocess the CONFIGURE_COMMAND and replaces the usual ";" list separator, so we use another one. + # See https://stackoverflow.com/questions/45414507/pass-a-list-of-prefix-paths-to-externalproject-add-in-cmake-args + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH:PATH=${QT_DIR},${ALICEVISION_ROOT} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() From 13f3715528065e6325aac4cb074f44f416058227 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 13:58:04 +0200 Subject: [PATCH 260/293] [cmake] another workaround --- CMakeLists.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d6996d322..2625faa6b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,9 +85,7 @@ ExternalProject_Add(${QTALICEVISION_TARGET} SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/qtAliceVision BINARY_DIR ${BUILD_DIR}/qtAliceVision_build INSTALL_DIR ${CMAKE_INSTALL_PREFIX} - LIST_SEPARATOR , # ExternalProject_Add preprocess the CONFIGURE_COMMAND and replaces the usual ";" list separator, so we use another one. - # See https://stackoverflow.com/questions/45414507/pass-a-list-of-prefix-paths-to-externalproject-add-in-cmake-args - CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH:PATH=${QT_DIR},${ALICEVISION_ROOT} -DCMAKE_INSTALL_PREFIX:PATH= + CONFIGURE_COMMAND ${CMAKE_COMMAND} ${CMAKE_CORE_BUILD_FLAGS} ${ALEMBIC_CMAKE_FLAGS} -DCMAKE_PREFIX_PATH:PATH=${QT_DIR}$${ALICEVISION_ROOT} -DCMAKE_INSTALL_PREFIX:PATH= ) endif() From a0c0fc6af4d85f7e9e5381fbf235ba76660df55f Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 8 Aug 2019 16:26:56 +0200 Subject: [PATCH 261/293] [github] add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..fa26aa59fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[bug]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Log** +If applicable, copy paste the relevant log output (please embed the text in a markdown code tag "```" ) + +**Desktop (please complete the following and other pertinent information):** + - OS: [e.g. win 10, osx, ] + - Version [e.g. 2019.1] + - Python version [e.g. 2.6] + - Qt/PySide version [e.g. 5.12.4] + - Binary version (if applicable) [e.g. 2019.1] + - Commit reference (if applicable) [e.g. 08ddbe2] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..52683c446f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[request]" +labels: feature request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 9a53e328e1fc40a5cbe6199e239b404e27f6c546 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 8 Aug 2019 16:31:50 +0200 Subject: [PATCH 262/293] [github] add pr template --- .github/pull_request_template.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..de3799adea --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + +## Description + + + +## Features list + + + + +## Implementation remarks + + + From a0baa5b3f2e0361faf39bbc4749fb924a8e907b8 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 8 Aug 2019 16:36:47 +0200 Subject: [PATCH 263/293] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..06cbb2f8a1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team privately at alicevision-team@googlegroups.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ + +[homepage]: https://www.contributor-covenant.org From 8ab46e29ff427262777f9198ab03055dfeff09bd Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 8 Aug 2019 16:43:35 +0200 Subject: [PATCH 264/293] Create CONTRIBUTING.md --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..543230a65b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +Contributing to Meshroom +=========================== + +Meshroom relies on a friendly and community-driven effort to create an open source photogrammetry solution. +In order to foster a friendly atmosphere where technical collaboration can flourish, +we recommend you to read the [code of conduct](CODE_OF_CONDUCT.md). + + +Contributing Workflow +--------------------- + +The contributing workflow relies on [Github Pull Requests](https://help.github.com/articles/using-pull-requests/). + +1. If it is an important change, we recommend you to discuss it on the mailing-list +before starting implementation. This ensure that the development is aligned with other +developpements already started and will be efficiently integrated. + +2. Create the corresponding issues. + +3. Create a branch and [draft a pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) "My new feature" so everyone can follow the development. +Explain the implementation in the PR description with links to issues. + +4. Implement the new feature(s). Add unit test if needed. +One feature per PR is ideal for review, but linked features can be part of the same PR. + +5. When it is ready for review, [mark the pull request as ready for review](https://help.github.com/en/articles/changing-the-stage-of-a-pull-request). + +6. The reviewers will look over the code and ask for changes, explain problems they found, +congratulate the author, etc. using the github comments. + +7. After approval, one of the developers with commit approval to the official main repository +will merge your fixes into the "develop" branch. + +8. If not already the case, your name will be added to the [contributors list](CONTRIBUTORS.md). From ec1a4bee1456018c5e808ed1b8d2bcd4265876d9 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Thu, 8 Aug 2019 17:02:10 +0200 Subject: [PATCH 265/293] [doc] add cii badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index de4225d468..6fc97709a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ![Meshroom - 3D Reconstruction Software](/docs/logo/banner-meshroom.png) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2997/badge)](https://bestpractices.coreinfrastructure.org/projects/2997) + Meshroom is a free, open-source 3D Reconstruction Software based on the [AliceVision](https://github.com/alicevision/AliceVision) Photogrammetric Computer Vision framework. Learn more details about the pipeline on [AliceVision website](http://alicevision.github.io). From f859f8759dfffaf39f760374aef09734042ff9ae Mon Sep 17 00:00:00 2001 From: Pramukta Kumar Date: Sun, 11 Aug 2019 09:01:34 -0500 Subject: [PATCH 266/293] add option to include 'unknown' feature types in order to support the conversion of dense point clouds via ConvertSfMFormat --- meshroom/nodes/aliceVision/ConvertSfMFormat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/nodes/aliceVision/ConvertSfMFormat.py b/meshroom/nodes/aliceVision/ConvertSfMFormat.py index 4d009da209..5260a59b79 100644 --- a/meshroom/nodes/aliceVision/ConvertSfMFormat.py +++ b/meshroom/nodes/aliceVision/ConvertSfMFormat.py @@ -30,7 +30,7 @@ class ConvertSfMFormat(desc.CommandLineNode): label='Describer Types', description='Describer types to keep.', value=['sift'], - values=['sift', 'sift_float', 'sift_upright', 'akaze', 'akaze_liop', 'akaze_mldb', 'cctag3', 'cctag4', 'sift_ocv', 'akaze_ocv'], + values=['sift', 'sift_float', 'sift_upright', 'akaze', 'akaze_liop', 'akaze_mldb', 'cctag3', 'cctag4', 'sift_ocv', 'akaze_ocv', 'unknown'], exclusive=False, uid=[0], joinChar=',', From 606cfc39c2f0665308d9ce7101270e5007432be6 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Mon, 12 Aug 2019 14:27:20 +0200 Subject: [PATCH 267/293] [ui] Viewer3D: fix Alembic visibility issues Binding the "enabled" property of AlembicLoader's ObjectPicker to the parent MediaLoader's own "enabled" property caused invalid state where the loaded AlembicEntity was partially visible when toggling object visibility (since Qt 5.13). In order to disable camera picking when AlembicEntity is disable, scale the camSelector component to 0. --- meshroom/ui/qml/Viewer3D/AlembicLoader.qml | 11 ++++++++--- meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 2 +- meshroom/ui/qml/Viewer3D/MediaLoader.qml | 3 +-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml index dba8096188..2b2fd74c12 100644 --- a/meshroom/ui/qml/Viewer3D/AlembicLoader.qml +++ b/meshroom/ui/qml/Viewer3D/AlembicLoader.qml @@ -11,6 +11,7 @@ import Qt3D.Extras 2.1 AlembicEntity { id: root + property bool cameraPickingEnabled: true // filter out non-reconstructed cameras skipHidden: true @@ -52,9 +53,13 @@ AlembicEntity { }, ObjectPicker { id: cameraPicker - enabled: root.enabled - onClicked: _reconstruction.selectedViewId = camSelector.viewId - } + onPressed: pick.accepted = cameraPickingEnabled + onReleased: _reconstruction.selectedViewId = camSelector.viewId + }, + // Qt 5.13: binding cameraPicker.enabled to cameraPickerEnabled + // causes rendering issues when entity gets disabled. + // Use a scale to 0 to disable picking. + Transform { scale: cameraPickingEnabled ? 1 : 0 } ] } } diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index dd3ec84e1b..56af186674 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -275,7 +275,7 @@ Entity { components: [ ObjectPicker { - enabled: parent.enabled && pickingEnabled + enabled: mediaLoader.enabled && pickingEnabled hoverEnabled: false onPressed: root.pressed(pick) } diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index 9cf51ddd6a..936197a09f 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -80,14 +80,13 @@ import Utils 1.0 id: abcLoaderEntityComponent MediaLoaderEntity { id: abcLoaderEntity - enabled: root.enabled Component.onCompleted: { var obj = Viewer3DSettings.abcLoaderComp.createObject(abcLoaderEntity, { 'source': source, 'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }), 'locatorScale': Qt.binding(function() { return Viewer3DSettings.cameraScale }), - 'enabled': Qt.binding(function() { return root.enabled }) + 'cameraPickingEnabled': Qt.binding(function() { return root.enabled }) }); obj.statusChanged.connect(function() { From 8a3d8fa0ad7e38102a4555a648b2609ac28a13f1 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 25 Jul 2019 21:01:44 +0200 Subject: [PATCH 268/293] [build] update PySide2 version in requirements.txt bump to 5.13.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 20a6a62700..a0994cf01c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # runtime psutil enum34;python_version<"3.4" -PySide2==5.11.1 +PySide2==5.13.0 markdown==2.6.11 From 66064dedb6a352554644efac8af66d9ab8c4018b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 16:56:32 +0200 Subject: [PATCH 269/293] [docker] new version of Qt has no more lib duplicates --- Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3de3bb2788..aac479ac42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,12 +46,9 @@ COPY . "${MESHROOM_DEV}" RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ find ${MESHROOM_BUNDLE} -name "*Qt5Web*" -delete && \ find ${MESHROOM_BUNDLE} -name "*Qt5Designer*" -delete && \ - rm ${MESHROOM_BUNDLE}/lib/PySide2/libclang.so* && \ rm -rf ${MESHROOM_BUNDLE}/lib/PySide2/typesystems/ ${MESHROOM_BUNDLE}/lib/PySide2/examples/ ${MESHROOM_BUNDLE}/lib/PySide2/include/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/translations/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/resources/ && \ - rm ${MESHROOM_BUNDLE}/lib/PySide2/libQt5* && \ rm ${MESHROOM_BUNDLE}/lib/PySide2/QtWeb* && \ - rm ${MESHROOM_BUNDLE}/lib/PySide2/libicu* && \ - rm ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-lupdate ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-rcc ${MESHROOM_BUNDLE}/lib/PySide2/shiboken2 + rm ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-lupdate ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-rcc # Install Qt (to build plugins) WORKDIR /tmp/qt From 9c593e5e20f8dec01f95b241066d336c6fbbf192 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 14:38:07 +0200 Subject: [PATCH 270/293] [docker] use AliceVision version in container name --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index aac479ac42..4d960ad6f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG CUDA_TAG=8.0 ARG OS_TAG=7 -FROM alicevision/alicevision:centos${OS_TAG}-cuda${CUDA_TAG} +FROM alicevision/alicevision:2.2.0-centos${OS_TAG}-cuda${CUDA_TAG} LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" # Execute with nvidia docker (https://github.com/nvidia/nvidia-docker/wiki/Installation-(version-2.0)) From 22e1fabebc77abd97887cc694756aac0fe2916b3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 13:26:58 +0200 Subject: [PATCH 271/293] Add release notes for 2019.2 --- CHANGES.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 280838f9c5..c8773c2b6f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,40 @@ For algorithmic changes related to the photogrammetric pipeline, please refer to [AliceVision changelog](https://github.com/alicevision/AliceVision/blob/develop/CHANGES.md). + +## Release 2019.2.0 (2019.08.08) + +Based on [AliceVision 2.2.0](https://github.com/alicevision/AliceVision/tree/v2.2.0). + +Release Notes Summary: + + - Visualisation: New visualization module of the features extraction. [PR](https://github.com/alicevision/meshroom/pull/539), [New QtAliceVision](https://github.com/alicevision/QtAliceVision) + - Support for RAW image files. + - Texturing: Largely improve the Texturing quality. + - Texturing: Speed improvements. + - Texturing: Add support for UDIM. + - Meshing: Export the dense point cloud in Alembic. + - Meshing: New option to export the full raw dense point cloud (with all 3D points candidates before cut and filtering). + - Meshing: Adds an option to export color data per vertex and MeshFiltering correctly preserves colors. + +Full Release Notes: + + - Move to PySide2 / Qt 5.13 + - SfMDataIO: Change root nodes (XForms instead of untyped objects) of Alembic SfMData for better interoperability with other 3D graphics applications (in particular Blender and Houdini). + - Improve performance of log display and node status update. [PR](https://github.com/alicevision/meshroom/pull/466) [PR](https://github.com/alicevision/meshroom/pull/548) + - Viewer3D: Add support for vertex-colored meshes. [PR](https://github.com/alicevision/meshroom/pull/550) + - New pipeline input for meshroom_photogrammetry command line and minor fixes to the input arguments. [PR](https://github.com/alicevision/meshroom/pull/567) [PR](https://github.com/alicevision/meshroom/pull/577) + - New arguments to meshroom. [PR](https://github.com/alicevision/meshroom/pull/413) + - HDR: New HDR module for the fusion of multiple LDR images. + - PrepareDenseScene: Add experimental option to correct Exposure Values (EV) of input images to uniformize dataset exposures. + - FeatureExtraction: Include CCTag in the release binaries both on Linux and Windows. + - ConvertSfMFormat: Enable to use simple regular expressions in the image white list of the ConvertSfMFormat. This enables to filter out cameras based on their filename. + +For more details see all PR merged: https://github.com/alicevision/meshroom/milestone/9 +See [AliceVision 2.2.0 Release Notes](https://github.com/alicevision/AliceVision/blob/v2.2.0/CHANGES.md) +for more details about algorithmic changes. + + ## Release 2019.1.0 (2019.02.27) Based on [AliceVision 2.1.0](https://github.com/alicevision/AliceVision/tree/v2.1.0). From 87c74ed56f3655acaefc7c5e660b7697b0e8fea3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 15:14:38 +0200 Subject: [PATCH 272/293] [doc] INSTALL: Add Qt information --- INSTALL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 3fd04ada78..4f4a357c24 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -36,6 +36,11 @@ pip install -r requirements.txt -r dev_requirements.txt ``` > Note: `dev_requirements` is only related to testing and packaging. It is not mandatory to run Meshroom. +### Qt/PySide +* PySide >= 5.12.2 +Warning: On Windows, the plugin AssimpSceneParser is missing from pre-built binaries, so you need to add it manually (from an older version for instance). +See https://bugreports.qt.io/browse/QTBUG-74535 + ### Qt Plugins Additional Qt plugins can be built to extend Meshroom UI features. They can be found on separate repositories, though they might get better integration in the future. From 8d275609e84951ee4151f5d9953487e84f5c9731 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 8 Aug 2019 15:17:15 +0200 Subject: [PATCH 273/293] [doc] INSTALL: add QtAliceVision --- INSTALL.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 4f4a357c24..24756a48db 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -60,3 +60,11 @@ This plugin also provides a QML Qt3D Entity to load depthmaps files stored in EX QT_PLUGIN_PATH=/path/to/QtOIIO/install QML2_IMPORT_PATH=/path/to/QtOIIO/install/qml ``` + +#### [QtAliceVision](https://github.com/alicevision/QtAliceVision) +Use AliceVision to load and visualize intermediate reconstruction files. +``` +QML2_IMPORT_PATH=/path/to/qtAliceVision/install/qml +``` + + From 1fce38a128281b181499ed61c5259c8220b00810 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 25 Jul 2019 21:02:36 +0200 Subject: [PATCH 274/293] [release] Update Meshroom version to 2019.2.0 --- meshroom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/__init__.py b/meshroom/__init__.py index 2a2fc4bb67..7bda50a187 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2019.1.0" +__version__ = "2019.2.0" __version_name__ = __version__ import os From 3e483ea1393b89ba56d366888fcc01d686a54002 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 14 Aug 2019 15:22:37 +0200 Subject: [PATCH 275/293] [release] Update multiview pipeline version to 2.2 --- meshroom/multiview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index a6f8be382a..1474b222d2 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -1,5 +1,5 @@ # Multiview pipeline version -__version__ = "2.1" +__version__ = "2.2" import os From 59da35095a25d3b08589c84149e52a8a2f480f50 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 16 Aug 2019 17:17:49 +0200 Subject: [PATCH 276/293] [doc] Minor fix to CONTRIBUTING --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 543230a65b..e3f4e00f48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,5 +30,3 @@ congratulate the author, etc. using the github comments. 7. After approval, one of the developers with commit approval to the official main repository will merge your fixes into the "develop" branch. - -8. If not already the case, your name will be added to the [contributors list](CONTRIBUTORS.md). From 85aeec4bd8f894f3128aa8f153c6af13c5fa4b67 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Fri, 16 Aug 2019 21:06:09 +0200 Subject: [PATCH 277/293] Added automatic stale detection and closing for issues --- .github/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..03dc195e92 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: false +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue is closed due to inactivity. Feel free to re-open if new information + is available. From d20ce2ece4885f27921078e4d177d0d66a18245e Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Fri, 16 Aug 2019 21:55:38 +0200 Subject: [PATCH 278/293] [github] set daysUntilStale to 120 --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 03dc195e92..e33f1ed97b 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,5 +1,5 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 +daysUntilStale: 120 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale From 9d8e3648b2ab3b4740b75ccc54d06d33907e51ba Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 21 Aug 2019 21:36:11 +0200 Subject: [PATCH 279/293] [core] submitters: if there is only one submitter, use it! More explicit error messages when the submitter is not found. --- meshroom/core/graph.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 858820ef02..0427b33fd5 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1150,9 +1150,15 @@ def submitGraph(graph, submitter, toNodes=None): logging.info("Nodes to process: {}".format(edgesToProcess)) logging.info("Edges to process: {}".format(edgesToProcess)) - sub = meshroom.core.submitters.get(submitter, None) + sub = None + if submitter: + sub = meshroom.core.submitters.get(submitter, None) + elif len(meshroom.core.submitters) == 1: + # if only one submitter available use it + sub = meshroom.core.submitters.values()[0] if sub is None: - raise RuntimeError("Unknown Submitter : " + submitter) + raise RuntimeError("Unknown Submitter: '{submitter}'. Available submitters are: '{allSubmitters}'.".format( + submitter=submitter, allSubmitters=str(meshroom.core.submitters.keys()))) try: res = sub.submit(nodesToProcess, edgesToProcess, graph.filepath) From c07750b5386e6675b3d21ed1a8e15937e7304c24 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 21 Aug 2019 21:41:45 +0200 Subject: [PATCH 280/293] [docker] minor simplification --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4d960ad6f3..144f80585d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,13 +37,14 @@ RUN yum install -y \ xcb-util-image # Install Python3 -RUN yum install -y centos-release-scl -RUN yum install -y rh-python36 +RUN yum install -y centos-release-scl && yum install -y rh-python36 COPY . "${MESHROOM_DEV}" +WORKDIR "${MESHROOM_DEV}" + # Install Meshroom requirements and freeze bundle -RUN source scl_source enable rh-python36 && cd "${MESHROOM_DEV}" && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ +RUN source scl_source enable rh-python36 && pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ find ${MESHROOM_BUNDLE} -name "*Qt5Web*" -delete && \ find ${MESHROOM_BUNDLE} -name "*Qt5Designer*" -delete && \ rm -rf ${MESHROOM_BUNDLE}/lib/PySide2/typesystems/ ${MESHROOM_BUNDLE}/lib/PySide2/examples/ ${MESHROOM_BUNDLE}/lib/PySide2/include/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/translations/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/resources/ && \ From 8ba80fcb91f2ba76e71698c4efe3d50d5a68164d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 21 Aug 2019 21:43:01 +0200 Subject: [PATCH 281/293] [docker] add dockerfile for python2 --- Dockerfile_py2 | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 Dockerfile_py2 diff --git a/Dockerfile_py2 b/Dockerfile_py2 new file mode 100644 index 0000000000..2641ee0ce7 --- /dev/null +++ b/Dockerfile_py2 @@ -0,0 +1,74 @@ +ARG CUDA_TAG=8.0 +ARG OS_TAG=7 +FROM alicevision/alicevision:2.2.0-centos${OS_TAG}-cuda${CUDA_TAG} +LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" + +# Execute with nvidia docker (https://github.com/nvidia/nvidia-docker/wiki/Installation-(version-2.0)) +# docker run -it --runtime=nvidia meshroom + +ENV MESHROOM_DEV=/opt/Meshroom \ + MESHROOM_BUILD=/tmp/Meshroom_build \ + MESHROOM_BUNDLE=/opt/Meshroom_bundle \ + QT_DIR=/opt/qt/5.13.0/gcc_64 \ + PATH="${PATH}:${MESHROOM_BUNDLE}" + +# Workaround for qmlAlembic/qtAliceVision builds: fuse lib/lib64 folders +RUN cp -rf ${AV_INSTALL}/lib/* ${AV_INSTALL}/lib64 && rm -rf ${AV_INSTALL}/lib && ln -s ${AV_INSTALL}/lib64 ${AV_INSTALL}/lib + +# Install libs needed by Qt +RUN yum install -y \ + flex \ + fontconfig \ + freetype \ + glib2 \ + libICE \ + libX11 \ + libxcb \ + libXext \ + libXi \ + libXrender \ + libSM \ + libXt-devel \ + libGLU-devel \ + mesa-libOSMesa-devel \ + mesa-libGL-devel \ + mesa-libGLU-devel \ + xcb-util-keysyms \ + xcb-util-image + +# Install Python2 +RUN yum install -y python-devel && curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py && python /tmp/get-pip.py + +COPY . "${MESHROOM_DEV}" + +WORKDIR "${MESHROOM_DEV}" + +# Install Meshroom requirements and freeze bundle +RUN pip install -r dev_requirements.txt -r requirements.txt && python setup.py install_exe -d "${MESHROOM_BUNDLE}" && \ + find ${MESHROOM_BUNDLE} -name "*Qt5Web*" -delete && \ + find ${MESHROOM_BUNDLE} -name "*Qt5Designer*" -delete && \ + rm -rf ${MESHROOM_BUNDLE}/lib/PySide2/typesystems/ ${MESHROOM_BUNDLE}/lib/PySide2/examples/ ${MESHROOM_BUNDLE}/lib/PySide2/include/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/translations/ ${MESHROOM_BUNDLE}/lib/PySide2/Qt/resources/ && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/QtWeb* && \ + rm ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-lupdate ${MESHROOM_BUNDLE}/lib/PySide2/pyside2-rcc + +# Install Qt (to build plugins) +WORKDIR /tmp/qt +# Qt version in specified in docker/qt-installer-noninteractive.qs +RUN curl -LO http://download.qt.io/official_releases/online_installers/qt-unified-linux-x64-online.run && \ + chmod u+x qt-unified-linux-x64-online.run && \ + ./qt-unified-linux-x64-online.run --verbose --platform minimal --script "${MESHROOM_DEV}/docker/qt-installer-noninteractive.qs" && \ + rm ./qt-unified-linux-x64-online.run + +WORKDIR ${MESHROOM_BUILD} + +# Build Meshroom plugins +RUN cmake "${MESHROOM_DEV}" -DALICEVISION_ROOT="${AV_INSTALL}" -DQT_DIR="${QT_DIR}" -DCMAKE_INSTALL_PREFIX="${MESHROOM_BUNDLE}/qtPlugins" +# RUN make -j8 qtOIIO +# RUN make -j8 qmlAlembic +# RUN make -j8 qtAliceVision +RUN make -j8 && cd /tmp && rm -rf ${MESHROOM_BUILD} + +RUN mv "${AV_BUNDLE}" "${MESHROOM_BUNDLE}/aliceVision" +RUN rm -rf ${MESHROOM_BUNDLE}/aliceVision/share/doc ${MESHROOM_BUNDLE}/aliceVision/share/eigen3 ${MESHROOM_BUNDLE}/aliceVision/share/fonts ${MESHROOM_BUNDLE}/aliceVision/share/lemon ${MESHROOM_BUNDLE}/aliceVision/share/libraw ${MESHROOM_BUNDLE}/aliceVision/share/man/ aliceVision/share/pkgconfig + + From 18e16811f43bf57fcb78ce5cb328ec2f320bd3c5 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 21 Aug 2019 21:46:35 +0200 Subject: [PATCH 282/293] [cxFreeze] bundle more context files --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3a132728a3..4bbe3df2fb 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def __init__(self, script, initScript=None, base=None, targetName=None, icons=No build_exe_options = { # include dynamically loaded plugins "packages": ["meshroom.nodes", "meshroom.submitters"], - "include_files": ['COPYING.md'] + "include_files": ["CHANGES.md", "COPYING.md", "LICENSE-MPL2.md", "README.md"] } if platform.system() == PlatformExecutable.Linux: From e5c40a9b7f69a3634edc93485d8fd47b3725e894 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 23 Aug 2019 14:42:53 +0200 Subject: [PATCH 283/293] [docker] go back to cuda-7.0 for compatibility --- Dockerfile | 2 +- Dockerfile_py2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 144f80585d..4de93473f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG CUDA_TAG=8.0 +ARG CUDA_TAG=7.0 ARG OS_TAG=7 FROM alicevision/alicevision:2.2.0-centos${OS_TAG}-cuda${CUDA_TAG} LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" diff --git a/Dockerfile_py2 b/Dockerfile_py2 index 2641ee0ce7..be08aa93af 100644 --- a/Dockerfile_py2 +++ b/Dockerfile_py2 @@ -1,4 +1,4 @@ -ARG CUDA_TAG=8.0 +ARG CUDA_TAG=7.0 ARG OS_TAG=7 FROM alicevision/alicevision:2.2.0-centos${OS_TAG}-cuda${CUDA_TAG} LABEL maintainer="AliceVision Team alicevision-team@googlegroups.com" From d59861f13ec425ca769baa8c7836f0a4ec628bdc Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 9 Sep 2019 16:49:35 +0200 Subject: [PATCH 284/293] [docker] minor fix for python-2 --- Dockerfile_py2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile_py2 b/Dockerfile_py2 index be08aa93af..4e277848db 100644 --- a/Dockerfile_py2 +++ b/Dockerfile_py2 @@ -37,7 +37,7 @@ RUN yum install -y \ xcb-util-image # Install Python2 -RUN yum install -y python-devel && curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py && python /tmp/get-pip.py +RUN yum install -y python-devel && curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py && python /tmp/get-pip.py && pip install --upgrade pip COPY . "${MESHROOM_DEV}" From 4d7ea327216fd4b9fd4af3cb4cebd84fb5b6f539 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 12:19:40 +0200 Subject: [PATCH 285/293] [core] stats bugfix: do not rely the ordering of the json entries * do not rely on the ordering of the json entries, as it can vary from one version to another. * ensure variables are always initialized (even in case of exception) * add some debug logging on errors in nvidia-smi parsing --- meshroom/core/stats.py | 52 ++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index b8b270328e..369852333a 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -36,6 +36,8 @@ def __init__(self): self.ramAvailable = 0 # GB self.vramAvailable = 0 # GB self.swapAvailable = 0 + self.gpuMemoryTotal = 0 + self.gpuName = '' if platform.system() == "Windows": # If the platform is Windows and nvidia-smi @@ -51,16 +53,22 @@ def __init__(self): p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) xmlGpu, stdError = p.communicate() - gpuTree = ET.fromstring(xmlGpu) + smiTree = ET.fromstring(xmlGpu) + gpuTree = smiTree.find('gpu') - gpuMemoryUsage = gpuTree[4].find('fb_memory_usage') + try: + self.gpuMemoryTotal = gpuTree.find('fb_memory_usage').find('total').text.split(" ")[0] + except Exception as e: + logging.debug('Failed to get gpuMemoryTotal: "{}".'.format(str(e))) + pass + try: + self.gpuName = gpuTree.find('product_name').text + except Exception as e: + logging.debug('Failed to get gpuName: "{}".'.format(str(e))) + pass - self.gpuMemoryTotal = gpuMemoryUsage[0].text.split(" ")[0] - self.gpuName = gpuTree[4].find('product_name').text - - - except: - pass + except Exception as e: + logging.debug('Failed to get information from nvidia_smi at init: "{}".'.format(str(e))) self.curves = defaultdict(list) @@ -87,13 +95,27 @@ def updateGpu(self): p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) xmlGpu, stdError = p.communicate() - gpuTree = ET.fromstring(xmlGpu) - - self._addKV('gpuMemoryUsed', gpuTree[4].find('fb_memory_usage')[1].text.split(" ")[0]) - self._addKV('gpuUsed', gpuTree[4].find('utilization')[0].text.split(" ")[0]) - self._addKV('gpuTemperature', gpuTree[4].find('temperature')[0].text.split(" ")[0]) - - except: + smiTree = ET.fromstring(xmlGpu) + gpuTree = smiTree.find('gpu') + + try: + self._addKV('gpuMemoryUsed', gpuTree.find('fb_memory_usage').find('used').text.split(" ")[0]) + except Exception as e: + logging.debug('Failed to get gpuMemoryUsed: "{}".'.format(str(e))) + pass + try: + self._addKV('gpuUsed', gpuTree.find('utilization').find('gpu_util').text.split(" ")[0]) + except Exception as e: + logging.debug('Failed to get gpuUsed: "{}".'.format(str(e))) + pass + try: + self._addKV('gpuTemperature', gpuTree.find('temperature').find('gpu_temp').text.split(" ")[0]) + except Exception as e: + logging.debug('Failed to get gpuTemperature: "{}".'.format(str(e))) + pass + + except Exception as e: + logging.debug('Failed to get information from nvidia_smi: "{}".'.format(str(e))) return def toDict(self): From 6505b8d9f92e44426e09a725139aff7d6d287702 Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Tue, 10 Sep 2019 13:16:21 +0200 Subject: [PATCH 286/293] [github] fix bug report --- .github/ISSUE_TEMPLATE/bug_report.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fa26aa59fd..54139c34b5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,15 +24,15 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Log** -If applicable, copy paste the relevant log output (please embed the text in a markdown code tag "```" ) +If applicable, copy paste the relevant log output (please embed the text in a markdown code tag "\`\`\`" ) **Desktop (please complete the following and other pertinent information):** - OS: [e.g. win 10, osx, ] - - Version [e.g. 2019.1] - Python version [e.g. 2.6] - Qt/PySide version [e.g. 5.12.4] - - Binary version (if applicable) [e.g. 2019.1] - - Commit reference (if applicable) [e.g. 08ddbe2] + - Meshroom version: please specify if you are using a release version or your own build + - Binary version (if applicable) [e.g. 2019.1] + - Commit reference (if applicable) [e.g. 08ddbe2] **Additional context** Add any other context about the problem here. From 210e7e25c760d09b4800d9f2123d0eda1344c09d Mon Sep 17 00:00:00 2001 From: Simone Gasparini Date: Tue, 10 Sep 2019 13:16:48 +0200 Subject: [PATCH 287/293] [github] added template for questions --- .github/ISSUE_TEMPLATE/question_help.md | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/question_help.md diff --git a/.github/ISSUE_TEMPLATE/question_help.md b/.github/ISSUE_TEMPLATE/question_help.md new file mode 100644 index 0000000000..26eef531c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question_help.md @@ -0,0 +1,31 @@ +--- +name: Question or help needed +about: Ask question or for help for issues not related to program failures (e.g. "where I can find this feature", "my dataset is not reconstructed properly", "which parameter setting shall I use" etc...) +title: "[question]" +labels: type:question +assignees: '' + +--- + +**Describe the problem** +A clear and concise description of what the problem is. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Dataset** +If applicable, add a link or *few* images to help better understand where the problem may come from. + +**Log** +If applicable, copy paste the relevant log output (please embed the text in a markdown code tag "\`\`\`" ) + +**Desktop (please complete the following and other pertinent information):** + - OS: [e.g. win 10, osx, ] + - Python version [e.g. 2.6] + - Qt/PySide version [e.g. 5.12.4] + - Meshroom version: please specify if you are using a release version or your own build + - Binary version (if applicable) [e.g. 2019.1] + - Commit reference (if applicable) [e.g. 08ddbe2] + +**Additional context** +Add any other context about the problem here. From 8f630d5c0919e45a5bf09ac360fc985701dba823 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 17:52:37 +0200 Subject: [PATCH 288/293] [ui] StatViewer: compatibility with previous "statistics" files --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 43 ++++++++++++---------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index e322f5e73f..5b54b08982 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -73,6 +73,13 @@ Item { readSourceFile() } + function getPropertyWithDefault(prop, name, defaultValue) { + if(prop.hasOwnProperty(name)) { + return prop[name]; + } + return defaultValue; + } + Timer { id: reloadTimer interval: root.deltaTime * 60000; running: true; repeat: false @@ -92,10 +99,8 @@ Item { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) { if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { - var jsonObject; - try { - jsonObject = JSON.parse(xhr.responseText); + root.jsonObject = JSON.parse(xhr.responseText); } catch(exc) { @@ -103,7 +108,6 @@ Item { root.jsonObject = {}; return; } - root.jsonObject = jsonObject; resetCharts(); sourceModified = xhr.getResponseHeader('Last-Modified') root.createCharts(); @@ -122,7 +126,7 @@ Item { } function createCharts() { - root.deltaTime = jsonObject.interval / 60.0; + root.deltaTime = getPropertyWithDefault(jsonObject, 'interval', 30) / 60.0; initCpuChart() initRamChart() initGpuChart() @@ -139,7 +143,7 @@ Item { var categoryCount = 0 var category do { - category = root.jsonObject.computer.curves["cpuUsage." + categoryCount] + category = jsonObject.computer.curves["cpuUsage." + categoryCount] if(category !== undefined) { categories.push(category) categoryCount++ @@ -149,7 +153,7 @@ Item { var nbCores = categories.length root.nbCores = nbCores - root.cpuFrequency = jsonObject.computer.cpuFreq + root.cpuFrequency = getPropertyWithDefault(jsonObject.computer, 'cpuFreq', -1) root.nbReads = categories[0].length-1 @@ -202,37 +206,37 @@ Item { **************************/ function initRamChart() { - root.ramTotal = jsonObject.computer.ramTotal - var ram = jsonObject.computer.curves.ramUsage + root.ramTotal = getPropertyWithDefault(jsonObject.computer, 'ramTotal', 1) + + var ram = getPropertyWithDefault(jsonObject.computer.curves, 'ramUsage', -1) var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM: " + root.ramTotal + "GB", valueAxisX2, valueAxisRam) if(ram.length === 1) { - ramSerie.append(0, ram[0] / 100 * root.ramTotal) - ramSerie.append(root.deltaTime, ram[0] / 100 * root.ramTotal) + // Create 2 entries if we have only one input value to create a segment that can be display + ramSerie.append(0, ram[0]) + ramSerie.append(root.deltaTime, ram[0]) } else { for(var i = 0; i < ram.length; i++) { - ramSerie.append(i * root.deltaTime, ram[i] / 100 * root.ramTotal) + ramSerie.append(i * root.deltaTime, ram[i]) } } ramSerie.color = colors[10] } - - /************************** *** GPU *** **************************/ function initGpuChart() { - root.gpuTotalMemory = jsonObject.computer.gpuMemoryTotal - root.gpuName = jsonObject.computer.gpuName + root.gpuTotalMemory = getPropertyWithDefault(jsonObject.computer, 'gpuMemoryTotal', 0) + root.gpuName = getPropertyWithDefault(jsonObject.computer, 'gpuName', '') - var gpuUsedMemory = jsonObject.computer.curves.gpuMemoryUsed - var gpuUsed = jsonObject.computer.curves.gpuUsed - var gpuTemperature = jsonObject.computer.curves.gpuTemperature + var gpuUsedMemory = getPropertyWithDefault(jsonObject.computer.curves, 'gpuMemoryUsed', 0) + var gpuUsed = getPropertyWithDefault(jsonObject.computer.curves, 'gpuUsed', 0) + var gpuTemperature = getPropertyWithDefault(jsonObject.computer.curves, 'gpuTemperature', 0) var gpuUsedSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "GPU", valueAxisX3, valueAxisY3) var gpuUsedMemorySerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Memory", valueAxisX3, valueAxisY3) @@ -261,7 +265,6 @@ Item { } - /************************** *** UI *** **************************/ From 8c62437a686bcdf9d8eab78c6eb3777493feba48 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 17:53:31 +0200 Subject: [PATCH 289/293] [core] stats: use cElementTree on python 2 --- meshroom/core/stats.py | 45 ++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index 369852333a..55edb98da9 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -1,13 +1,19 @@ from collections import defaultdict -from distutils import spawn -from subprocess import Popen, PIPE -import xml.etree.ElementTree as ET +import subprocess import logging import psutil import time import threading import platform import os +import sys + +if sys.version_info[0] == 2: + # On Python 2 use C implementation for performance and to avoid lots of warnings + from xml.etree import cElementTree as ET +else: + import xml.etree.ElementTree as ET + def bytes2human(n): """ @@ -40,6 +46,7 @@ def __init__(self): self.gpuName = '' if platform.system() == "Windows": + from distutils import spawn # If the platform is Windows and nvidia-smi # could not be found from the environment path, # try to find it from system drive with default installation path @@ -50,7 +57,7 @@ def __init__(self): self.nvidia_smi = "nvidia-smi" try: - p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) + p = subprocess.Popen([self.nvidia_smi, "-q", "-x"], stdout=subprocess.PIPE) xmlGpu, stdError = p.communicate() smiTree = ET.fromstring(xmlGpu) @@ -92,7 +99,7 @@ def update(self): def updateGpu(self): try: - p = Popen([self.nvidia_smi, "-q", "-x"], stdout=PIPE) + p = subprocess.Popen([self.nvidia_smi, "-q", "-x"], stdout=subprocess.PIPE) xmlGpu, stdError = p.communicate() smiTree = ET.fromstring(xmlGpu) @@ -212,7 +219,7 @@ def fromDict(self, d): class Statistics: """ """ - fileVersion = 1.0 + fileVersion = 2.0 def __init__(self): self.computer = ComputerStatistics() @@ -241,16 +248,24 @@ def toDict(self): } def fromDict(self, d): - version = d.get('fileVersion', 1.0) + version = d.get('fileVersion', 0.0) if version != self.fileVersion: - logging.info('Cannot load statistics, version was {} and we only support {}.'.format(version, self.fileVersion)) - self.computer = {} - self.process = {} - self.times = [] - return - self.computer.fromDict(d.get('computer', {})) - self.process.fromDict(d.get('process', {})) - self.times = d.get('times', []) + logging.debug('Statistics: file version was {} and the current version is {}.'.format(version, self.fileVersion)) + self.computer = {} + self.process = {} + self.times = [] + try: + self.computer.fromDict(d.get('computer', {})) + except Exception as e: + logging.debug('Failed while loading statistics: computer: "{}".'.format(str(e))) + try: + self.process.fromDict(d.get('process', {})) + except Exception as e: + logging.debug('Failed while loading statistics: process: "{}".'.format(str(e))) + try: + self.times = d.get('times', []) + except Exception as e: + logging.debug('Failed while loading statistics: times: "{}".'.format(str(e))) bytesPerGiga = 1024. * 1024. * 1024. From 4cc78ad5ec841f1b1089cb07b37e7b8e86e8929d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 17:55:18 +0200 Subject: [PATCH 290/293] [ui] StatViewer: more compatibility with previous "statistics" files * compute max peak ram if no total ram info * hide GPU chart if the file is from a previous version --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 26 +++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 5b54b08982..b5837f77d7 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -17,6 +17,8 @@ Item { property var sourceModified: undefined property var jsonObject + property real fileVersion: 0.0 + property int nbReads: 1 property real deltaTime: 1 @@ -24,6 +26,7 @@ Item { property int cpuFrequency: 0 property int ramTotal + property string ramLabel: "RAM: " property int gpuTotalMemory property int gpuMaxAxis: 100 @@ -127,6 +130,7 @@ Item { function createCharts() { root.deltaTime = getPropertyWithDefault(jsonObject, 'interval', 30) / 60.0; + root.fileVersion = getPropertyWithDefault(jsonObject, 'fileVersion', 0.0) initCpuChart() initRamChart() initGpuChart() @@ -207,11 +211,21 @@ Item { function initRamChart() { - root.ramTotal = getPropertyWithDefault(jsonObject.computer, 'ramTotal', 1) - var ram = getPropertyWithDefault(jsonObject.computer.curves, 'ramUsage', -1) - var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM: " + root.ramTotal + "GB", valueAxisX2, valueAxisRam) + root.ramTotal = getPropertyWithDefault(jsonObject.computer, 'ramTotal', -1) + root.ramLabel = "RAM: " + if(root.ramTotal <= 0) + { + var maxRamPeak = 0 + for(var i = 0; i < ram.length; i++) { + maxRamPeak = Math.max(maxRamPeak, ram[i]) + } + root.ramTotal = maxRamPeak + root.ramLabel = "RAM Max Peak: " + } + + var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, root.ramLabel + root.ramTotal + "GB", valueAxisX2, valueAxisRam) if(ram.length === 1) { // Create 2 entries if we have only one input value to create a segment that can be display @@ -222,7 +236,6 @@ Item { ramSerie.append(i * root.deltaTime, ram[i]) } } - ramSerie.color = colors[10] } @@ -419,7 +432,7 @@ Item { plotAreaColor: "transparent" titleColor: textColor - title: "RAM: " + root.ramTotal + "GB" + title: root.ramLabel + root.ramTotal + "GB" ValueAxis { id: valueAxisY2 @@ -486,7 +499,8 @@ Item { plotAreaColor: "transparent" titleColor: textColor - title: "GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB" + visible: (root.fileVersion >= 2.0) // No GPU information was collected before stats 2.0 fileVersion + title: (root.gpuName || root.gpuTotalMemory) ? ("GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB") : "No GPU" ValueAxis { id: valueAxisY3 From 40c3430707b695dd6ba18c1c291785d28aa19a17 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 18:51:11 +0200 Subject: [PATCH 291/293] [ui] StatViewer: do not display uninitialied values --- meshroom/ui/qml/GraphEditor/StatViewer.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index b5837f77d7..6ebf29c92b 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -122,6 +122,7 @@ Item { } function resetCharts() { + root.fileVersion = 0.0 cpuLegend.clear() cpuChart.removeAllSeries() ramChart.removeAllSeries() @@ -378,6 +379,7 @@ Item { plotAreaColor: "transparent" titleColor: textColor + visible: (root.fileVersion > 0.0) // only visible if we have valid information title: "CPU: " + root.nbCores + " cores, " + root.cpuFrequency + "Hz" ValueAxis { @@ -432,6 +434,7 @@ Item { plotAreaColor: "transparent" titleColor: textColor + visible: (root.fileVersion > 0.0) // only visible if we have valid information title: root.ramLabel + root.ramTotal + "GB" ValueAxis { From 6c7523243a1e5422f2c636cde0a263a69fee60a5 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 10 Sep 2019 18:53:37 +0200 Subject: [PATCH 292/293] [core] stats: no processing in ComputerStatistics constructor ComputerStatistics is instanciated for each NodeChunk, so any computation here takes time. Instead we initialize the values on the first update(). --- meshroom/core/stats.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index 55edb98da9..ec07dbef31 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -35,15 +35,25 @@ def bytes2human(n): class ComputerStatistics: def __init__(self): - # TODO: init self.nbCores = 0 - self.cpuFreq = psutil.cpu_freq().max - self.ramTotal = psutil.virtual_memory().total / 1024/1024/1024 + self.cpuFreq = 0 + self.ramTotal = 0 self.ramAvailable = 0 # GB self.vramAvailable = 0 # GB self.swapAvailable = 0 self.gpuMemoryTotal = 0 self.gpuName = '' + self.curves = defaultdict(list) + + self._isInit = False + + def initOnFirstTime(self): + if self._isInit: + return + self._isInit = True + + self.cpuFreq = psutil.cpu_freq().max + self.ramTotal = psutil.virtual_memory().total / 1024/1024/1024 if platform.system() == "Windows": from distutils import spawn @@ -77,8 +87,6 @@ def __init__(self): except Exception as e: logging.debug('Failed to get information from nvidia_smi at init: "{}".'.format(str(e))) - self.curves = defaultdict(list) - def _addKV(self, k, v): if isinstance(v, tuple): for ki, vi in v._asdict().items(): @@ -90,6 +98,7 @@ def _addKV(self, k, v): self.curves[k].append(v) def update(self): + self.initOnFirstTime() self._addKV('cpuUsage', psutil.cpu_percent(percpu=True)) # interval=None => non-blocking (percentage since last call) self._addKV('ramUsage', psutil.virtual_memory().percent) self._addKV('swapUsage', psutil.swap_memory().percent) From 683ea8457b92478f6c4dc2d096d5b2393fcc8461 Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 13 Sep 2019 11:04:23 +0200 Subject: [PATCH 293/293] [build] fix cxFreeze version for Python 2.7 compatibility latest cxFreeze 6.0 is not compatible with python 2.7 and breaks tests --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 9b248788d0..8a9d354b7d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,5 +1,5 @@ # packaging -cx_Freeze +cx_Freeze==5.1.1 # testing pytest \ No newline at end of file