diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 87ec1d84..5312dd89 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -10,7 +10,7 @@ class Attribute(BaseObject): """ """ - def __init__(self, name, label, description, value, advanced, uid, group, enabled): + def __init__(self, name, label, description, value, advanced, semantic, uid, group, enabled): super(Attribute, self).__init__() self._name = name self._label = label @@ -20,6 +20,7 @@ class Attribute(BaseObject): self._group = group self._advanced = advanced self._enabled = enabled + self._semantic = semantic name = Property(str, lambda self: self._name, constant=True) label = Property(str, lambda self: self._label, constant=True) @@ -29,6 +30,7 @@ class Attribute(BaseObject): group = Property(str, lambda self: self._group, constant=True) advanced = Property(bool, lambda self: self._advanced, constant=True) enabled = Property(Variant, lambda self: self._enabled, constant=True) + semantic = Property(str, lambda self: self._semantic, constant=True) type = Property(str, lambda self: self.__class__.__name__, constant=True) def validateValue(self, value): @@ -55,13 +57,13 @@ class Attribute(BaseObject): class ListAttribute(Attribute): """ A list of Attributes """ - def __init__(self, elementDesc, name, label, description, group='allParams', advanced=False, enabled=True, joinChar=' '): + def __init__(self, elementDesc, name, label, description, group='allParams', advanced=False, semantic='', enabled=True, 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=advanced, enabled=enabled) + super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group, advanced=advanced, semantic=semantic, enabled=enabled) elementDesc = Property(Attribute, lambda self: self._elementDesc, constant=True) uid = Property(Variant, lambda self: self.elementDesc.uid, constant=True) @@ -92,13 +94,13 @@ class ListAttribute(Attribute): class GroupAttribute(Attribute): """ A macro Attribute composed of several Attributes """ - def __init__(self, groupDesc, name, label, description, group='allParams', advanced=False, enabled=True, joinChar=' '): + def __init__(self, groupDesc, name, label, description, group='allParams', advanced=False, semantic='', enabled=True, 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=advanced, enabled=enabled) + super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group, advanced=advanced, semantic=semantic, enabled=enabled) groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) @@ -166,15 +168,15 @@ class GroupAttribute(Attribute): class Param(Attribute): """ """ - def __init__(self, name, label, description, value, uid, group, advanced, enabled): - super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + def __init__(self, name, label, description, value, uid, group, advanced, semantic, enabled): + super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) class File(Attribute): """ """ - def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, enabled=True): - super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): + super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): if not isinstance(value, pyCompatibility.basestring): @@ -185,8 +187,8 @@ class File(Attribute): class BoolParam(Param): """ """ - def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, enabled=True): - super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): + super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): try: @@ -198,9 +200,9 @@ class BoolParam(Param): class IntParam(Param): """ """ - def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, enabled=True): + def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, semantic='', enabled=True): self._range = range - super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): # handle unsigned int values that are translated to int by shiboken and may overflow @@ -217,9 +219,9 @@ class IntParam(Param): class FloatParam(Param): """ """ - def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, enabled=True): + def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, semantic='', enabled=True): self._range = range - super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): try: @@ -233,13 +235,13 @@ class FloatParam(Param): class ChoiceParam(Param): """ """ - def __init__(self, name, label, description, value, values, exclusive, uid, group='allParams', joinChar=' ', advanced=False, enabled=True): + def __init__(self, name, label, description, value, values, exclusive, uid, group='allParams', joinChar=' ', advanced=False, semantic='', enabled=True): 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, advanced=advanced, enabled=enabled) + super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def conformValue(self, val): """ Conform 'val' to the correct type and check for its validity """ @@ -264,8 +266,8 @@ class ChoiceParam(Param): class StringParam(Param): """ """ - def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, enabled=True): - super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, enabled=enabled) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): + super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): if not isinstance(value, pyCompatibility.basestring): diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index 6dd48783..5bb38e06 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -38,6 +38,13 @@ It is robust to motion-blur, depth-of-field, occlusion. Be careful to have enoug value='', uid=[0], ), + desc.File( + name='masksFolder', + label='Masks Folder', + description='Use masks to filter features. Filename should be the same or the image uid.', + value='', + uid=[0], + ), desc.ChoiceParam( name='describerTypes', label='Describer Types', diff --git a/meshroom/nodes/aliceVision/ImageMasking.py b/meshroom/nodes/aliceVision/ImageMasking.py new file mode 100644 index 00000000..93a8540d --- /dev/null +++ b/meshroom/nodes/aliceVision/ImageMasking.py @@ -0,0 +1,147 @@ +__version__ = "3.0" + +from meshroom.core import desc + + +class ImageMasking(desc.CommandLineNode): + commandLine = 'aliceVision_imageMasking {allParams}' + size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=40) + commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' + + inputs = [ + desc.File( + name='input', + label='Input', + description='''SfMData file.''', + value='', + uid=[0], + ), + desc.ChoiceParam( + name='algorithm', + label='Algorithm', + description='', + value='HSV', + values=['HSV', 'AutoGrayscaleThreshold'], + exclusive=True, + uid=[0], + ), + desc.GroupAttribute( + name="hsv", + label="HSV Parameters", + description="""Values to select: + - Green: default values + - White: Tolerance = 1, minSaturation = 0, maxSaturation = 0.1, minValue = 0.8, maxValue = 1 + - Black: Tolerance = 1, minSaturation = 0, maxSaturation = 0.1, minValue = 0, maxValue = 0.2 + """, + group=None, + enabled=lambda node: node.algorithm.value == 'HSV', + groupDesc=[ + desc.FloatParam( + name='hsvHue', + label='Hue', + description='Hue value to isolate in [0,1] range. 0 = red, 0.33 = green, 0.66 = blue, 1 = red.', + semantic='color/hue', + value=0.33, + range=(0, 1, 0.01), + uid=[0] + ), + desc.FloatParam( + name='hsvHueRange', + label='Tolerance', + description='Tolerance around the hue value to isolate.', + value=0.1, + range=(0, 1, 0.01), + uid=[0] + ), + desc.FloatParam( + name='hsvMinSaturation', + label='Min Saturation', + description='Hue is meaningless if saturation is low. Do not mask pixels below this threshold.', + value=0.3, + range=(0, 1, 0.01), + uid=[0] + ), + desc.FloatParam( + name='hsvMaxSaturation', + label='Max Saturation', + description='Do not mask pixels above this threshold. It might be useful to mask white/black pixels.', + value=1, + range=(0, 1, 0.01), + uid=[0] + ), + desc.FloatParam( + name='hsvMinValue', + label='Min Value', + description='Hue is meaningless if value is low. Do not mask pixels below this threshold.', + value=0.3, + range=(0, 1, 0.01), + uid=[0] + ), + desc.FloatParam( + name='hsvMaxValue', + label='Max Value', + description='Do not mask pixels above this threshold. It might be useful to mask white/black pixels.', + value=1, + range=(0, 1, 0.01), + uid=[0] + ), + ]), + desc.BoolParam( + name='invert', + label='Invert', + description='''If ticked, the selected area is ignored. + If not, only the selected area is considered.''', + value=True, + uid=[0] + ), + desc.IntParam( + name='growRadius', + label='Grow Radius', + description='Grow the selected area. It might be used to fill the holes: then use shrinkRadius to restore the initial coutours.', + value=0, + range=(0, 50, 1), + uid=[0] + ), + desc.IntParam( + name='shrinkRadius', + label='Shrink Radius', + description='Shrink the selected area.', + value=0, + range=(0, 50, 1), + uid=[0] + ), + desc.File( + name='depthMapFolder', + label='Depth Mask Folder', + description='''Depth Mask Folder''', + value='', + uid=[0], + ), + desc.StringParam( + name='depthMapExp', + label='Depth Mask Expression', + description='''Depth Mask Expression, like "{inputFolder}/{stem}-depth.{ext}".''', + 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.''', + value=desc.Node.internalFolder, + uid=[], + ), + ] diff --git a/meshroom/nodes/aliceVision/MeshMasking.py b/meshroom/nodes/aliceVision/MeshMasking.py new file mode 100644 index 00000000..8ae68111 --- /dev/null +++ b/meshroom/nodes/aliceVision/MeshMasking.py @@ -0,0 +1,98 @@ +__version__ = "1.0" + +from meshroom.core import desc + + +class MeshMasking(desc.CommandLineNode): + commandLine = 'aliceVision_meshMasking {allParams}' + category = 'Mesh Post-Processing' + documentation = ''' +Decimate triangles based on image masks. +''' + + inputs = [ + desc.File( + name='input', + label='Dense SfMData', + description='SfMData file.', + value='', + uid=[0], + ), + desc.File( + name='inputMesh', + label='Input Mesh', + description='''Input Mesh (OBJ file format).''', + value='', + uid=[0], + ), + desc.ListAttribute( + elementDesc=desc.File( + name="masksFolder", + label="Masks Folder", + description="", + value="", + uid=[0], + ), + name="masksFolders", + label="Masks Folders", + description='Use masks from specific folder(s). Filename should be the same or the image uid.', + ), + desc.IntParam( + name='threshold', + label='Threshold', + description='The minimum number of visibility to keep a vertex.', + value=1, + range=(1, 100, 1), + uid=[0] + ), + desc.BoolParam( + name='smoothBoundary', + label='Smooth Boundary', + description='Modify the triangles at the boundary to fit the masks.', + value=False, + uid=[0] + ), + desc.BoolParam( + name='invert', + label='Invert', + description='''If ticked, the selected area is ignored. + If not, only the selected area is considered.''', + value=False, + uid=[0] + ), + desc.BoolParam( + name='undistortMasks', + label='Undistort Masks', + description='''Undistort the masks with the same parameters as the matching image. + Tick it if the masks are drawn on the original images.''', + value=False, + uid=[0] + ), + desc.BoolParam( + name='usePointsVisibilities', + label='Use points visibilities', + description='''Use the points visibilities from the meshing to filter triangles. + Example: when they are occluded, back-face, etc.''', + value=False, + 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='outputMesh', + label='Output Mesh', + description='''Output mesh (OBJ file format).''', + value=desc.Node.internalFolder + 'mesh.obj', + uid=[], + ), + ] diff --git a/meshroom/nodes/aliceVision/PrepareDenseScene.py b/meshroom/nodes/aliceVision/PrepareDenseScene.py index ac2fd0ec..31d0ecf1 100644 --- a/meshroom/nodes/aliceVision/PrepareDenseScene.py +++ b/meshroom/nodes/aliceVision/PrepareDenseScene.py @@ -34,6 +34,18 @@ This node export undistorted images so the depth map and texturing can be comput label="Images Folders", description='Use images from specific folder(s). Filename should be the same or the image uid.', ), + desc.ListAttribute( + elementDesc=desc.File( + name="masksFolder", + label="Masks Folder", + description="", + value="", + uid=[0], + ), + name="masksFolders", + label="Masks Folders", + description='Use masks from specific folder(s). Filename should be the same or the image uid.', + ), desc.ChoiceParam( name='outputFileType', label='Output File Type', diff --git a/meshroom/nodes/aliceVision/SfMDistances.py b/meshroom/nodes/aliceVision/SfMDistances.py new file mode 100644 index 00000000..24ab2354 --- /dev/null +++ b/meshroom/nodes/aliceVision/SfMDistances.py @@ -0,0 +1,67 @@ +__version__ = "3.0" + +from meshroom.core import desc + + +class SfMDistances(desc.CommandLineNode): + commandLine = 'aliceVision_utils_sfmDistances {allParams}' + size = desc.DynamicNodeSize('input') + + inputs = [ + desc.File( + name='input', + label='Input', + description='''SfMData file.''', + value='', + uid=[0], + ), + desc.ChoiceParam( + name='objectType', + label='Type', + description='', + value='landmarks', + values=['landmarks', 'cameras'], + exclusive=True, + uid=[0], + ), + desc.ChoiceParam( + name='landmarksDescriberTypes', + label='Describer Types', + description='Describer types used to describe an image (only used when using "landmarks").', + value=['cctag3'], + values=['sift', 'sift_float', 'sift_upright', 'akaze', 'akaze_liop', 'akaze_mldb', 'cctag3', 'cctag4', 'sift_ocv', 'akaze_ocv'], + exclusive=False, + uid=[0], + joinChar=',', + ), + desc.StringParam( + name='A', + label='A IDs', + description='It will display the distances between A and B elements.\n' + 'This value should be an ID or a list of IDs of landmarks IDs or cameras (UID or filename without extension).\n' + 'It will list all elements if empty.', + value='', + uid=[0], + ), + desc.StringParam( + name='B', + label='B IDs', + description='It will display the distances between A and B elements.\n' + 'This value should be an ID or a list of IDs of landmarks IDs or cameras (UID or filename without extension).\n' + 'It will list all elements if empty.', + 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 = [ + ] diff --git a/meshroom/nodes/aliceVision/SfMTransform.py b/meshroom/nodes/aliceVision/SfMTransform.py index e8f960b4..ad602c6d 100644 --- a/meshroom/nodes/aliceVision/SfMTransform.py +++ b/meshroom/nodes/aliceVision/SfMTransform.py @@ -53,7 +53,7 @@ The transformation can be based on: label='Transformation', 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", + " * from_single_camera: Camera UID or simplified regular expression to match image filepath (like '*camera2*.jpg')", value='', uid=[0], enabled=lambda node: node.method.value == "transformation" or node.method.value == "from_single_camera", diff --git a/meshroom/nodes/aliceVision/StructureFromMotion.py b/meshroom/nodes/aliceVision/StructureFromMotion.py index 2a392693..09914ac0 100644 --- a/meshroom/nodes/aliceVision/StructureFromMotion.py +++ b/meshroom/nodes/aliceVision/StructureFromMotion.py @@ -270,15 +270,36 @@ It iterates like that, adding cameras and triangulating new 2D features into 3D uid=[0], advanced=True, ), + desc.IntParam( + name='rigMinNbCamerasForCalibration', + label='Min Nb Cameras For Rig Calibration', + description='Minimal number of cameras to start the calibration of the rig', + value=20, + range=(1, 50, 1), + uid=[0], + advanced=True, + ), desc.BoolParam( name='lockAllIntrinsics', - label='Force Lock of All Intrinsic Camera Parameters.', + 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=False, uid=[0], ), + desc.IntParam( + name='minNbCamerasToRefinePrincipalPoint', + label='Min Nb Cameras To Refine Principal Point', + description='Minimal number of cameras to refine the principal point of the cameras (one of the intrinsic parameters of the camera). ' + 'If we do not have enough cameras, the principal point in consider is considered in the center of the image. ' + 'If minNbCamerasToRefinePrincipalPoint<=0, the principal point is never refined. ' + 'If minNbCamerasToRefinePrincipalPoint==1, the principal point is always refined.', + value=3, + range=(0, 20, 1), + uid=[0], + advanced=True, + ), desc.BoolParam( name='filterTrackForks', label='Filter Track Forks', diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 7c918196..83ab8ca3 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -145,7 +145,10 @@ RowLayout { { case "ChoiceParam": return attribute.desc.exclusive ? comboBox_component : multiChoice_component case "IntParam": return slider_component - case "FloatParam": return slider_component + case "FloatParam": + if(attribute.desc.semantic === 'color/hue') + return color_hue_component + return slider_component case "BoolParam": return checkbox_component case "ListAttribute": return listAttribute_component case "GroupAttribute": return groupAttribute_component @@ -379,5 +382,66 @@ RowLayout { } } } + + Component { + id: color_hue_component + RowLayout { + TextField { + implicitWidth: 100 + enabled: root.editable + // cast value to string to avoid intrusive scientific notations on numbers + property string displayValue: String(slider.pressed ? slider.formattedValue : attribute.value) + text: displayValue + selectByMouse: true + validator: DoubleValidator { + locale: 'C' // use '.' decimal separator disregarding the system locale + } + onEditingFinished: setTextFieldAttribute(text) + onAccepted: setTextFieldAttribute(text) + Component.onDestruction: { + if(activeFocus) + setTextFieldAttribute(text) + } + } + Rectangle { + height: slider.height + width: height + color: Qt.hsla(slider.pressed ? slider.formattedValue : attribute.value, 1, 0.5, 1) + } + Slider { + Layout.fillWidth: true + + id: slider + readonly property int stepDecimalCount: 2 + readonly property real formattedValue: value.toFixed(stepDecimalCount) + enabled: root.editable + value: attribute.value + from: 0 + to: 1 + stepSize: 0.01 + snapMode: Slider.SnapAlways + onPressedChanged: { + if(!pressed) + _reconstruction.setAttribute(attribute, formattedValue) + } + + background: ShaderEffect { + width: control.availableWidth + height: control.availableHeight + blending: false + fragmentShader: " + varying mediump vec2 qt_TexCoord0; + vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + void main() { + gl_FragColor = vec4(hsv2rgb(vec3(qt_TexCoord0.x, 1.0, 1.0)), 1.0); + }" + } + } + } + } } } diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index 96bfc516..1ca01331 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -49,6 +49,7 @@ import Utils 1.0 case ".abc": if(Viewer3DSettings.supportAlembic) component = abcLoaderEntityComponent; break; case ".exr": if(Viewer3DSettings.supportDepthMap) component = exrLoaderComponent; break; case ".obj": + case ".stl": default: component = sceneLoaderEntityComponent; break; } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index aded135a..2cf485b6 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -13,7 +13,7 @@ Item { // supported 3D files extensions readonly property var supportedExtensions: { - var exts = ['.obj']; + var exts = ['.obj', '.stl']; if(supportAlembic) exts.push('.abc'); if(supportDepthMap)