diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 0246ade5..c5a006f5 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -38,6 +38,8 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None) attr = cls(node, description, isOutput, root, parent) if value is not None: attr.value = value + else: + attr.resetToDefaultValue() return attr @@ -72,8 +74,7 @@ class Attribute(BaseObject): self._invalidationValue = "" self._value = None - # do not emit value changed on initialization - self.resetValue(emitSignals=False) + self.initValue() @property def node(self): @@ -240,7 +241,11 @@ class Attribute(BaseObject): def upgradeValue(self, exportedValue): self._set_value(exportedValue) - def resetValue(self, emitSignals=True): + def initValue(self): + if self.desc._valueType is not None: + self._value = self.desc._valueType() + + def resetToDefaultValue(self, emitSignals=True): self._set_value(copy.copy(self.defaultValue()), emitSignals=emitSignals) def requestGraphUpdate(self): @@ -321,7 +326,7 @@ class Attribute(BaseObject): return if isinstance(v, Attribute): g.addEdge(v, self) - self.resetValue() + self.resetToDefaultValue() elif self.isInput and Attribute.isLinkExpression(v): # value is a link to another attribute link = v[1:-1] @@ -330,7 +335,7 @@ class Attribute(BaseObject): g.addEdge(g.node(linkNode).attribute(linkAttr), self) except KeyError as err: logging.warning('Connect Attribute from Expression failed.\nExpression: "{exp}"\nError: "{err}".'.format(exp=v, err=err)) - self.resetValue() + self.resetToDefaultValue() def getExportValue(self): if self.isLink: @@ -370,7 +375,13 @@ class Attribute(BaseObject): def defaultValue(self): if isinstance(self.desc.value, types.FunctionType): - return self.desc.value(self) + try: + return self.desc.value(self) + except Exception as e: + if not self.node.isCompatibilityNode: + # log message only if we are not in compatibility mode + logging.warning("Failed to evaluate default value (node lambda) for attribute '{}': {}".format(self.name, e)) + return None # Need to force a copy, for the case where the value is a list (avoid reference to the desc value) return copy.copy(self.desc.value) @@ -504,7 +515,10 @@ class ListAttribute(Attribute): def index(self, item): return self._value.indexOf(item) - def resetValue(self, emitSignals=True): + def initValue(self): + self.resetToDefaultValue(emitSignals=False) + + def resetToDefaultValue(self, emitSignals=True): self._value = ListModel(parent=self) if emitSignals: self.valueChanged.emit() @@ -650,14 +664,6 @@ class GroupAttribute(Attribute): def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): super(GroupAttribute, self).__init__(node, attributeDesc, isOutput, root, parent) - subAttributes = [] - for subAttrDesc in self.attributeDesc.groupDesc: - childAttr = attributeFactory(subAttrDesc, None, self.isOutput, self.node, self) - subAttributes.append(childAttr) - childAttr.valueChanged.connect(self.valueChanged) - - self._value.reset(subAttributes) - def __getattr__(self, key): try: return super(GroupAttribute, self).__getattr__(key) @@ -696,10 +702,18 @@ class GroupAttribute(Attribute): else: raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value))) - def resetValue(self, emitSignals=True): + def initValue(self): self._value = DictModel(keyAttrName='name', parent=self) - if emitSignals: - self.valueChanged.emit() + subAttributes = [] + for subAttrDesc in self.attributeDesc.groupDesc: + childAttr = attributeFactory(subAttrDesc, None, self.isOutput, self.node, self) + subAttributes.append(childAttr) + childAttr.valueChanged.connect(self.valueChanged) + self._value.reset(subAttributes) + + def resetToDefaultValue(self, emitSignals=True): + for attrDesc in self.desc._groupDesc: + self._value.get(attrDesc.name).resetToDefaultValue() @Slot(str, result=Attribute) def childAttribute(self, key): diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 69ce08e4..8e7f23c0 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -32,6 +32,7 @@ class Attribute(BaseObject): self._errorMessage = errorMessage self._visible = visible self._isExpression = (isinstance(self._value, str) and "{" in self._value) or isinstance(self._value, types.FunctionType) + self._valueType = None name = Property(str, lambda self: self._name, constant=True) label = Property(str, lambda self: self._label, constant=True) @@ -228,6 +229,7 @@ class File(Attribute): """ def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True): super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible) + self._valueType = str def validateValue(self, value): if not isinstance(value, str): @@ -247,6 +249,7 @@ class BoolParam(Param): """ def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True): super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible) + self._valueType = bool def validateValue(self, value): try: @@ -270,6 +273,7 @@ class IntParam(Param): self._range = range super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, validValue=validValue, errorMessage=errorMessage, visible=visible) + self._valueType = int def validateValue(self, value): # handle unsigned int values that are translated to int by shiboken and may overflow @@ -293,6 +297,7 @@ class FloatParam(Param): self._range = range super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, validValue=validValue, errorMessage=errorMessage, visible=visible) + self._valueType = float def validateValue(self, value): try: @@ -312,6 +317,8 @@ class PushButtonParam(Param): """ def __init__(self, name, label, description, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True): super(PushButtonParam, self).__init__(name=name, label=label, description=description, value=None, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible) + self._valueType = None + def validateValue(self, value): pass def checkValueTypes(self): @@ -373,6 +380,7 @@ class StringParam(Param): def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True): super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible) + self._valueType = str def validateValue(self, value): if not isinstance(value, str): @@ -390,6 +398,7 @@ class ColorParam(Param): """ def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True): super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible) + self._valueType = str def validateValue(self, value): if not isinstance(value, str) or len(value.split(" ")) > 1: @@ -746,7 +755,7 @@ class InitNode: """ for attrName in attributeNames: if node.hasAttribute(attrName): - node.attribute(attrName).resetValue() + node.attribute(attrName).resetToDefaultValue() def extendAttributes(self, node, attributesDict): """ diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index b720ff8a..bd4dc625 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -268,7 +268,7 @@ class Graph(BaseObject): if not isinstance(graphData, dict): raise RuntimeError('loadGraph error: Graph is not a dict. File: {}'.format(filepath)) - + self._fileDateVersion = os.path.getmtime(filepath) self.header = fileData.get(Graph.IO.Keys.Header, {}) @@ -551,13 +551,13 @@ class Graph(BaseObject): # find top-level links if Attribute.isLinkExpression(attr.value): skippedEdges[attr] = attr.value - attr.resetValue() + attr.resetToDefaultValue() # find links in ListAttribute children elif isinstance(attr, ListAttribute): for child in attr.value: if Attribute.isLinkExpression(child.value): skippedEdges[child] = child.value - child.resetValue() + child.resetToDefaultValue() return node, skippedEdges def duplicateNodes(self, srcNodes): @@ -933,7 +933,7 @@ class Graph(BaseObject): for edge in self.getEdges(dependenciesOnly=dependenciesOnly): nodeEdges[edge.src.node].add(edge.dst.node) - + return nodeEdges def dfs(self, visitor, startNodes=None, longestPathFirst=False): @@ -1548,7 +1548,7 @@ class Graph(BaseObject): @property def fileDateVersion(self): return self._fileDateVersion - + @fileDateVersion.setter def fileDateVersion(self, value): self._fileDateVersion = value diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 0aa7b3f2..067fb5b3 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -760,13 +760,14 @@ class BaseNode(BaseObject): except AttributeError as e: # If we load an old scene, the lambda associated to the 'value' could try to access other params that could not exist yet logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'.format(nodeName=self.name, attrName=attr.name)) - try: - attr.value = defaultValue.format(**self._cmdVars) - attr._invalidationValue = defaultValue.format(**cmdVarsNoCache) - except KeyError as e: - logging.warning('Invalid expression with missing key on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) - except ValueError as e: - logging.warning('Invalid expression value on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) + if defaultValue is not None: + try: + attr.value = defaultValue.format(**self._cmdVars) + attr._invalidationValue = defaultValue.format(**cmdVarsNoCache) + except KeyError as e: + logging.warning('Invalid expression with missing key on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) + except ValueError as e: + logging.warning('Invalid expression value on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) v = attr.getValueStr(withQuotes=True) @@ -1272,13 +1273,13 @@ class Node(BaseNode): self._internalFolder = self.nodeDesc.internalFolder for attrDesc in self.nodeDesc.inputs: - self._attributes.add(attributeFactory(attrDesc, None, False, self)) + self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False, node=self)) for attrDesc in self.nodeDesc.outputs: - self._attributes.add(attributeFactory(attrDesc, None, True, self)) + self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=True, node=self)) for attrDesc in self.nodeDesc.internalInputs: - self._internalAttributes.add(attributeFactory(attrDesc, None, False, self)) + self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False, node=self)) # List attributes per uid for attr in self._attributes: @@ -1292,8 +1293,6 @@ class Node(BaseNode): for uidIndex in attr.attributeDesc.uid: self.attributesPerUid[uidIndex].add(attr) - self.setAttributeValues(kwargs) - self.setInternalAttributeValues(kwargs) self.optionalCallOnDescriptor("onNodeCreated") def optionalCallOnDescriptor(self, methodName, *args, **kwargs): @@ -1641,7 +1640,7 @@ class CompatibilityNode(BaseNode): try: upgradedAttrValues = node.nodeDesc.upgradeAttributeValues(attrValues, self.version) except Exception as e: - logging.error("Error in the upgrade implementation of the node: {}.\n{}".format(self.name, str(e))) + logging.error("Error in the upgrade implementation of the node: {}.\n{}".format(self.name, repr(e))) upgradedAttrValues = attrValues if not isinstance(upgradedAttrValues, dict): @@ -1650,13 +1649,8 @@ class CompatibilityNode(BaseNode): node.upgradeAttributeValues(upgradedAttrValues) - try: - upgradedIntAttrValues = node.nodeDesc.upgradeAttributeValues(intAttrValues, self.version) - except Exception as e: - logging.error("Error in the upgrade implementation of the node: {}.\n{}".format(self.name, str(e))) - upgradedIntAttrValues = intAttrValues + node.upgradeInternalAttributeValues(intAttrValues) - node.upgradeInternalAttributeValues(upgradedIntAttrValues) return node compatibilityIssue = Property(int, lambda self: self.issue.value, constant=True) @@ -1753,8 +1747,7 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False): break if compatibilityIssue is None: - node = Node(nodeType, position, **inputs, **outputs) - node.setInternalAttributeValues(internalInputs) + node = Node(nodeType, position, **inputs, **internalInputs, **outputs) else: logging.debug("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name)) node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index ac3295e9..95a54025 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -378,12 +378,12 @@ class RemoveImagesCommand(GraphCommand): def redoImpl(self): for i in range(len(self.cameraInits)): # Reset viewpoints - self.cameraInits[i].viewpoints.resetValue() + self.cameraInits[i].viewpoints.resetToDefaultValue() self.cameraInits[i].viewpoints.valueChanged.emit() self.cameraInits[i].viewpoints.requestGraphUpdate() # Reset intrinsics - self.cameraInits[i].intrinsics.resetValue() + self.cameraInits[i].intrinsics.resetToDefaultValue() self.cameraInits[i].intrinsics.valueChanged.emit() self.cameraInits[i].intrinsics.requestGraphUpdate() @@ -434,7 +434,8 @@ class UpgradeNodeCommand(GraphCommand): self.graph.removeNode(self.nodeName) # recreate compatibility node with GraphModification(self.graph): - node = nodeFactory(self.nodeDict) + # We come back from an upgrade, so we enforce uidConflict=True as there was a uid conflict before + node = nodeFactory(self.nodeDict, name=self.nodeName, uidConflict=True) self.graph.addNode(node, self.nodeName) # recreate out edges for dstAttr, srcAttr in self.outEdges.items(): diff --git a/meshroom/ui/components/filepath.py b/meshroom/ui/components/filepath.py index 610fce5f..ed450609 100644 --- a/meshroom/ui/components/filepath.py +++ b/meshroom/ui/components/filepath.py @@ -103,14 +103,15 @@ class FilepathHelper(QObject): def resolve(self, path, vp): # Resolve dynamic path that depends on viewpoint + vpPath = vp.childAttribute("path").value replacements = { "": str(vp.childAttribute("viewId").value), "": str(vp.childAttribute("intrinsicId").value), "": str(vp.childAttribute("poseId").value), - "": vp.childAttribute("path").value, - "": FilepathHelper.basename(FilepathHelper, vp.childAttribute("path").value), - "": FilepathHelper.removeExtension(FilepathHelper, FilepathHelper.basename(FilepathHelper, vp.childAttribute("path").value)), - "": FilepathHelper.extension(FilepathHelper, vp.childAttribute("path").value), + "": vpPath, + "": FilepathHelper.basename(FilepathHelper, vpPath), + "": FilepathHelper.removeExtension(FilepathHelper, FilepathHelper.basename(FilepathHelper, vpPath)), + "": FilepathHelper.extension(FilepathHelper, vpPath), } resolved = path diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 2e39f983..a5af5dfc 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -375,7 +375,6 @@ class UIGraph(QObject): oldGraph.deleteLater() self._graph.updated.connect(self.onGraphUpdated) - self._graph.update() self._taskManager.update(self._graph) # perform auto-layout if graph does not provide nodes positions if Graph.IO.Features.NodesPositions not in self._graph.fileFeatures: diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index e7b4a0bd..c9ff4036 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -424,7 +424,8 @@ class Reconstruction(UIGraph): "SfMAlignment"], # All nodes generating a sfmData file "sfmData": ["CameraInit", "DistortionCalibration", "StructureFromMotion", "GlobalSfM", - "PanoramaEstimation", "SfMTransfer", "SfMTransform", "SfMAlignment"], + "PanoramaEstimation", "SfMTransfer", "SfMTransform", "SfMAlignment", + "ApplyCalibration"], # All nodes generating depth map files "allDepthMap": ["DepthMap", "DepthMapFilter"], # Nodes that can be used to provide features folders to the UI @@ -496,10 +497,11 @@ class Reconstruction(UIGraph): self._activeNodes.get(key).node = None def onCameraInitChanged(self): + if self._cameraInit is None: + return # Update active nodes when CameraInit changes nodes = self._graph.dfsOnDiscover(startNodes=[self._cameraInit], reverse=True)[0] self.setActiveNodes(nodes) - self.resetActiveNodePerCategory() @Slot() @Slot(str) @@ -574,8 +576,8 @@ class Reconstruction(UIGraph): self.selectedViewId = "-1" self.tempCameraInit = None self.updateCameraInits() - self.sfm = self.lastSfmNode() self.resetActiveNodePerCategory() + self.sfm = self.lastSfmNode() if not self._graph: return @@ -602,9 +604,6 @@ class Reconstruction(UIGraph): if self.cameraInit is None or self.cameraInit not in cameraInits: self.cameraInit = cameraInits[0] if cameraInits else None - # Manually emit the signal to ensure the active CameraInit index is always up-to-date in the UI - self.cameraInitChanged.emit() - def getCameraInitIndex(self): if not self._cameraInit: # No CameraInit node @@ -924,10 +923,10 @@ class Reconstruction(UIGraph): 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) + vp.intrinsicId.resetToDefaultValue() # reset intrinsic assignation + vp.metadata.resetToDefaultValue() # and metadata (to clear any previous 'SensorWidth' entries) # reset existing intrinsics list - cameraInitCopy.intrinsics.resetValue() + cameraInitCopy.intrinsics.resetToDefaultValue() try: self.setBuildingIntrinsics(True) diff --git a/tests/test_nodeCommandLineFormatting.py b/tests/test_nodeCommandLineFormatting.py index ae618b07..31ac539e 100644 --- a/tests/test_nodeCommandLineFormatting.py +++ b/tests/test_nodeCommandLineFormatting.py @@ -24,7 +24,7 @@ def test_formatting_listOfFiles(): n1.featuresFolders.extend("single value with space") assert n1.featuresFolders.getValueStr() == '"single value with space"' - n1.featuresFolders.resetValue() + n1.featuresFolders.resetToDefaultValue() assert n1.featuresFolders.getValueStr() == '' n1.featuresFolders.extend(inputImages)