Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bin/meshroom_info
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ version_mode_parser = subparsers.add_parser(
"version", help="Display Meshroom version.")
version_mode_parser.add_argument("-p", "--path", action="store_true")

# Attribute Converter info subparser
converters_mode_parser = subparsers.add_parser(
"attrconvert", help="Display AttributeConverter nodes info.")

# Node info subparser
nodes_mode_parser = subparsers.add_parser(
"nodeinfo", help="Display nodes info.")
Expand All @@ -48,6 +52,18 @@ def get_version(args):
print(f"Meshroom is located at {meshroom._MESHROOM_ROOT}")


# ===== ATTRIBUTE CONVERTERS =====
def get_attributeconverter_info(args):
import meshroom.core
meshroom.core.initNodes()
converterNodes = meshroom.core.AttributeConverterRegistry.getAllConverters()
print(f"Available Attribute Converters ({len(converterNodes)}):")
for (srcType, dstType), converters in meshroom.core.AttributeConverterRegistry._converters.items():
print(f"\n Convert from {srcType.__name__} to {dstType.__name__}:")
for converter in converters:
print(f" - {converter.__name__}")


# ===== NODES =====
def get_nodes_info(args):
import meshroom.core
Expand Down Expand Up @@ -161,6 +177,8 @@ if __name__ == "__main__":

if args.command == "version":
get_version(args)
elif args.command == "attrconvert":
get_attributeconverter_info(args)
elif args.command == "nodeinfo":
get_nodes_info(args)
elif args.command == "pipelines":
Expand Down
53 changes: 52 additions & 1 deletion meshroom/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@
except Exception:
pass

from meshroom.core.plugins import NodePlugin, NodePluginManager, Plugin, processEnvFactory, formatNodeDescriptionErrorMessage
from meshroom.core.plugins import (
NodePlugin,
NodePluginManager,
Plugin,
processEnvFactory,
formatNodeDescriptionErrorMessage
)
from meshroom.core.submitter import BaseSubmitter
from meshroom.core.attributeConverter import AttributeConverter, AttributeConverterRegistry
from meshroom.env import EnvVar, meshroomFolder
from . import desc
from .desc import MrNodeType
Expand Down Expand Up @@ -197,6 +204,23 @@ def loadClassesSubmitters(folder: str, packageName: str) -> list[BaseSubmitter]:
return loadClasses(folder, packageName, BaseSubmitter)


def loadAttributeConverterClasses(folder: str, packageName: str) -> list[AttributeConverter]:
"""
Return the list of all the AttributeConverter nodes that were found during
the search of the Python module named "packageName" that located in the folder
"folder". An AttributeConverter node is found if a file within "packageName"
contains a class inheriting from `AttributeConverter`.

Args:
folder: the folder to load the module from.
packageName: the name of the module to look for nodes in.

Returns:
list[AttributeConverter]: a list of all the atribute converters that were found in the module.
"""
return loadClasses(folder, packageName, AttributeConverter)


class Version:
"""
Version provides convenient properties and methods to manipulate and compare versions.
Expand Down Expand Up @@ -411,6 +435,28 @@ def loadAllSubmitters(folder) -> list[BaseSubmitter]:
return submitters


def registerAttributeConverter(converter: AttributeConverter):
AttributeConverterRegistry.add(converter())


def loadAttributeConverter(folder, packageName) -> list[AttributeConverter]:
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
if not os.path.isdir(folder):
logging.error(f"AttributeConverter folder '{folder}' does not exist.")
return []

return loadAttributeConverterClasses(folder, packageName)
Comment thread
Alxiice marked this conversation as resolved.


def loadAllAttributeConverters(folder) -> list[AttributeConverter]:
attributeConverters = []
for _, package, ispkg in pkgutil.iter_modules([folder]):
if ispkg:
converters = loadAttributeConverter(folder, package)
if converters:
attributeConverters.extend(converters)
return attributeConverters


def loadPipelineTemplates(folder: str):
if not os.path.isdir(folder):
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
Expand All @@ -424,6 +470,11 @@ def initNodes():
additionalNodesPath = EnvVar.getList(EnvVar.MESHROOM_NODES_PATH)
nodesFolders = [os.path.join(meshroomFolder, "nodes")] + additionalNodesPath
for f in nodesFolders:
# Load converter nodes
converterNodes = loadAllAttributeConverters(folder=f)
for cn in converterNodes:
registerAttributeConverter(cn)
# Load nodes
plugins = loadAllNodes(folder=f)
if plugins:
for plugin in plugins:
Expand Down
33 changes: 22 additions & 11 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
from meshroom.core.desc.validators import NotEmptyValidator
from meshroom.core import desc, hashValue

from meshroom.core.attributeConverter import AttributeConverterRegistry
from meshroom.core.desc import Attribute as AttributeDescription

from meshroom.core.keyValues import KeyValues
Expand Down Expand Up @@ -239,7 +239,8 @@ def _getValue(self):
if self.keyable:
raise RuntimeError(f"Cannot get value of {self._getFullName()}, the attribute is keyable.")
if self.isLink:
return self._getInputLink().value
edge = self.node.graph.edge(self)
return edge.resolvedValue()
self._resolveValue()
return self._value

Expand Down Expand Up @@ -323,7 +324,7 @@ def _handleLinkValue(self, value) -> bool:
self._linkExpression = value
return True

def _applyExpr(self):
def _applyExpr(self, converterMap: dict = None):
"""
For string parameters with an expression (when loaded from file),
this function convert the expression into a real edge in the graph
Expand Down Expand Up @@ -352,7 +353,13 @@ def _applyExpr(self):
attr = node.attribute(linkAttrName) if node.hasAttribute(linkAttrName) else node.internalAttribute(linkAttrName)
if attr is None:
raise InvalidEdgeError(self.fullName, link, "Source attribute does not exist.")
attr.connectTo(self)
connectedEdge, _ = attr.connectTo(self)
if connectedEdge:
src, dst = connectedEdge[0]
if converterMap and (converterName:=converterMap.get((src.fullName, dst.fullName))):
logging.info(f"Edge {src.fullName}->{dst.fullName}: set converter to {converterName}")
edge = self.node.graph.edge(dst)
edge.setConverter(converterName)
except InvalidEdgeError as err:
logging.warning(err)
except Exception as err:
Expand Down Expand Up @@ -667,7 +674,9 @@ def _validateIncomingConnection(self, connectingAttribute: Attribute) -> bool:
Returns:
True if the connection is valid, False otherwise.
"""
return self.baseType == connectingAttribute.baseType
if self.baseType == connectingAttribute.baseType:
return True
return AttributeConverterRegistry.hasConverter(connectingAttribute.baseType, self.baseType)

def connectTo(self, dstAttribute: Attribute) -> tuple[list[list[Attribute]], list[list[Attribute]]]:
"""
Expand Down Expand Up @@ -870,7 +879,9 @@ def __len__(self):

def getValues(self):
if (linkParam := self._getInputLink()) is not None:
return linkParam.getValues()
edges = [e for e in self.node.graph.edges.values() if e.dst == self]
if edges:
return edges[0].resolvedValues()
return self._values if self._values is not None else self._desc._values

def setValues(self, values):
Expand Down Expand Up @@ -1035,9 +1046,9 @@ def _setValue(self, value):
self.requestGraphUpdate()

# Override
def _applyExpr(self):
def _applyExpr(self, converterMap: dict = None):
if self._linkExpression:
super()._applyExpr()
super()._applyExpr(converterMap)
else:
for value in self._value:
value._applyExpr()
Expand Down Expand Up @@ -1284,12 +1295,12 @@ def _setValue(self, exportedValue):
raise AttributeError(f"Failed to set on GroupAttribute: {str(value)}")

# Override
def _applyExpr(self):
def _applyExpr(self, converterMap: dict = None):
if self._linkExpression:
super()._applyExpr()
super()._applyExpr(converterMap)
else:
for value in self._value:
value._applyExpr()
value._applyExpr(converterMap)

# Override
def resetToDefaultValue(self):
Expand Down
102 changes: 102 additions & 0 deletions meshroom/core/attributeConverter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
attributeConverter: base descriptors class for AttributeConverter nodes
"""

import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, ClassVar
from collections import defaultdict
from itertools import chain

if TYPE_CHECKING:
from meshroom.core.desc.attribute import Attribute


class AttributeConverter(ABC):
"""
Base class for converting the value of a source Attribute
into a value for a destination Attribute of a different type,
so a connection can be made between them.
"""

name: ClassVar[str] = ""
description: ClassVar[str] = ""
priority: ClassVar[int] = 10 # Put a higher number to prioritize specific converters

# Input / Output classes
srcType: ClassVar["Attribute"] = None
dstType: ClassVar["Attribute"] = None

def __init__(self):
if not all ((self.srcType, self.dstType)):
raise TypeError(
f"Class '{self.__class__.__name__}' must define srcType and dstType."
)

@classmethod
def getName(cls):
return cls.name or cls.__name__

def canConvert(self, srcType, dstType):
""" Check if this converter corresponds to a source/destination attribute pair.
"""
return isinstance(srcType, self.srcType) and isinstance(dstType, self.dstType)

@abstractmethod
def convert(self, value):
""" Convert a value from the source attribute's type to a value for
the destination attribute's type.
"""
return value

def __repr__(self):
return f"<AttributeConverter {self.getName()} ({self.srcType.__name__} -> {self.dstType.__name__})>"


class AttributeConverterRegistry:
"""
Registry of available converters
"""

# { (srcType, dstType): [converters] }
_converters: dict[tuple["Attribute", "Attribute"], list[AttributeConverter]] = defaultdict(list)

@classmethod
def add(cls, converter: AttributeConverter):
if not issubclass(converter.__class__, AttributeConverter):
raise TypeError(f"{converter} parent class must subclass AttributeConverter")
logging.info(
f"Add converter class: {converter.getName()} "
f"({converter.srcType.__name__} -> {converter.dstType.__name__})"
)
cls._converters[(converter.srcType.__name__, converter.dstType.__name__)].append(converter)

@classmethod
def getAllConverters(cls) -> list[AttributeConverter]:
return list(chain.from_iterable(cls._converters.values()))

@classmethod
def getConverterByName(cls, name):
for c in cls.getAllConverters():
if c.getName() == name:
return c
return None

@classmethod
def hasConverter(cls, srcType: "Attribute", dstType: "Attribute") -> list[AttributeConverter]:
return ((srcType, dstType)) in cls._converters

Check notice on line 87 in meshroom/core/attributeConverter.py

View check run for this annotation

codefactor.io / CodeFactor

meshroom/core/attributeConverter.py#L87

Multiple spaces after keyword. (E271)

@classmethod
def getConverters(cls, srcType: "Attribute", dstType: "Attribute") -> list[AttributeConverter]:
""" Get priority-ordered converters.
"""
converters = cls._converters.get((srcType, dstType), [])
return sorted(converters, key=lambda c: -c.priority)

@classmethod
def getConverter(cls, srcType: "Attribute", dstType: "Attribute") -> AttributeConverter:
""" Get highest priority converter. """
converters = cls.getConverters(srcType, dstType)
if not converters:
return None
return converters[0]
Loading
Loading