diff --git a/meshroom/core/desc/interface.py b/meshroom/core/desc/interface.py new file mode 100644 index 0000000000..54667448a7 --- /dev/null +++ b/meshroom/core/desc/interface.py @@ -0,0 +1,69 @@ +class Interface: + """Marker base class for node descriptor interfaces. + + Inherit from this to declare a new interface. Classes that inherit from + an Interface are automatically discovered by InterfaceMeta and listed in + the ``interfaces`` attribute of the concrete node descriptor class. + """ + + @staticmethod + def upstreamNodesWithInterface(node, attribute, interface_name): + result = [] + visited = set() + stack = [attribute] + + while stack: + attr = stack.pop() + if not attr.isLink: + continue + source_node = attr.inputRootLink.node + if source_node in visited: + continue + visited.add(source_node) + if interface_name in source_node.nodeDesc.interfaces: + result.append(source_node) + continue + for _, input_attr in source_node.attributes.items(): + stack.append(input_attr) + + return result + +class InterfaceMeta(type): + """Metaclass that populates the ``interfaces`` attribute on any class that uses it. + + When a class is created with this metaclass, ``interfaces`` is set to the list + of ``Interface`` subclass names found in the class's MRO (excluding the class + itself and the base ``Interface`` marker). + """ + def __new__(mcs, name, bases, namespace): + cls = super().__new__(mcs, name, bases, namespace) + cls.interfaces = [ + b.__name__ for b in type.mro(cls) + if b is not Interface and b is not cls + and isinstance(b, type) and issubclass(b, Interface) + ] + return cls + +class InterfacedClass(metaclass=InterfaceMeta): + """Base class for objects that want automatic interface discovery via InterfaceMeta.""" + pass + + +class FeatureProviderInterface(Interface): + + def getFeaturesFolders(self, node) -> list: + raise NotImplementedError + + def getDescriberTypes(self, node) -> list: + raise NotImplementedError + +class MatchProviderInterface(Interface): + + def getMatchesFolders(self, node) -> list: + raise NotImplementedError + +class TrackProviderInterface(Interface): + + def getTracksFile(self, node) -> str: + raise NotImplementedError + diff --git a/meshroom/core/desc/node.py b/meshroom/core/desc/node.py index 51f8bd9abb..7137185e49 100644 --- a/meshroom/core/desc/node.py +++ b/meshroom/core/desc/node.py @@ -18,6 +18,7 @@ from .computation import Level, StaticNodeSize from .attribute import Attribute, ChoiceParam, ColorParam, Flow, IntParam, StringParam, ListAttribute +from .interface import InterfacedClass _MESHROOM_COMPUTE = (Path(_MESHROOM_ROOT) / "bin" / "meshroom_compute").as_posix() _MESHROOM_COMPUTE_DEPS = ["psutil"] @@ -212,7 +213,7 @@ def getInternalFlowOutputs(cls, mrNodeType: MrNodeType) -> list[Attribute]: return cls.FLOW_OUT -class BaseNode(object): +class BaseNode(InterfacedClass): """ """ cpu = Level.NORMAL diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 428b06ac61..799efbe64a 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -844,7 +844,7 @@ FocusScope { ExifOrientedViewer { id: featuresViewerLoader active: displayFeatures.checked && !useExternal - property var activeNode: _currentScene ? _currentScene.activeNodes.get("featureProvider").node : null + property var activeNode: _currentScene ? _currentScene.activeNodes.get("FeatureProviderInterface").node : null width: imgContainer.width height: imgContainer.height anchors.centerIn: parent @@ -856,7 +856,13 @@ FocusScope { if (active) { // Instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource setSource("FeaturesViewer.qml", { - "model": Qt.binding(function() { return activeNode ? activeNode.attribute("describerTypes").value : "" }), + "model": Qt.binding(function() { + let result = [] + if (activeNode) { + result = _currentScene.callInterfaceMethod("FeatureProviderInterface", "getDescriberTypes") || "" + } + return result + }), "currentViewId": Qt.binding(function() { return _currentScene.selectedViewId }), "features": Qt.binding(function() { return mfeaturesLoader.status === Loader.Ready ? mfeaturesLoader.item : null }), "tracks": Qt.binding(function() { return mtracksLoader.status === Loader.Ready ? mtracksLoader.item : null }), @@ -1171,7 +1177,7 @@ FocusScope { if (!root.aliceVisionPluginAvailable) { return null } - return _currentScene ? _currentScene.activeNodes.get("featureProvider").node : null + return _currentScene ? _currentScene.activeNodes.get("FeatureProviderInterface").node : null } property bool isComputed: activeNode && activeNode.isComputed active: isUsed && isComputed @@ -1182,23 +1188,16 @@ FocusScope { // so it can fail safely if the C++ plugin is not available setSource("MFeatures.qml", { "describerTypes": Qt.binding(function() { - return activeNode ? activeNode.attribute("describerTypes").value : {} + let result = [] + if (activeNode) { + result = _currentScene.callInterfaceMethod("FeatureProviderInterface", "getDescriberTypes") || "" + } + return result }), "featureFolders": Qt.binding(function() { let result = [] if (activeNode) { - if (activeNode.nodeType == "FeatureExtraction" && isComputed) { - result.push(activeNode.attribute("output").value) - } - else if (activeNode.nodeType == "RomaReducer" && isComputed) { - result.push(activeNode.attribute("featuresFolder").value) - } - else if (activeNode.hasAttribute("featuresFolders")) { - for (let i = 0; i < activeNode.attribute("featuresFolders").value.count; i++) { - let attr = activeNode.attribute("featuresFolders").value.at(i) - result.push(attr.value) - } - } + result = _currentScene.callInterfaceMethod("FeatureProviderInterface", "getFeaturesFolders") || "" } return result }), @@ -1282,12 +1281,12 @@ FocusScope { if (_currentScene) { //Try first to use tracks - if (_currentScene.activeNodes.get("trackProvider").node) + if (_currentScene.activeNodes.get("TrackProviderInterface").node) { - return _currentScene.activeNodes.get("trackProvider").node + return _currentScene.activeNodes.get("TrackProviderInterface").node } - return _currentScene.activeNodes.get("matchProvider").node + return _currentScene.activeNodes.get("MatchProviderInterface").node } return null @@ -1306,26 +1305,14 @@ FocusScope { "matchingFolders": Qt.binding(function() { let result = [] if (activeNode) { - if (activeNode.nodeType == "FeatureMatching" && isComputed) { - result.push(activeNode.attribute("output").value) - } else if (activeNode.hasAttribute("matchesFolders")) { - for (let i = 0; i < activeNode.attribute("matchesFolders").value.count; i++) { - let attr = activeNode.attribute("matchesFolders").value.at(i) - result.push(attr.value) - } - } + result = _currentScene.callInterfaceMethod("MatchProviderInterface", "getMatchesFolders") || [] } return result }), "tracksFile": Qt.binding(function() { let result = "" if (activeNode) { - if (activeNode.nodeType == "TracksBuilding" && isComputed) { - result = activeNode.attribute("output").value - } - else if (activeNode.hasAttribute("tracksFilename")) { - result = activeNode.attribute("tracksFilename").value - } + result = _currentScene.callInterfaceMethod("TrackProviderInterface", "getTracksFile") || "" } return result }) diff --git a/meshroom/ui/scene.py b/meshroom/ui/scene.py index 743dd78653..f721ec4d09 100755 --- a/meshroom/ui/scene.py +++ b/meshroom/ui/scene.py @@ -330,12 +330,6 @@ class Scene(UIGraph): "ApplyCalibration", "SfMExpanding", "SfMBootstraping"], # All nodes generating depth map files "allDepthMap": ["DepthMap", "DepthMapFilter"], - # Nodes that can be used to provide features folders to the UI - "featureProvider": ["FeatureExtraction", "FeatureMatching", "StructureFromMotion", "RomaReducer"], - # Nodes that can be used to provide matches folders to the UI - "matchProvider": ["FeatureMatching", "StructureFromMotion", "RomaReducer"], - # Nodes that can be used to provide tracks files to the UI - "trackProvider": ["TracksBuilding", "SfMBootstraping", "SfMExpanding"] } # Nodes accessed from the UI uiNodes = [ @@ -418,9 +412,16 @@ def initActiveNodes(self): # For all nodes declared to be accessed by the UI usedNodeTypes = {j for i in self.activeNodeCategories.values() for j in i} allLoadedNodeTypes = set(meshroom.core.pluginManager.getRegisteredNodePlugins().keys()) + + for nodeType, nodePlugin in meshroom.core.pluginManager.getRegisteredNodePlugins().items(): + nodeDesc = nodePlugin.nodeDescriptor + for interface in nodeDesc.interfaces: + if not self._activeNodes.get(interface): + self._activeNodes.add(ActiveNode(interface, parent=self)) + allUiNodes = set(self.uiNodes) | usedNodeTypes | allLoadedNodeTypes - for nodeType in allUiNodes: + for nodeType in allUiNodes: self._activeNodes.add(ActiveNode(nodeType, parent=self)) def clearActiveNodes(self): @@ -985,11 +986,38 @@ def setBuildingIntrinsics(self, value): pluginsReloaded = Signal(list, list) + @Slot(str, str, result="QVariant") + def callInterfaceMethod(self, interfaceName, methodName): + """Call a method defined by an interface on the active node for that interface. + + Args: + interfaceName: name of the interface (e.g. "FeatureProviderInterface") + methodName: name of the method to call on the nodeDesc (e.g. "getFeaturesFolders") + + Returns: + The result of the method call, or None if the interface/method is not available. + """ + entry = self.activeNodes.get(interfaceName) + if not entry or not entry.node: + return None + node = entry.node + method = getattr(node.nodeDesc, methodName, None) + if callable(method): + try: + return method(node) + except Exception as e: + logging.error(f"callInterfaceMethod: {interfaceName}.{methodName} raised: {e}") + return None + return None + @Slot(QObject) def setActiveNode(self, node, categories=True, inputs=True): """ Set node as the active node of its type and of its categories. Also upgrade related input nodes. """ + for interface in node.nodeDesc.interfaces: + self.activeNodes.getr(interface).node = node + if categories: for category, nodeTypes in self.activeNodeCategories.items(): if node.nodeType in nodeTypes: