mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
Merge pull request #2436 from alicevision/fix/compatibilityUpgrade
Fix compatibility upgrade issue
This commit is contained in:
commit
9a09310f07
9 changed files with 79 additions and 63 deletions
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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 = {
|
||||
"<VIEW_ID>": str(vp.childAttribute("viewId").value),
|
||||
"<INTRINSIC_ID>": str(vp.childAttribute("intrinsicId").value),
|
||||
"<POSE_ID>": str(vp.childAttribute("poseId").value),
|
||||
"<PATH>": vp.childAttribute("path").value,
|
||||
"<FILENAME>": FilepathHelper.basename(FilepathHelper, vp.childAttribute("path").value),
|
||||
"<FILESTEM>": FilepathHelper.removeExtension(FilepathHelper, FilepathHelper.basename(FilepathHelper, vp.childAttribute("path").value)),
|
||||
"<EXTENSION>": FilepathHelper.extension(FilepathHelper, vp.childAttribute("path").value),
|
||||
"<PATH>": vpPath,
|
||||
"<FILENAME>": FilepathHelper.basename(FilepathHelper, vpPath),
|
||||
"<FILESTEM>": FilepathHelper.removeExtension(FilepathHelper, FilepathHelper.basename(FilepathHelper, vpPath)),
|
||||
"<EXTENSION>": FilepathHelper.extension(FilepathHelper, vpPath),
|
||||
}
|
||||
|
||||
resolved = path
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue