Skip to content

API documentation

Photo

Bases: QObject

A base class representing a single photo. Provides access to the RGB/Grayscale pixel data for a photo as well as access to label images (LabelImg) associated with this Photo. This class acts only as the interface defining what the classes subclassing this class should implement.

Source code in maphis\common\photo.py
class Photo(QObject):
    """
    A base class representing a single photo. Provides access to the RGB/Grayscale pixel data for a photo as well as
    access to label images (`LabelImg`) associated with this Photo.
    This class acts only as the interface defining what the classes subclassing this class should implement.
    """

    AnnotationIndex = int

    new_annotation_added = Signal(AnnotationIndex, Annotation)
    annotation_deleted = Signal(AnnotationIndex, Annotation)

    def __init__(self, parent: Optional[PySide6.QtCore.QObject] = None) -> None:
        super().__init__(parent)

    @property
    @abc.abstractmethod
    def _subscriber(self) -> Subscriber:
        pass

    @property
    @abc.abstractmethod
    def image_name(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def image_path(self) -> Path:
        pass

    @image_path.setter
    @abc.abstractmethod
    def image_path(self, path: Path):
        return Path()

    @property
    @abc.abstractmethod
    def image(self) -> np.ndarray:
        """Returns the pixel data for the photo. Beware that the order of sizes returned by the property `image_size`
        (width, height) is different from the size stored in the `shape` (height, width, channels) tuple of the
        returned pixel data.

        Returns:
            the pixel data
        """
        pass

    @property
    @abc.abstractmethod
    def image_size(self) -> typing.Tuple[int, int]:
        """Returns the image size (width, height).

        Returns:
            the image size in (width, height) order.
        """
        pass

    @abc.abstractmethod
    def __getitem__(self, label_name: str) -> LabelImg:
        """Returns the `LabelImg` instance stored under `label_name`.

        Args:
            label_name (str): The name under which the `LabelImg` is stored

        Returns:
            a `LabelImg` instance

        Raises:
            KeyError: If no `LabelImg` is stored under `label_name`
        """
        pass

    @property
    @abc.abstractmethod
    def label_images_(self) -> Dict[str, LabelImg]:
        pass

    @property
    @abc.abstractmethod
    def label_image_info(self) -> Dict[str, LabelImgInfo]:
        """Returns a dict `label_name` -> `LabelImgInfo`.

        Returns:
            dictionary mapping label names to `LabelImgInfo` objects
        """
        pass

    @property
    def image_scale(self) -> Optional[pint.Quantity]:
        """Return image scale as `pint.Quantity` if set, otherwise `None`.

        Returns:
            the image scale if set, otherwise `None`.

        """
        return None

    @image_scale.setter
    def image_scale(self, scale: Optional[pint.Quantity]):
        """Sets the image scale.

        Args:
            scale (Optional[pint.Quantity]): optional image scale
        """
        pass

    @property
    def scale_setting(self) -> Optional[ScaleSetting]:
        return None

    @scale_setting.setter
    def scale_setting(self, setting: Optional[ScaleSetting]):
        pass

    # TODO rename this to 'approvals'
    @property
    def approved(self) -> Dict[str, Optional[str]]:
        pass

    @abc.abstractmethod
    def has_segmentation_for(self, label_name: str) -> bool:
        return False

    @abc.abstractmethod
    def rotate(self, ccw: bool):
        pass

    @abc.abstractmethod
    def resize(self, factor: float):
        pass

    @abc.abstractmethod
    def save(self):
        pass

    @property
    @abc.abstractmethod
    def has_unsaved_changes(self) -> bool:
        pass

    @property
    def tags(self) -> typing.Set[str]:
        return set()

    @tags.setter
    def tags(self, _tags: typing.Set[str]):
        pass

    def add_tag(self, tag: str):
        pass

    def remove_tag(self, tag: str):
        pass

    def toggle_tag(self, tag: str, enabled: bool):
        pass

    @property
    def thumbnail(self) -> typing.Optional[QImage]:
        return QImage()

    @thumbnail.setter
    def thumbnail(self, thumb: typing.Optional[QImage]):
        pass

    @property
    def import_time(self) -> QDateTime:
        return None

    @import_time.setter
    def import_time(self, _time: QDateTime):
        pass

    @property
    def annotations(self) -> typing.Dict[AnnotationType, typing.List[Annotation]]:
        return {}

    def get_annotations(self, ann_type: AnnotationType) -> typing.List[Annotation]:
        return []

    def insert_new_annotation(self, ann_type: AnnotationType, annotation: Annotation):
        pass

    def __hash__(self) -> int:
        return hash(self.image_path)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        return hash(self) == hash(other)

image: np.ndarray abstractmethod property

Returns the pixel data for the photo. Beware that the order of sizes returned by the property image_size (width, height) is different from the size stored in the shape (height, width, channels) tuple of the returned pixel data.

Returns:

Type Description
ndarray

the pixel data

image_scale: Optional[pint.Quantity] property writable

Return image scale as pint.Quantity if set, otherwise None.

Returns:

Type Description
Optional[Quantity]

the image scale if set, otherwise None.

image_size: typing.Tuple[int, int] abstractmethod property

Returns the image size (width, height).

Returns:

Type Description
Tuple[int, int]

the image size in (width, height) order.

label_image_info: Dict[str, LabelImgInfo] abstractmethod property

Returns a dict label_name -> LabelImgInfo.

Returns:

Type Description
Dict[str, LabelImgInfo]

dictionary mapping label names to LabelImgInfo objects

__getitem__(label_name) abstractmethod

Returns the LabelImg instance stored under label_name.

Parameters:

Name Type Description Default
label_name str

The name under which the LabelImg is stored

required

Returns:

Type Description
LabelImg

a LabelImg instance

Raises:

Type Description
KeyError

If no LabelImg is stored under label_name

Source code in maphis\common\photo.py
@abc.abstractmethod
def __getitem__(self, label_name: str) -> LabelImg:
    """Returns the `LabelImg` instance stored under `label_name`.

    Args:
        label_name (str): The name under which the `LabelImg` is stored

    Returns:
        a `LabelImg` instance

    Raises:
        KeyError: If no `LabelImg` is stored under `label_name`
    """
    pass

LabelImg

Bases: QObject

A class representing a label image.

Source code in maphis\common\label_image.py
class LabelImg(QObject):
    """
    A class representing a label image.
    """

    property_added = Signal(object, RegionProperty)
    property_removed = Signal(object, RegionProperty)
    property_updated = Signal(object, RegionProperty)
    properties_need_recomputation = Signal(object)
    all_properties_valid = Signal(object)

    def __init__(self, image_size: Tuple[int, int]):
        super(LabelImg, self).__init__(None)
        self._label_img: Optional[np.ndarray] = None
        self.size = image_size
        self._path: typing.Optional[Path] = None
        self._bbox: typing.Optional[typing.Tuple[int, int, int, int]]
        self._region_props: Dict[int, Dict[str, RegionProperty]] = {}
        self.label_img_type: LabelImgType = LabelImgType.Regions
        self.label_info: typing.Optional[LabelImgInfo] = None
        self.label_semantic: str = ''
        self._used_labels: Optional[typing.Set[int]] = None
        self._dirty_flag: bool = False
        self.prop_list: typing.Set[RegionProperty] = set()
        self.timestamp: int = -1
        self.is_segmented: bool = False

    @property
    def path(self) -> typing.Optional[Path]:
        if self._path is not None:
            return self._path
        return None

    @property
    def filename(self) -> str:
        return self.path.name

    @property
    def label_image(self) -> np.ndarray:
        if self._label_img is None:
            if self._path.exists():
                self._label_img = io.imread(str(self._path))
                self.is_segmented = bool(np.any(self._label_img > 0))
            else:
                self._label_img = np.zeros(self.size[::-1], np.uint32)
        return self._label_img

    @label_image.setter
    def label_image(self, lbl_nd: np.ndarray):
        if self._label_img is None:
            self._label_img = lbl_nd
        else:
            self.set_image(lbl_nd)
        self.is_segmented = bool(np.any(self._label_img > 0))
        self._dirty_flag = True
        self._invalidate_measurements()

    @property
    def is_set(self) -> bool:
        return self._label_img is not None

    def reload(self):
        if self._path.exists():
            loaded_img = io.imread(str(self._path))
            self._label_img = loaded_img.astype(np.uint32)
            self._used_labels = set(np.unique(self._label_img))
        else:
            self._label_img = np.zeros(self.size[::-1], dtype=np.uint32)
            self._used_labels = {0}

    def unload(self):
        self.save()
        self._label_img = None

    def _serialize_prop_value(self, reg_prop: RegionProperty) -> Union[typing.Any, str]:
        """Returns a representation of `RegionProperty.value` in a form suitable for serialization in .json."""
        if reg_prop.prop_type == PropertyType.NDArray:
            path = f'{self._path}_{reg_prop.info.name}_{reg_prop.label}.npy'
            fname = Path(path).name
            np.save(path, reg_prop.value[0])
            return repr((fname, reg_prop.value[1]))  # return path to the serialized ndarray and the unit that the array is in
        return repr(reg_prop.value)

    def save(self):
        if self._dirty_flag:
            if self._label_img is not None:
                self._used_labels = set(np.unique(self._label_img))
                io.imsave(str(self._path), self._label_img, check_contrast=False)
                #im = Image.fromarray(self._label_img)
                #im.save(self._path)
            prop_dict = {
                self.label_hierarchy.code(label): {
                    'measurements': [
                        prop.to_dict(label_folder=self._path.parent, npy_fname=f'{self._path.name}_{prop.info.name}_{prop.label}.npy')
                            # 'name': prop.info.name,
                            # 'label': prop.label,
                            # 'value': self._serialize_prop_value(prop),
                            # # 'unit': prop.unit,
                            # 'prop_type': prop.prop_type,
                            # 'num_vals': prop.num_vals,
                            # 'val_names': prop.val_names,
                            # 'col_names': prop.col_names,
                            # 'row_names': prop.row_names,
                            # 'key': prop.info.key,
                            # 'description': prop.info.description,
                            # 'prop_comp_key': prop.prop_comp_key,
                            # 'local_key': prop.local_key,
                            # 'settings': prop.settings,
                            # 'up_to_date': prop.up_to_date
                        for prop in prop_dict.values()
                    ]
                } for label, prop_dict in self._region_props.items()
            }
            with open(f'{self._path}_measurements.json', 'w') as f:
                json.dump(prop_dict, f, indent=2)
            self._dirty_flag = False

    @classmethod
    def create2(cls, path: Path, image_size: typing.Tuple[int, int], label_info: LabelImgInfo, label_name: str) -> 'LabelImg':
        """Creates new `LabelImg` object.

        path: Path - where this label image should be stored
        image_size: (height, width)
        label_info: LabelImgInfo
        label_name: str - not used, stored in `label_info`
        """
        lbl = LabelImg(image_size)
        #lbl._type = label_type
        lbl._path = path
        #lbl.label_img_type = label_type
        lbl.label_info = label_info
        lbl.label_semantic = label_info.name
        lbl._load_measurements()
        return lbl

    def make_empty(self, size: typing.Tuple[int, int]):
        self._label_img = np.zeros(size, np.uint32)
        self._used_labels = {0}

    def set_image(self, img: np.ndarray):
        if self._label_img is not None and img is not None:
            if self._label_img.shape != img.shape:
                raise ValueError(f'The shape must be {self._label_img.shape}, got {img.shape}.')
            elif self._label_img.dtype != img.dtype:
                raise ValueError(f'The dtype must be {self._label_img.dtype}, got {img.dtype}.')
        self._label_img = img
        self._dirty_flag = True
        self._invalidate_measurements()

    def clone(self) -> 'LabelImg':
        lbl = LabelImg(self.size)
        lbl._path = self._path
        lbl._label_img = self._label_img.copy() if self._label_img is not None else None
        # lbl.label_img_type = self.label_img_type
        lbl.label_info = self.label_info
        lbl.label_semantic = self.label_semantic
        lbl._label_hierarchy = self.label_hierarchy
        lbl._used_labels = self._used_labels
        lbl._dirty_flag = self._dirty_flag
        return lbl

    def _compute_bbox(self):
        coords = np.nonzero(self._label_img)
        top, left = np.min(coords[0]), np.min(coords[1])
        bottom, right = np.max(coords[0]), np.max(coords[1])
        self._bug_bbox = [left, top, right, bottom]

    @property
    def region_props(self) -> Dict[int, Dict[str, RegionProperty]]:
        """Returns a dictionary of (label -> (property_key -> `RegionProperty`) pairs."""
        return self._region_props

    @region_props.setter
    def region_props(self, props: Dict[int, Dict[str, RegionProperty]]):
        for label, props in props.items():
            label_props = self._region_props.setdefault(label, dict())
            for prop_key, prop in props.items():
                label_props[prop_key] = prop
                self.property_added.emit(self, prop)
                #label_prop = label_props.setdefault(prop_key, RegionProperty())
                #label_prop.label = prop.label
                #label_prop.info = prop.info
                #label_prop.value = prop.value
                #label_prop.unit = prop.unit
                #label_prop.prop_type = prop.prop_type
                #label_prop.num_vals = prop.num_vals
        if self.has_valid_measurements():
            self.all_properties_valid.emit(self)
        self._dirty_flag = True

    def clear_region_props(self):
        self._region_props.clear()

    def set_region_prop(self, region_label: int, prop: RegionProperty):
        # self.region_props = {region_label: {prop.info.key: prop}}
        self.region_props = {region_label: {prop.prop_key: prop}}
        if prop in self.prop_list:  # meaning the combination of (property_key, region_label) is in self.prop_list (it does not take into account the actual value of the property)
            self.prop_list.remove(prop)
        self.prop_list.add(prop)

    def get_region_props(self, region_label: int) -> Optional[Dict[str, RegionProperty]]:
        """Returns (property_key -> `RegionProperty) for `region_label`."""
        if region_label in self._region_props:
            return self._region_props[region_label]
        return None

    def remove_property(self, label: int, property_key: str):
        if label not in self._region_props:
            return
        if property_key not in self._region_props[label]:
            return
        reg_prop: RegionProperty = self._region_props[label][property_key]
        del self._region_props[label][property_key]
        self.prop_list.remove(reg_prop)
        self.set_dirty()
        self.property_removed.emit(self, reg_prop)
        if self.has_valid_measurements():
            self.all_properties_valid.emit(self)

    @property
    def label_hierarchy(self) -> LabelHierarchy:
        return self.label_info.label_hierarchy

    @label_hierarchy.setter
    def label_hierarchy(self, lab_hier: LabelHierarchy):
        pass
        # TODO handle possible changes to the label image

    def __getitem__(self, level: int) -> Optional[np.ndarray]:
        """Returns a label image generated from this one, where all the labels are from the level `level` of `label_hierarchy`."""
        if self.label_hierarchy is None:
            return self.label_image
        if level >= len(self.label_info.label_hierarchy.hierarchy_levels):
            return None
        level_mask = self.label_info.label_hierarchy.hierarchy_levels[level].accumulated_bit_mask
        return np.bitwise_and(self.label_image, level_mask)

    @property
    def used_labels(self) -> Optional[typing.Set[int]]:
        """Returns the set of labels that are present in this label image."""
        if self._dirty_flag or self._used_labels is None:
            self._used_labels = set(np.unique(self.label_image))
            self._dirty_flag = False
        return self._used_labels

    def set_dirty(self):
        self._dirty_flag = True
        self._invalidate_measurements()
        self.timestamp = time.time()

    def _load_measurements(self):
        self._dirty_flag = False
        if (path := Path(f'{str(self._path)}_measurements.json')).exists():
            with open(path) as f:
                props = json.load(f)
            for code in props.keys():
                for prop_dict in props[code]['measurements']:
                    prop_dict['label_image_folder'] = str(self.path.parent)
                    prop_dict['image_path'] = self._path
                    reg_prop = RegionProperty.from_dict(prop_dict)
                    if reg_prop is None:
                        continue
                    # TODO remove these two if's eventually
                    if (prop_key := reg_prop.prop_comp_key).startswith('arthropod_describer.'):
                        _, suffix = prop_key.split('arthropod_describer.')
                        reg_prop.prop_comp_key = f'maphis.{suffix}'
                        self._dirty_flag = True
                    if (prop_key := reg_prop.info.key).startswith('arthropod_describer.'):
                        _, suffix = prop_key.split('arthropod_describer.')
                        reg_prop.info.key = f'maphis.{suffix}'
                        self._dirty_flag = True
                    self.set_region_prop(reg_prop.label, reg_prop)

    def rotate(self, ccw: bool):
        unload = self._label_img is not None
        if self._label_img is None:
            self.reload()
        self._label_img = ndimage.rotate(self._label_img, 90 if ccw else -90, order=0, prefilter=False)
        self.size = self._label_img.shape[::-1]
        self._dirty_flag = True
        if unload:
            self.unload()

    #def save(self):
    #    if self._dirty_flag:
    #        self.unload()
    #        self._dirty_flag = False

    def resize(self, factor: float):
        unload = self._label_img is not None
        if self._label_img is None:
            self.reload()
        im = Image.fromarray(self._label_img)
        sz = (int(round(factor * self.size[0])),
              int(round(factor * self.size[1])))
        self.size = sz
        im = im.resize(sz, resample=Image.NEAREST)
        self._label_img = np.asarray(im, dtype=np.uint32)
        self._dirty_flag = True
        if unload:
            self.unload()

    def mask_for(self, label: int) -> np.ndarray:
        """Returns a binary (np.bool) image where `True` values mark pixels whose value is `label` or are descendants of `label`."""
        level = self.label_hierarchy.get_level(label)
        level_img = self[level]
        return level_img == label

    @property
    def has_unsaved_changes(self) -> bool:
        return self._dirty_flag

    @property
    def bbox(self) -> typing.Optional[typing.Tuple[int, int, int, int]]:
        mask = self._label_img > 0
        rr, cc = np.nonzero(mask)
        if len(rr) == 0:
            return None
        top, left, bottom, right = np.min(rr), np.min(cc), np.max(rr), np.max(cc)

        return left, top, right - left + 1, bottom - top + 1

    def _invalidate_measurements(self):
        need_to_recompute = False
        for region_label, region_props in self._region_props.items():
            for region_prop, region_prop in region_props.items():
                region_prop.up_to_date = False
                need_to_recompute = True
        # TODO fire a signal
        if need_to_recompute:
            self.properties_need_recomputation.emit(self)

    def has_valid_measurements(self) -> bool:
        for region_label, region_props in self._region_props.items():
            for prop_key, region_prop in region_props.items():
                if not region_prop.up_to_date:
                    return False
        return True

region_props: Dict[int, Dict[str, RegionProperty]] property writable

Returns a dictionary of (label -> (property_key -> RegionProperty) pairs.

used_labels: Optional[typing.Set[int]] property

Returns the set of labels that are present in this label image.

__getitem__(level)

Returns a label image generated from this one, where all the labels are from the level level of label_hierarchy.

Source code in maphis\common\label_image.py
def __getitem__(self, level: int) -> Optional[np.ndarray]:
    """Returns a label image generated from this one, where all the labels are from the level `level` of `label_hierarchy`."""
    if self.label_hierarchy is None:
        return self.label_image
    if level >= len(self.label_info.label_hierarchy.hierarchy_levels):
        return None
    level_mask = self.label_info.label_hierarchy.hierarchy_levels[level].accumulated_bit_mask
    return np.bitwise_and(self.label_image, level_mask)

create2(path, image_size, label_info, label_name) classmethod

Creates new LabelImg object.

path: Path - where this label image should be stored image_size: (height, width) label_info: LabelImgInfo label_name: str - not used, stored in label_info

Source code in maphis\common\label_image.py
@classmethod
def create2(cls, path: Path, image_size: typing.Tuple[int, int], label_info: LabelImgInfo, label_name: str) -> 'LabelImg':
    """Creates new `LabelImg` object.

    path: Path - where this label image should be stored
    image_size: (height, width)
    label_info: LabelImgInfo
    label_name: str - not used, stored in `label_info`
    """
    lbl = LabelImg(image_size)
    #lbl._type = label_type
    lbl._path = path
    #lbl.label_img_type = label_type
    lbl.label_info = label_info
    lbl.label_semantic = label_info.name
    lbl._load_measurements()
    return lbl

get_region_props(region_label)

Returns (property_key -> RegionProperty) forregion_label`.

Source code in maphis\common\label_image.py
def get_region_props(self, region_label: int) -> Optional[Dict[str, RegionProperty]]:
    """Returns (property_key -> `RegionProperty) for `region_label`."""
    if region_label in self._region_props:
        return self._region_props[region_label]
    return None

mask_for(label)

Returns a binary (np.bool) image where True values mark pixels whose value is label or are descendants of label.

Source code in maphis\common\label_image.py
def mask_for(self, label: int) -> np.ndarray:
    """Returns a binary (np.bool) image where `True` values mark pixels whose value is `label` or are descendants of `label`."""
    level = self.label_hierarchy.get_level(label)
    level_img = self[level]
    return level_img == label

LabelImgInfo

Info about a particular LabelImg object.

Source code in maphis\common\label_image.py
class LabelImgInfo:
    """
    Info about a particular `LabelImg` object.
    """

    def __init__(self, label_name: str, is_default: bool, constrain_to: typing.Dict[str, typing.List[str]], label_hierarchy: Optional[LabelHierarchy]=None):
        self.name: str = label_name
        self.is_default = is_default  # if this is the default label image to show at startup
        # self.constrain_to: Optional[str] = always_constrain_to  # whether the editing should be always constrained to some other label image
        # self.can_constrain_to: Optional[List[str]] = allow_constrain_to  # what other label images can serve as constraints
        #
        # if self.constrain_to is not None:
        #     self.can_constrain_to = None
        self.constrain_to: typing.Dict[str, typing.List[str]] = constrain_to
        self.label_hierarchy: LabelHierarchy = label_hierarchy

    @property
    def can_be_constrained(self) -> bool:
        # return self.constrain_to is not None or self.can_constrain_to is not None
        return len(self.constrain_to) > 0

    def to_dict(self) -> typing.Dict[str, typing.Any]:
        return {
            'name': self.name,
            'label_hierarchy_file': self.label_hierarchy._fname,
            'constrain_to': [
                {
                    'label_image_name': label_img_name,
                    'regions': region_codes
                }
            for label_img_name, region_codes in self.constrain_to.items()],
            'is_default': self.is_default
        }

    @staticmethod
    def from_dict(_info_dict: typing.Dict[str, typing.Any]) -> 'LabelImgInfo':
        return LabelImgInfo(label_name=_info_dict['name'],
                            is_default=_info_dict['is_default'],
                            constrain_to={constraint_entry['label_image_name']: constraint_entry['regions'] for constraint_entry in _info_dict['constrain_to']})

Action

A class specifying the common interface for RegionComputation, PropertyComputation and GeneralAction.

Attributes:

Name Type Description
info Info

metadata

_user_params Dict[str, Param]

dictionary of user Params. It is up to the programmer under what keys the Params are stored.

_group str

a category where this Action belongs.

_settings_widget Optional[ParamWidget]

an optional ParamWidget if you want to parameters to the user

Source code in maphis\common\action.py
class Action:
    """A class specifying the common interface for `RegionComputation`, `PropertyComputation` and `GeneralAction`.

    Attributes:
        info (Info): metadata
        _user_params (Dict[str, Param]): dictionary of user `Param`s. It is up to the programmer under what
            keys the `Param`s are stored.
        _group (str): a category where this `Action` belongs.
        _settings_widget (Optional[ParamWidget]): an optional `ParamWidget` if you want to parameters to the user
    """

    FOLDER = ''
    TEMPLATE = ''
    default_user_params: typing.Dict[str, Param] = {}

    def __init__(self, info: typing.Optional[Info] = None):
        self.info = Info() if info is None else info
        self._user_params: typing.Dict[str, Param] = self.default_user_params
        self._group = 'General'
        self._default_call_func = None
        self._setting_widget: typing.Optional[ParamWidget] = None

    @property
    def user_params(self) -> typing.List[Param]:
        """
        Returns:
            list of `Param` instances.
        """
        return list(self._user_params.values())

    @property
    def user_params_dict(self) -> typing.Dict[str, Param]:
        return self._user_params

    @property
    def can_be_executed(self) -> typing.Tuple[bool, str]:
        """Determines whether this `Action` can be executed or not.

        Returns:
            `(True, <message>)` if this `Action` can be executed, otherwise `(False, <message>)`. Generally, `<message>`
             is presented to the user in a GUI dialog window to let the user know why the `Action` cannot be executed.
        """
        return True, ''

    @property
    def group(self) -> str:
        """Group the Action belongs to.
        It is used to organize `PropertyComputation`s, or where to put `GeneralAction`s in the application's
        menu bar.
        """
        return self._group

    def __hash__(self) -> int:
        return hash(self.info.key)

    def __eq__(self, other):
        return hash(self) == hash(other)

    @property
    def setting_widget(self) -> typing.Optional[QWidget]:
        """An optional widget containing parameters to customize the `Action`s behaviour.
        Returns:
            optional widget with user parameters.
        """
        if len(self._user_params) > 0 and self._setting_widget is None:
            self._setting_widget = ParamWidget(list(self._user_params.values()))
        return None if self._setting_widget is None else self._setting_widget.widget

    def _setup_params(self):
        pass

    def current_settings_to_str_dict(self) -> typing.Dict[str, typing.Dict[str, str]]:
        """Serialize the current values of user parameters provided by the `Action` into a dictionary.
        The returned dictionary has 2 entries:
            - 'standard_parameters' -> a dictionary of serialized `Param` values, keyed by the keys of
                `self._user_params`
            - 'custom_parameters' -> this can be anything, it is up to the `Action` implementation to correctly
                deserialize in the method `Action.setup_settings_from_dict`.

        Returns:
            dictionary of string representations of user parameters for this `Action`
        """
        return {
            'standard_parameters': {param_key: str(param.value) for param_key, param in self._user_params.items()},
            'custom_parameters': {}
        }

    def setup_settings_from_dict(self, _dict: typing.Dict[str, typing.Dict[str, str]]):
        """Deserializes parameter values from the provided `_dict` argument.
        Custom implementation of subclasses of `Action` must take care of properly deserializing values stored under the
        'custom_parameters' key.

        Args:
            _dict: string representations of parameter values
        """
        standard_params: typing.Dict[str, str] = _dict['standard_parameters']
        for param_key, param_value_str in standard_params.items():
            param_obj: Param = self.user_params_dict[param_key]
            param_obj.parse(param_value_str)

    def initialize(self) -> typing.Tuple[bool, str]:
        """Prepare everything before executing the `Action` such as loading heavy resources etc.

        Returns:
            `(True, <message>)` if the `Action` is properly initialized, `(False, <message>)` otherwise. Generally,
                `<message>` is displayed inside a GUI dialog window to let the user know why the initialization failed.
        """
        return True, ''

can_be_executed: typing.Tuple[bool, str] property

Determines whether this Action can be executed or not.

Returns:

Type Description
Tuple[bool, str]

(True, <message>) if this Action can be executed, otherwise (False, <message>). Generally, <message> is presented to the user in a GUI dialog window to let the user know why the Action cannot be executed.

group: str property

Group the Action belongs to. It is used to organize PropertyComputations, or where to put GeneralActions in the application's menu bar.

setting_widget: typing.Optional[QWidget] property

An optional widget containing parameters to customize the Actions behaviour. Returns: optional widget with user parameters.

user_params: typing.List[Param] property

Returns:

Type Description
List[Param]

list of Param instances.

current_settings_to_str_dict()

Serialize the current values of user parameters provided by the Action into a dictionary. The returned dictionary has 2 entries: - 'standard_parameters' -> a dictionary of serialized Param values, keyed by the keys of self._user_params - 'custom_parameters' -> this can be anything, it is up to the Action implementation to correctly deserialize in the method Action.setup_settings_from_dict.

Returns:

Type Description
Dict[str, Dict[str, str]]

dictionary of string representations of user parameters for this Action

Source code in maphis\common\action.py
def current_settings_to_str_dict(self) -> typing.Dict[str, typing.Dict[str, str]]:
    """Serialize the current values of user parameters provided by the `Action` into a dictionary.
    The returned dictionary has 2 entries:
        - 'standard_parameters' -> a dictionary of serialized `Param` values, keyed by the keys of
            `self._user_params`
        - 'custom_parameters' -> this can be anything, it is up to the `Action` implementation to correctly
            deserialize in the method `Action.setup_settings_from_dict`.

    Returns:
        dictionary of string representations of user parameters for this `Action`
    """
    return {
        'standard_parameters': {param_key: str(param.value) for param_key, param in self._user_params.items()},
        'custom_parameters': {}
    }

initialize()

Prepare everything before executing the Action such as loading heavy resources etc.

Returns:

Type Description
Tuple[bool, str]

(True, <message>) if the Action is properly initialized, (False, <message>) otherwise. Generally, <message> is displayed inside a GUI dialog window to let the user know why the initialization failed.

Source code in maphis\common\action.py
def initialize(self) -> typing.Tuple[bool, str]:
    """Prepare everything before executing the `Action` such as loading heavy resources etc.

    Returns:
        `(True, <message>)` if the `Action` is properly initialized, `(False, <message>)` otherwise. Generally,
            `<message>` is displayed inside a GUI dialog window to let the user know why the initialization failed.
    """
    return True, ''

setup_settings_from_dict(_dict)

Deserializes parameter values from the provided _dict argument. Custom implementation of subclasses of Action must take care of properly deserializing values stored under the 'custom_parameters' key.

Parameters:

Name Type Description Default
_dict Dict[str, Dict[str, str]]

string representations of parameter values

required
Source code in maphis\common\action.py
def setup_settings_from_dict(self, _dict: typing.Dict[str, typing.Dict[str, str]]):
    """Deserializes parameter values from the provided `_dict` argument.
    Custom implementation of subclasses of `Action` must take care of properly deserializing values stored under the
    'custom_parameters' key.

    Args:
        _dict: string representations of parameter values
    """
    standard_params: typing.Dict[str, str] = _dict['standard_parameters']
    for param_key, param_value_str in standard_params.items():
        param_obj: Param = self.user_params_dict[param_key]
        param_obj.parse(param_value_str)

GeneralAction

Bases: Action

Base class for custom GeneralAction.

Source code in maphis\common\action.py
class GeneralAction(Action):
    """Base class for custom [GeneralAction][maphis.common.action.GeneralAction].
    """

    FOLDER = 'general'
    TEMPLATE = 'general_action_template.py'

    def __init__(self, info: typing.Optional[Info] = None):
        super(GeneralAction, self).__init__(info)
        self._general_action_context: GeneralActionContext = GeneralActionContext.Application

    @classmethod
    def from_call_func(cls, _func: typing.Callable[[State, 'ActionContext'], None]) -> 'GeneralAction':
        if hasattr(_func, 'info'):
            info = _func.info
        else:
            info = None
        gen_act = GeneralAction(info)
        if hasattr(_func, '_group'):
            gen_act._group = _func._group
        if hasattr(_func, 'general_action_context'):
            gen_act._general_action_context = _func.general_action_context
        gen_act._default_call_func = _func

        return gen_act

    @abc.abstractmethod
    def __call__(self, state: State, context: 'ActionContext') -> None:
        if self._default_call_func is not None:
            return self._default_call_func(self, state, context)

    @property
    def general_action_context(self) -> GeneralActionContext:
        return self._general_action_context

GeneralActionContext

Bases: IntEnum

The necessary context in which a GeneralAction is enabled.

Source code in maphis\common\action.py
class GeneralActionContext(IntEnum):
    """The necessary context in which a [GeneralAction][maphis.common.action.GeneralAction] is enabled.
    """
    Application = 0
    Project = 1
    Photo = 2
    LabelImg = 3
    LabelEditor = 4
    Measurements = 5

PropertyComputation

Bases: Action

Base class for custom PropertyComputations

Attributes:

Name Type Description
_units UnitRegistry

the unit registry (pint.UnitRegistry) that is used by the application.

_pixels Unit

(pint.Unit): pixels unit

_no_unit Unit

no unit for unitless dimensionless values

example_props_dict Dict[str, RegionProperty]

dictionary of exemplary RegionPropertys

_export_target str

the values computed by this computation will be stored in the sheet (xlsx export) or file (csv export) named by the value specified by this variable.

Source code in maphis\common\action.py
class PropertyComputation(Action):
    """Base class for custom `PropertyComputation`s

    Attributes:
        _units (pint.UnitRegistry): the unit registry (`pint.UnitRegistry`) that is used by the application.
        _pixels: (pint.Unit): pixels unit
        _no_unit (pint.Unit): `no unit` for unitless dimensionless values
        example_props_dict (Dict[str, RegionProperty]): dictionary of exemplary `RegionProperty`s
        _export_target (str): the values computed by this computation will be stored in the sheet (xlsx export) or
            file (csv export) named by the value specified by this variable.
    """
    FOLDER = 'properties'
    TEMPLATE = 'property_computation_template.py'

    def __init__(self, info: typing.Optional[Info] = None):
        super(PropertyComputation, self).__init__(info)
        # doc_dict = get_dict_from_doc_str(self.__doc__)
        self._region_restricted = self.__doc__ is not None and "REGION_RESTRICTED" in self.__doc__
        self._units: pint.UnitRegistry = ureg
        self._pixels: pint.Unit = self._units['pixel']
        self._no_unit: pint.Unit = self._units['dimensionless']
        self.example_props_dict: typing.Dict[str, RegionProperty] = {}
        self._export_target: str = 'common'
        self._computes_dict: typing.Dict[str, Info] = {}

    @abc.abstractmethod
    def __call__(self, photo: Photo, region_labels: typing.List[int], regions_cache: RegionsCache, props: typing.List[str]) -> typing.List[RegionProperty]:
        """The actual computation of the property. The property is computed for the `photo` and only for
        regions specified by `region_labels`. If this computation allows to compute various variants, they will be
        specified in `props` (for example, when you are computing GLCM properties, you have these options to compute:
        'Contrast', 'Dissimilarity', 'Energy' and some more. These are considered as variants).

        Args:
            photo: photo for which to compute the property
            region_labels: labels of regions that we want to compute the property for
            regions_cache: regions cache for efficient access to regions
            props: which variants of the property (if any) to compute

        Returns:
            list of properties computed for the photo.
        """
        if self._default_call_func is not None:
            return self._default_call_func(self, photo, region_labels, regions_cache, props)
        return []

    @property
    def region_restricted(self) -> bool:
        return self._region_restricted

    @property
    @abc.abstractmethod
    def computes(self) -> typing.Dict[str, Info]:
        return {k: rp.info for k, rp in self.example_props_dict.items()}

    @abc.abstractmethod
    def example(self, prop_name: str) -> RegionProperty:
        if len(self.example_props_dict) > 0:
            return copy.deepcopy(self.example_props_dict[prop_name])
        prop = RegionProperty()
        prop.prop_comp_key = self.info.key
        prop.local_key = prop_name
        prop.info = copy.deepcopy(self.info)
        prop.settings = self.current_settings_to_str_dict()
        prop.value = ScalarValue(1 * self._no_unit)
        return prop

    def target_worksheet(self, prop_name: str) -> str:
        return self._export_target

    @property
    @abc.abstractmethod
    def requested_props(self) -> typing.List[str]:
        return []

    @classmethod
    def get_representation(cls, reg_prop: RegionProperty, role: Qt.ItemDataRole, alternative_rep: bool=False) -> typing.Any:
        if role == Qt.ItemDataRole.DisplayRole:
            return str(reg_prop.value)
        return None

    @classmethod
    def from_call_func(cls, _func) -> 'PropertyComputation':
        if hasattr(_func, 'info'):
            info = _func.info
        else:
            info = None
        comp = PropertyComputation(info)
        if hasattr(_func, '_group'):
            comp._group = _func._group
        if (params_dict := getattr(_func, 'maphis_params', None)) is not None:
            comp._user_params = params_dict
        comp._default_call_func = _func
        comp.example_props_dict = _func.example_property
        comp._export_target = _func.export_target
        return comp

__call__(photo, region_labels, regions_cache, props) abstractmethod

The actual computation of the property. The property is computed for the photo and only for regions specified by region_labels. If this computation allows to compute various variants, they will be specified in props (for example, when you are computing GLCM properties, you have these options to compute: 'Contrast', 'Dissimilarity', 'Energy' and some more. These are considered as variants).

Parameters:

Name Type Description Default
photo Photo

photo for which to compute the property

required
region_labels List[int]

labels of regions that we want to compute the property for

required
regions_cache RegionsCache

regions cache for efficient access to regions

required
props List[str]

which variants of the property (if any) to compute

required

Returns:

Type Description
List[RegionProperty]

list of properties computed for the photo.

Source code in maphis\common\action.py
@abc.abstractmethod
def __call__(self, photo: Photo, region_labels: typing.List[int], regions_cache: RegionsCache, props: typing.List[str]) -> typing.List[RegionProperty]:
    """The actual computation of the property. The property is computed for the `photo` and only for
    regions specified by `region_labels`. If this computation allows to compute various variants, they will be
    specified in `props` (for example, when you are computing GLCM properties, you have these options to compute:
    'Contrast', 'Dissimilarity', 'Energy' and some more. These are considered as variants).

    Args:
        photo: photo for which to compute the property
        region_labels: labels of regions that we want to compute the property for
        regions_cache: regions cache for efficient access to regions
        props: which variants of the property (if any) to compute

    Returns:
        list of properties computed for the photo.
    """
    if self._default_call_func is not None:
        return self._default_call_func(self, photo, region_labels, regions_cache, props)
    return []

RegionComputation

Bases: Action

Base class for custom RegionComputations.

Source code in maphis\common\action.py
class RegionComputation(Action):
    """Base class for custom `RegionComputation`s.
    """
    FOLDER = 'regions'
    TEMPLATE = 'region_computation_template.py'

    def __init__(self, info: typing.Optional[Info] = None):
        super(RegionComputation, self).__init__(info)
        self._region_restricted = False if self.__doc__ is None else "REGION_RESTRICTED" in self.__doc__

    @classmethod
    def from_call_func(cls, func: typing.Callable[[Photo, typing.Optional[typing.Set[int]], typing.Optional[Storage]], typing.List[LabelImg]]) -> 'RegionComputation':
        if hasattr(func, 'info'):
            info = func.info
        else:
            info = None
        comp = RegionComputation(info)
        comp._default_call_func = func
        if hasattr(func, '_group'):
            comp._group = func._group
        if (params_dict := getattr(func, 'maphis_params', None)) is not None:
            comp._user_params = params_dict
        return comp

    @abc.abstractmethod
    def __call__(self, photo: Photo, labels: typing.Optional[typing.Set[int]] = None, storage: typing.Optional[Storage]=None) -> typing.List[LabelImg]:
        """The actual implementation of the functionality that is to be provided.

        Args:
            photo: the photo that the `RegionComputation` should compute for.
            labels: restrict the computation on this set of labels
            storage: the `Storage` the photo belongs to

        Returns:
            list of `LabelImg`s that were modified. The `LabelImg`s in the list must originate from `photo`.
        """
        if self._default_call_func is not None:
            return self._default_call_func(self, photo, labels, storage)
        return []

    @property
    def region_restricted(self) -> bool:
        return self._region_restricted

__call__(photo, labels=None, storage=None) abstractmethod

The actual implementation of the functionality that is to be provided.

Parameters:

Name Type Description Default
photo Photo

the photo that the RegionComputation should compute for.

required
labels Optional[Set[int]]

restrict the computation on this set of labels

None
storage Optional[Storage]

the Storage the photo belongs to

None

Returns:

Type Description
List[LabelImg]

list of LabelImgs that were modified. The LabelImgs in the list must originate from photo.

Source code in maphis\common\action.py
@abc.abstractmethod
def __call__(self, photo: Photo, labels: typing.Optional[typing.Set[int]] = None, storage: typing.Optional[Storage]=None) -> typing.List[LabelImg]:
    """The actual implementation of the functionality that is to be provided.

    Args:
        photo: the photo that the `RegionComputation` should compute for.
        labels: restrict the computation on this set of labels
        storage: the `Storage` the photo belongs to

    Returns:
        list of `LabelImg`s that were modified. The `LabelImg`s in the list must originate from `photo`.
    """
    if self._default_call_func is not None:
        return self._default_call_func(self, photo, labels, storage)
    return []

action_info(name, description, group='General')

Decorator to provide information about Action subclasses. By an Action subclass it is understood either classes that subclass GeneralAction, RegionComputation or PropertyComputation, or functions that are decorated by @region_computation, @scalar_property_computation, @vector_property_computation or @general_action.

Parameters:

Name Type Description Default
name str

the name - how it would be shown in GUI

required
description str

description of what it does

required
group str

category - for GeneralActions it determines the placement in the menu bar, e.g. 'File:Export:as JSON' will make it accessible in the menu bar as 'File>Export>as Json'. For PropertyComputations it determines the category in the dialog for computing new properties.

'General'

Returns:

Type Description
Union[GeneralAction, PropertyComputation, RegionComputation, Callable[[Any], Any]]

the decorated Action subclass or function

Source code in maphis\common\action.py
def action_info(name: str, description: str, group: str="General")\
        -> typing.Union[GeneralAction, PropertyComputation, RegionComputation, typing.Callable[[typing.Any], typing.Any]]:
    """Decorator to provide information about [Action][maphis.common.action.Action] subclasses. By an [Action][maphis.common.action.Action] subclass it is understood either
    classes that subclass [GeneralAction][maphis.common.action.GeneralAction], [RegionComputation][maphis.common.action.RegionComputation]
     or [PropertyComputation][maphis.common.action.PropertyComputation], or functions that are
    decorated by [@region_computation][maphis.common.action.region_computation], [@scalar_property_computation][maphis.common.action.scalar_property_computation],
    [@vector_property_computation][maphis.common.action.vector_property_computation] or [@general_action][maphis.common.action.general_action].

    Args:
        name (str): the name - how it would be shown in GUI
        description (str): description of what it does
        group (str): category - for `GeneralAction`s it determines the placement in the menu bar, e.g. 'File:Export:as JSON' will make it accessible
            in the menu bar as 'File>Export>as Json'. For `PropertyComputation`s it determines the category in the
            dialog for computing new properties.

    Returns:
        the decorated `Action` subclass or function
    """
    def wrap(_obj: object):
        if inspect.isclass(_obj):
            old_init = _obj.__init__

            def __init__(self, *args, **kwargs):
                old_init(self, *args, **kwargs)
                self.info = Info(name, description, f'{_obj.__module__}.{_obj.__name__}')
                self._group = group
            _obj.__init__ = __init__
            return _obj
        elif inspect.isfunction(_obj):
            func_name = _obj.__name__
            _obj.info = Info(name, description, f'{_obj.__module__}.{func_name}')
            _obj._group = group
            return _obj
    return wrap

general_action(name, description, group='General', general_action_context=GeneralActionContext.Application)

Decorator to turn a function of appropriate signature into a GeneralAction subclass. The function signature is expected to be as follows: (GeneralAction, State, ActionContext) -> None

<func_name> is used as a key of your computation in MAPHIS and will be stored in project files to correctly identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret name for your function.

Parameters:

Name Type Description Default
name str

name of the GeneralAction

required
description str

what this action does

required
group str

placement of this action in the menu bar, e.g. File:Save as:Json will place this action under File>Save as>Json menu bar entry.

'General'
general_action_context GeneralActionContext

what is the necessary context for this action to be enabled in

Application

Returns:

Type Description
Callable[[GeneralAction, State, ActionContext], None]

the decorated function which will be turned into GeneralAction by MAPHIS.

Source code in maphis\common\action.py
def general_action(name: str, description: str, group: str="General",
                   general_action_context: GeneralActionContext=GeneralActionContext.Application) \
        -> typing.Callable[[GeneralAction, State, 'ActionContext'], None]:
    """Decorator to turn a function of appropriate signature into a `GeneralAction` subclass.
    The function signature is expected to be as follows:
    <func_name>(`GeneralAction`, `State`, `ActionContext`) -> None

    `<func_name>` is used as a key of your computation in MAPHIS and will be stored in project files to correctly
    identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret
    name for your function.

    Args:
        name: name of the `GeneralAction`
        description: what this action does
        group: placement of this action in the menu bar, e.g. `File:Save as:Json` will place this action under `File>Save as>Json` menu bar entry.
        general_action_context (GeneralActionContext): what is the necessary context for this action to be enabled in

    Returns:
       the decorated function which will be turned into `GeneralAction` by MAPHIS.
    """

    def wrap(_func: typing.Callable[[GeneralAction, State, 'ActionContext'], None]):
        action_wrapped = action_info(name, description, group)(_func)
        action_wrapped.action_type = 'general'
        action_wrapped.general_action_context = general_action_context
        return action_wrapped
    return wrap

param_bool(name, description, key, default_value)

Decorator that adds a boolean user parameter to subclasses of GeneralAction, RegionComputation or PropertyComputation or functions that have been decorated with @general_action, @region_computation or @property_computation decorators.

Parameters:

Name Type Description Default
name str

name of the parameter - how it will be presented in the GUI

required
description str

what this parameter affects

required
key str

how this parameter will be referenced to in your implementation, but also in project files.

required
default_value bool

default value

required

Returns:

Type Description
Union[GeneralAction, PropertyComputation, RegionComputation, Callable[[Any], Any]]

the decorated Action or decorated function with the added parameter

Source code in maphis\common\action.py
def param_bool(name: str, description: str, key: str, default_value: bool) \
    -> typing.Union[GeneralAction, PropertyComputation, RegionComputation, typing.Callable[[typing.Any], typing.Any]]:
    """ Decorator that adds a boolean user parameter to subclasses of `GeneralAction`, `RegionComputation` or
    `PropertyComputation` or functions that have been decorated with `@general_action`, `@region_computation` or
    `@property_computation` decorators.

    Args:
        name: name of the parameter - how it will be presented in the GUI
        description (str): what this parameter affects
        key (str): how this parameter will be referenced to in your implementation, but also in project files.
        default_value (bool): default value

    Returns:
        the decorated `Action` or decorated function with the added parameter
    """
    def wrap(_obj: object):
        param = ParamBuilder().bool_param().name(name).description(description).key(key) \
            .default_value(default_value).build()
        if inspect.isfunction(_obj):
            if not hasattr(_obj, 'maphis_params'):
                _obj.maphis_params = {}
            _obj.maphis_params[param.key] = param
        elif inspect.isclass(_obj):
            if not hasattr(_obj, 'default_user_params'):
                _obj.default_user_params = {}
            _obj.default_user_params[param.key] = param
        return _obj
    return wrap

param_int(name, description, key, default_value, min_value, max_value)

Decorator that adds an integer user parameter to subclasses of GeneralAction, RegionComputation or PropertyComputation or functions that have been decorated with @general_action, @region_computation or @property_computation decorators.

Parameters:

Name Type Description Default
name str

name of the parameter - how it will be presented in the GUI

required
description str

what this parameter affects

required
key str

how this parameter will be referenced to in your implementation, but also in project files.

required
default_value int

default value

required
min_value int

the lowest permissible value

required
max_value int

the highest permissible value

required

Returns:

Type Description
Union[GeneralAction, PropertyComputation, RegionComputation, Callable[[Any], Any]]

the decorated Action or decorated function with the added parameter

Source code in maphis\common\action.py
def param_int(name: str, description: str, key: str, default_value: int, min_value: int, max_value: int) \
    -> typing.Union[GeneralAction, PropertyComputation, RegionComputation, typing.Callable[[typing.Any], typing.Any]]:
    """ Decorator that adds an integer user parameter to subclasses of `GeneralAction`, `RegionComputation` or
    `PropertyComputation` or functions that have been decorated with `@general_action`, `@region_computation` or
    `@property_computation` decorators.

    Args:
        name (str): name of the parameter - how it will be presented in the GUI
        description (str): what this parameter affects
        key (str): how this parameter will be referenced to in your implementation, but also in project files.
        default_value (int): default value
        min_value (int): the lowest permissible value
        max_value (int): the highest permissible value

    Returns:
        the decorated `Action` or decorated function with the added parameter
    """
    def wrap(_obj: object):
        param = ParamBuilder().int_param().name(name).description(description).key(key) \
            .default_value(default_value).min_value(min_value).max_value(max_value).build()
        if inspect.isfunction(_obj):
            if not hasattr(_obj, 'maphis_params'):
                _obj.maphis_params = {}
            _obj.maphis_params[param.key] = param
        elif inspect.isclass(_obj):
            if not hasattr(_obj, 'default_user_params'):
                _obj.default_user_params = {}
            _obj.default_user_params[param.key] = param
        return _obj
    return wrap

param_string(name, description, key, default_value)

Decorator that adds a string user parameter to subclasses of GeneralAction, RegionComputation or PropertyComputation or functions that have been decorated with @general_action, @region_computation or @property_computation decorators.

Parameters:

Name Type Description Default
name str

name of the parameter - how it will be presented in the GUI

required
description str

what this parameter affects

required
key str

how this parameter will be referenced to in your implementation, but also in project files.

required
default_value str

default value

required

Returns:

Type Description
Union[GeneralAction, PropertyComputation, RegionComputation, Callable[[Any], Any]]

the decorated Action or decorated function with the added parameter

Source code in maphis\common\action.py
def param_string(name: str, description: str, key: str, default_value: str) \
    -> typing.Union[GeneralAction, PropertyComputation, RegionComputation, typing.Callable[[typing.Any], typing.Any]]:
    """ Decorator that adds a string user parameter to subclasses of `GeneralAction`, `RegionComputation` or
    `PropertyComputation` or functions that have been decorated with `@general_action`, `@region_computation` or
    `@property_computation` decorators.

    Args:
        name: name of the parameter - how it will be presented in the GUI
        description (str): what this parameter affects
        key (str): how this parameter will be referenced to in your implementation, but also in project files.
        default_value (str): default value

    Returns:
        the decorated `Action` or decorated function with the added parameter
    """
    def wrap(_obj: object):
        param = ParamBuilder().string_param().name(name).description(description).key(key) \
            .default_value(default_value).build()
        if inspect.isfunction(_obj):
            if not hasattr(_obj, 'maphis_params'):
                _obj.maphis_params = {}
            _obj.maphis_params[param.key] = param
        elif inspect.isclass(_obj):
            if not hasattr(_obj, 'default_user_params'):
                _obj.default_user_params = {}
            _obj.default_user_params[param.key] = param
        return _obj
    return wrap

region_computation(name, description, group='General')

Decorator to turn a function of appropriate signature into a RegionComputation subclass. The function signature is expected to be one of these two: 1. (RegionComputation, Photo, *args) -> List[LabelImg] 2. (RegionComputation, Photo, Optional[Set[int]], Storage) -> List[LabelImg]

<func_name> is used as a key of your computation in MAPHIS and will be stored in project files to correctly identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret name for your function.

Parameters:

Name Type Description Default
name str

name of the RegionComputation

required
description str

what this computation does

required
group str

category, most likely will be 'Segmentation' for RegionComputation, but not limited to.

'General'

Returns:

Type Description
Callable[[RegionComputation, Photo, Optional[Set[int]], Storage], List[LabelImg]]

the decorated function which will be turned into RegionComputation by MAPHIS.

Source code in maphis\common\action.py
def region_computation(name: str, description: str, group: str="General") \
        -> typing.Callable[[RegionComputation, Photo, typing.Optional[typing.Set[int]], Storage], typing.List[LabelImg]]:
    """Decorator to turn a function of appropriate signature into a [`RegionComputation`][maphis.common.action.RegionComputation] subclass.
    The function signature is expected to be one of these two:
    1. <func_name>(`RegionComputation`, `Photo`, *args) -> List[`LabelImg`]
    2. <func_name>(`RegionComputation`, `Photo`, Optional[Set[int]], `Storage`) -> List[`LabelImg`]

    `<func_name>` is used as a key of your computation in MAPHIS and will be stored in project files to correctly
    identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret
    name for your function.

    Args:
        name: name of the `RegionComputation`
        description: what this computation does
        group: category, most likely will be 'Segmentation' for `RegionComputation`, but not limited to.

    Returns:
       the decorated function which will be turned into `RegionComputation` by MAPHIS.
    """
    def wrap(_func: typing.Callable[[RegionComputation, Photo, typing.Optional[typing.Set[int]], Storage], typing.List[LabelImg]]):
        action_wrapped = action_info(name, description, group)(_func)
        action_wrapped.action_type = 'region'
        return action_wrapped
    return wrap

scalar_property_computation(name, description, group, export_target, default_value)

Decorator to turn a function of appropriate signature into a PropertyComputation subclass that generates scalar properties. The function signature is expected to be as follows: (PropertyComputation, Photo, List[int], RegionsCache, List[str]) -> List[RegionProperty]

<func_name> is used as a key of your computation in MAPHIS and will be stored in project files to correctly identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret name for your function.

Parameters:

Name Type Description Default
name str

name of the PropertyComputation

required
description str

what kind of scalar property this computation computes

required
group str

category of property, e.g., 'Dimensions', 'Shape',... (see the Compute new measurements dialog in MAPHIS)

required
export_target str

name of the sheet in Excel spreadsheet where values of this property will be stored. In case of CSV export, export_target will be a suffix of one of the generated csv files.

required
default_value ScalarValue

default value for this property

required

Returns:

Type Description
Callable[[PropertyComputation, Photo, List[int], RegionsCache, List[str]], List[RegionProperty]]

the decorated function which will be turned into PropertyComputation by MAPHIS.

Source code in maphis\common\action.py
def scalar_property_computation(name: str, description: str, group: str, export_target: str, default_value: ScalarValue)\
        -> typing.Callable[[PropertyComputation, Photo, typing.List[int], RegionsCache, typing.List[str]], typing.List[RegionProperty]]:
    """Decorator to turn a function of appropriate signature into a [`PropertyComputation`][maphis.common.action.PropertyComputation] subclass that generates
    scalar properties.
    The function signature is expected to be as follows:
    <func_name>([`PropertyComputation`][maphis.common.action.PropertyComputation], [`Photo`][maphis.common.photo.Photo], List[int], `RegionsCache`, List[str]) -> List[`RegionProperty`]

    `<func_name>` is used as a key of your computation in MAPHIS and will be stored in project files to correctly
    identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret
    name for your function.

    Args:
        name: name of the [`PropertyComputation`][maphis.common.action.PropertyComputation]
        description: what kind of scalar property this computation computes
        group: category of property, e.g., 'Dimensions', 'Shape',... (see the `Compute new measurements` dialog in MAPHIS)
        export_target (str): name of the sheet in Excel spreadsheet where values of this property will be stored. In case of CSV export, `export_target` will be a suffix of one of the generated csv files.
        default_value (ScalarValue): default value for this property

    Returns:
        the decorated function which will be turned into [`PropertyComputation`][maphis.common.action.PropertyComputation] by MAPHIS.
    """
    def wrap(_func: typing.Callable[[PropertyComputation, Photo, typing.List[int], RegionsCache, typing.List[str]], typing.List[RegionProperty]]):
        _comp_wrapped = _define_property_computation(name, description, group, export_target)(_func)
        prop = RegionProperty()
        prop.value = default_value
        prop.label = 0
        prop.prop_comp_key = _comp_wrapped.info.key
        prop.local_key = _func.__name__
        prop.info = _comp_wrapped.info
        _comp_wrapped.example_property = {_func.__name__: prop}
        return _comp_wrapped

    return wrap

vector_property_computation(name, description, group, export_target, value_count, value_names, unit)

Decorator to turn a function of appropriate signature into a PropertyComputation subclass that generates vector properties. The function signature is expected to be as follows: <func_name>(PropertyComputation, Photo, List[int], RegionsCache, List[str]) -> List[RegionProperty]

<func_name> is used as a key of your computation in MAPHIS and will be stored in project files to correctly identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret name for your function.

Parameters:

Name Type Description Default
name str

name of the PropertyComputation

required
description str

what kind of vector property this computation computes

required
group str

category of property, e.g., 'Dimensions', 'Shape',... (see the Compute new measurements dialog in MAPHIS)

required
export_target str
required

Returns:

Type Description
Callable[[PropertyComputation, Photo, List[int], RegionsCache, List[str]], List[RegionProperty]]

the decorated function which will be turned into PropertyComputation by MAPHIS.

Source code in maphis\common\action.py
def vector_property_computation(name: str, description: str, group: str, export_target: str,
                                value_count: int, value_names: typing.List[str], unit: pint.Unit) -> \
        typing.Callable[[PropertyComputation, Photo, typing.List[int], RegionsCache, typing.List[str]], typing.List[RegionProperty]]:
    """Decorator to turn a function of appropriate signature into a `PropertyComputation` subclass that generates
    vector properties.
    The function signature is expected to be as follows:
    `<func_name>(PropertyComputation, Photo, List[int], RegionsCache, List[str]) -> List[RegionProperty]`

    `<func_name>` is used as a key of your computation in MAPHIS and will be stored in project files to correctly
    identify the origin of results (segmentations, properties, etc.), so it's good provide clear and easy to interpret
    name for your function.

    Args:
        name: name of the `PropertyComputation`
        description: what kind of vector property this computation computes
        group: category of property, e.g., 'Dimensions', 'Shape',... (see the `Compute new measurements` dialog in MAPHIS)
        export_target (str):

    Returns:
       the decorated function which will be turned into `PropertyComputation` by MAPHIS.
    """
    def wrap(_func: typing.Callable[[Photo, typing.List[int], RegionsCache, typing.List[str]], typing.List[RegionProperty]]):
        _comp_wrapped = _define_property_computation(name, description, group, export_target)(_func)
        prop = RegionProperty()
        prop.value = VectorValue(([0] * value_count) * unit)
        prop.value._column_names = value_names
        prop.label = 0
        prop.prop_comp_key = _comp_wrapped.info.key
        prop.local_key = _func.__name__
        prop.info = _comp_wrapped.info
        _comp_wrapped.example_property = {_func.__name__: prop}
        return _comp_wrapped
    return wrap

Region dataclass

Object representing a region. mask and image attributes are subimages of the original label image and photo that the region represented by this object originates from.

Attributes:

Name Type Description
label int

the integer label of the region

mask ndarray

binary mask of the region

image ndarray

subimage of the photo this region covers

bbox Tuple[int, int, int, int]

corners of the bounding box of this region - (top, left, bottom, right)

Source code in maphis\common\regions_cache.py
@dataclasses.dataclass
class Region:
    """Object representing a region. `mask` and `image` attributes are subimages of the original label image and photo that
    the region represented by this object originates from.

    Attributes:
        label (int): the integer label of the region
        mask (np.ndarray): binary mask of the region
        image (np.ndarray): subimage of the photo this region covers
        bbox (Tuple[int, int, int, int]): corners of the bounding box of this region - (top, left, bottom, right)
    """

    label: int
    mask: np.ndarray
    image: np.ndarray
    bbox: typing.Tuple[int, int, int, int]  # top, left, bottom, right

RegionsCache

Stores Region objects, one for each integer label present in region_labels. Intended for efficient access to regions so you don't have to repeatedly search for them.

Attributes:

Name Type Description
regions typing.Dict[int, [`Region`][maphis.common.regions_cache.Region]]

dictionary of Region objects, keyed by corresponding region labels

data_storage Dict[str, Any]

this is for you to cache arbitrary data pertaining to the specific photo, that you expect you will need at some later point.

Source code in maphis\common\regions_cache.py
class RegionsCache:
    """Stores [`Region`][maphis.common.regions_cache.Region] objects, one for each integer label present in `region_labels`.
    Intended for efficient access to regions so you don't have to repeatedly search for them.

    Attributes:
        regions (typing.Dict[int, [`Region`][maphis.common.regions_cache.Region]]): dictionary of [`Region`][maphis.common.regions_cache.Region] objects, keyed by corresponding region labels
        data_storage (typing.Dict[str, typing.Any]): this is for you to cache arbitrary data pertaining to the specific photo, that you expect you will need
            at some later point.

    """
    def __init__(self, region_labels: typing.Set[int], photo: Photo, label_name: str):
        self.regions: typing.Dict[int, Region] = {}
        label_img = photo[label_name]
        regions_by_level = label_img.label_hierarchy.group_by_level(region_labels)

        for level, labels in regions_by_level.items():
            label_img_on_level = label_img[level]
            for label in labels:
                region_mask = label_img_on_level == label
                yy, xx = np.nonzero(region_mask)
                if len(yy) == 0:
                    continue
                top, left, bottom, right = np.min(yy), np.min(xx), np.max(yy), np.max(xx)

                mask_roi = region_mask[top:bottom+1, left:right+1]
                image_roi = photo.image[top:bottom+1, left:right+1]

                region = Region(label, mask_roi, image_roi, (top, left, bottom-top+1, right-left+1))
                self.regions[label] = region
        self.data_storage: typing.Dict[str, typing.Any] = {}

RegionProperty

A class representing a property for a specific region.

Source code in maphis\measurement\region_property.py
class RegionProperty:
    """A class representing a property for a specific region.
    """

    def __init__(self):
        self._info: Optional[Info] = None
        self.label: int = -1
        # self.prop_type: PropertyType = PropertyType.Scalar
        # self.value: Optional[Union[Value, Tuple[List[Any], Union[CompoundUnit, Unit]]]] = None  # either Value, or a tuple of list of values(float, int...) and a Unit
        # self.num_vals: int = 1
        # self._val_names: typing.List[str] = []  # names of individual scalar values or matrices
        # self.col_names: typing.List[str] = []  # in the case when self.prop_type = PropertyType.NDArray
        # self.row_names: typing.List[str] = []  # same here
        self.value: typing.Optional[Value] = None
        self.vector_viz: Optional[QPixmap] = None

        self._str_rep: str = ''
        self._update_str_rep()

        self.prop_comp_key: str = ''
        self.local_key: str = ''
        self.settings: typing.Dict[str, typing.Dict[str, str]] = {}
        self._up_to_date: bool = True
        self.image_path: Path = Path()

    @property
    def info(self) -> Optional[Info]:
        return self._info

    @info.setter
    def info(self, info: Optional[Info]):
        self._info = info
        self._update_str_rep()

    @property
    def prop_key(self) -> str:
        return f'{self.prop_comp_key}.{self.local_key}'

    def __str__(self) -> str:
        # return f'{self.info.name}: {self.format_value()} {self.unit}'
        # if len(self._str_rep) == 0:
        #     self._update_str_rep()
        # return self._str_rep
        return f'{"" if self._info is None else self._info.name}: {self.value.value}'

    def _update_str_rep(self):
        if self.value is None:
            return
        # if isinstance(self.value, Value):
        self._str_rep = f'{"" if self._info is None else self._info.name}: {self.value.value}'
        # return
        # val_str = self.format_value()
        # self._str_rep = f'{"" if self._info is None else self.info.name}: {val_str} {self.value.value}'

    @property
    def prop_type(self) -> PropertyType:
        return self.value.value_type

    # def format_value(self) -> str:
    #     if self.prop_type == PropertyType.Scalar:  # So self.value is of type `Value`
    #         # if type(self.value) == float:
    #         #     return f'{self.value:.2f}'
    #         return str(self.value)
    #     elif self.prop_type == PropertyType.Intensity: # self.value is of type Tuple[List[Any], CompoundUnit]
    #         if self.num_vals == 1:
    #             if type(self.value[0][0]) == float:
    #                 return f'{self.value[0][0]:.2f} {self.val_names[0]}'
    #             return f'{self.value[0][0]} {self.val_names[0]}'
    #         else:
    #             val_string = '('
    #             for i in range(self.num_vals):
    #                 val_string += f'{self.value[0][i]:.2f} {self.val_names[i]}, '
    #             return val_string[:-2] + ')'
    #     elif self.prop_type == PropertyType.Vector:
    #         val_string = ''
    #         for i in range(self.num_vals):
    #             val_string += f'{self.value[0][i]:.2f}, '
    #         return val_string[:-2]
    #     else:
    #         val_string = ''
    #         #print(f"self.prop_type: {self.prop_type}")
    #         #print(f"self.num_vals: {self.num_vals}")
    #         #print(f"self.value: {self.value}")
    #         for i in range(self.num_vals):
    #             val_string += f'{self.value[0][i]}, '
    #         #print(f"val_string[:-2]: {val_string[:-2]}")
    #         return val_string[:-2]
    #     raise ValueError(f'Unsupported type of value: {type(self.value)}')

    # @property
    # def val_names(self) -> typing.List[str]:
    #     if len(self._val_names) == 0:
    #         return [f'value_{i}' for i in range(self.num_vals)]
    #     return self._val_names

    # @val_names.setter
    # def val_names(self, val_names: typing.Sequence[str]):
    #     self._val_names = list(val_names)

    @classmethod
    def from_dict(cls, dict_obj: typing.Dict[str, typing.Any]) -> 'RegionProperty':
        reg_prop = RegionProperty()
        reg_prop.label = dict_obj['label']

        # check whether `value_dict` is an actual `dict` - meaning it is an actual `Value` implementation
        # or if it is a string, indicating that it is the previous version of Value storage
        if dict_obj.get('property_version', 1.0) < 2.0:
            raise 
            # __type = PropertyType(dict_obj['prop_type'])
            # value_dict = {
            #
            # }
            #
            # if __type == PropertyType.Scalar:
            #     value = ScalarValue(0 * ureg('pixel'))
            #     value_dict = value.to_dict()
            #     value_dict['value'] =
            # elif __type == PropertyType.NDArray:
            #     path_unit = eval(dict_obj['value'])
            #     ndarr = np.load(path_unit[0])
            #     unit: Unit = path_unit[1]
            #     reg_prop.value = (np.load(path_unit[0]), path_unit[1])
        else:
            value_dict = dict_obj['value']

        image_path: Path = dict_obj['image_path']

        if (value_type := ValueType(value_dict['type'])) == ValueType.Scalar:
            reg_prop.value = ScalarValue.from_dict(value_dict)
        elif value_type == ValueType.Vector:
            reg_prop.value = VectorValue.from_dict(value_dict)
        elif value_type == ValueType.Matrix:
            value_dict['path_to_value'] = image_path.parent / value_dict['path_to_value']
            reg_prop.value = MatrixValue.from_dict(value_dict)
        reg_prop.prop_comp_key = dict_obj['prop_comp_key']
        reg_prop.local_key = dict_obj['local_key']
        reg_prop.settings = dict_obj['settings']
        reg_prop._up_to_date = dict_obj.get('up_to_date', True)
        info = Info()
        info.name = dict_obj['name']
        info.key = dict_obj['key']
        info.description = dict_obj['description']
        reg_prop.info = info
        return reg_prop

    def to_dict(self, **kwargs) -> typing.Dict[str, typing.Any]:
        return {
            'name': self.info.name,
            'label': self.label,
            'value': self.value.to_dict(**kwargs),
            'key': self.info.key,
            'description': self.info.description,
            'prop_comp_key': self.prop_comp_key,
            'local_key': self.local_key,
            'settings': self.settings,
            'up_to_date': self.up_to_date,
            'property_version': 2.0
        }

    def __hash__(self) -> int:
        # return hash((self.info.key, self.label))
        return hash((self.prop_key, self.label))

    def __eq__(self, other) -> bool:
        if not isinstance(other, RegionProperty):
            return False
        return hash(self) == hash(other)

    @property
    def up_to_date(self) -> bool:
        return self._up_to_date

    @up_to_date.setter
    def up_to_date(self, is_up_to_date: bool):
        self._up_to_date = is_up_to_date

MatrixValue

Bases: Value

Source code in maphis\measurement\values.py
class MatrixValue(Value):
    def __init__(self, value: pint.Quantity):
        super().__init__(value)
        self._row_names: typing.List[str] = []
        self._column_names: typing.List[str] = []
        self._channel_names: typing.List[str] = []

    @property
    def value_type(self) -> ValueType:
        return ValueType.Matrix

    @property
    def count(self) -> int:
        return len(self._value.magnitude)

    @property
    def dims(self) -> int:
        return self._value.magnitude.dim

    @property
    def row_count(self) -> int:
        return self._value.magnitude.shape[1]

    @property
    def column_count(self) -> int:
        return self._value.magnitude.shape[2]

    @property
    def channel_count(self) -> int:
        return self._value.magnitude.shape[0]

    @property
    def row_names(self) -> typing.List[str]:
        if len(self._row_names) < self.row_count:
            self._row_names = [f'row_{i}' for i in range(self.row_count)]
        return self._row_names

    @row_names.setter
    def row_names(self, rnames: typing.Iterable[str]):
        self._row_names = list(rnames)

    @property
    def column_names(self) -> typing.List[str]:
        if len(self._column_names) < self.column_count:
            self._column_names = [f'col_{i}' for i in range(self.column_count)]
        return self._column_names

    @column_names.setter
    def column_names(self, cnames: typing.Iterable[str]):
        self._column_names = list(cnames)

    @property
    def channel_names(self) -> typing.List[str]:
        if len(self._channel_names) < self.channel_count:
            self._channel_names = [f'ch_{i}' for i in range(self.channel_count)]
        return self._channel_names

    @channel_names.setter
    def channel_names(self, chnames: typing.Iterable[str]):
        self._channel_names = list(chnames)

    @property
    def raw_value(self) -> np.ndarray:
        return self._value.magnitude

    def to_dict(self, **kwargs) -> typing.Dict[str, str]:
        ndarr = self._value.magnitude
        folder: typing.Optional[Path] = kwargs.get('label_folder', None)
        npy_fname: typing.Optional[str] = kwargs.get('npy_fname', None)
        if folder is not None:
            if npy_fname is not None:
                np.save(str(folder / npy_fname), ndarr)
            else:
                raise ValueError(f'Provide a name for the file to be save in the folder {folder}')
            value = None
        else:
            value = np.array_str(self.raw_value).replace('[', '').replace(']', '').replace('\n', '')
        return {
            'type': self.value_type,
            'shape': self.raw_value.shape,
            'value': value,
            'path_to_value': npy_fname,
            'units': str(self._value.units),
            'row_names': self.row_names,
            'column_names': self.column_names,
            'channel_names': self.channel_names,
            'dtype': str(self.raw_value.dtype)
        }

    @classmethod
    def from_dict(cls, _dict: typing.Dict[str, str]) -> 'MatrixValue':
        """
        Constructs a `MatrixValue` from a dictionary. Raises KeyError if the dictionary does not
        have all the needed fields.
        """
        matrix_path = Path(_dict['path_to_value']) if _dict.get('path_to_value', None) is not None else None
        if matrix_path is not None:
            ndarr = np.load(str(matrix_path))
        else:
            ndarr = np.fromstring(_dict['value'], dtype=np.dtype(_dict['dtype']), sep=' ').reshape(_dict['shape'])
        units = ureg(_dict['units'])
        matrix_val = MatrixValue(ndarr * units)
        matrix_val.row_names = _dict['row_names']
        matrix_val.column_names = _dict['column_names']
        matrix_val.channel_names = _dict['channel_names']
        return matrix_val

from_dict(_dict) classmethod

Constructs a MatrixValue from a dictionary. Raises KeyError if the dictionary does not have all the needed fields.

Source code in maphis\measurement\values.py
@classmethod
def from_dict(cls, _dict: typing.Dict[str, str]) -> 'MatrixValue':
    """
    Constructs a `MatrixValue` from a dictionary. Raises KeyError if the dictionary does not
    have all the needed fields.
    """
    matrix_path = Path(_dict['path_to_value']) if _dict.get('path_to_value', None) is not None else None
    if matrix_path is not None:
        ndarr = np.load(str(matrix_path))
    else:
        ndarr = np.fromstring(_dict['value'], dtype=np.dtype(_dict['dtype']), sep=' ').reshape(_dict['shape'])
    units = ureg(_dict['units'])
    matrix_val = MatrixValue(ndarr * units)
    matrix_val.row_names = _dict['row_names']
    matrix_val.column_names = _dict['column_names']
    matrix_val.channel_names = _dict['channel_names']
    return matrix_val

ScalarValue

Bases: Value

Source code in maphis\measurement\values.py
class ScalarValue(Value):
    def __init__(self, value: pint.Quantity):
        super().__init__(value)

    @property
    def count(self) -> int:
        return 1

    @property
    def value_type(self) -> ValueType:
        return ValueType.Scalar

    def to_dict(self, **kwargs) -> typing.Dict[str, str]:
        return {
            'type': self.value_type,
            'value': str(self._value.magnitude),
            'units': str(self._value.units),
            'dtype': str(self.raw_value.dtype) if type(self.raw_value) == np.ndarray else type(self.raw_value).__name__
        }

    @classmethod
    def from_dict(cls, _dict: typing.Dict[str, str]) -> typing.Optional['ScalarValue']:
        """
        Constructs a `ScalarValue` from a dictionary. Raises KeyError if the dictionary does not
        have all the needed fields.
        """
        return ScalarValue(ureg.Quantity(_dict['value'] + ' ' + _dict['units']))

from_dict(_dict) classmethod

Constructs a ScalarValue from a dictionary. Raises KeyError if the dictionary does not have all the needed fields.

Source code in maphis\measurement\values.py
@classmethod
def from_dict(cls, _dict: typing.Dict[str, str]) -> typing.Optional['ScalarValue']:
    """
    Constructs a `ScalarValue` from a dictionary. Raises KeyError if the dictionary does not
    have all the needed fields.
    """
    return ScalarValue(ureg.Quantity(_dict['value'] + ' ' + _dict['units']))

VectorValue

Bases: Value

Source code in maphis\measurement\values.py
class VectorValue(Value):
    def __init__(self, value: pint.Quantity):
        super().__init__(value)
        self._column_names: typing.List[str] = []

    @property
    def count(self) -> int:
        return self._value.magnitude.shape[0]

    @property
    def value_type(self) -> ValueType:
        return ValueType.Vector

    @property
    def column_names(self) -> typing.List[str]:
        if len(self._column_names) < self.count:
            self._column_names = [f'col_{i}' for i in range(self.count)]
        return self._column_names

    @column_names.setter
    def column_names(self, cnames: typing.Iterable[str]):
        self._column_names = list(cnames)

    def to_dict(self, **kwargs) -> typing.Dict[str, str]:
        return {
            'type': self.value_type,
            'shape': self.raw_value.shape,
            'value': np.array_str(self._value.magnitude).replace('[', '').replace(']', '').replace('\n', ''),
            'units': str(self._value.units),
            'column_names': self.column_names,
            'dtype': str(self.raw_value.dtype)
        }

    @classmethod
    def from_dict(cls, _dict: typing.Dict[str, str]) -> 'VectorValue':
        """
        Constructs a `VectorValue` from a dictionary. Raises KeyError if the dictionary does not
        have all the needed fields.
        """
        raw_vals_str = _dict['value']
        unit_str = _dict['units']
        raw_value = np.fromstring(raw_vals_str, dtype=np.dtype(_dict['dtype']), sep=' ')
        unit = ureg(unit_str)
        value = raw_value * unit
        vec_value = VectorValue(value)
        vec_value.column_names = _dict['column_names']

        return vec_value

from_dict(_dict) classmethod

Constructs a VectorValue from a dictionary. Raises KeyError if the dictionary does not have all the needed fields.

Source code in maphis\measurement\values.py
@classmethod
def from_dict(cls, _dict: typing.Dict[str, str]) -> 'VectorValue':
    """
    Constructs a `VectorValue` from a dictionary. Raises KeyError if the dictionary does not
    have all the needed fields.
    """
    raw_vals_str = _dict['value']
    unit_str = _dict['units']
    raw_value = np.fromstring(raw_vals_str, dtype=np.dtype(_dict['dtype']), sep=' ')
    unit = ureg(unit_str)
    value = raw_value * unit
    vec_value = VectorValue(value)
    vec_value.column_names = _dict['column_names']

    return vec_value