Skip to content
Merged
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
42 changes: 29 additions & 13 deletions qubalab/objects/classification.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
from __future__ import annotations
import random
from typing import Optional
from typing import Optional, Union


class Classification(object):
"""
Simple class to store the name and color of a classification.
Simple class to store the names and color of a classification.
"""

_cached_classifications = {}

def __init__(self, name: str, color: Optional[tuple[int, int, int]] = None):
def __init__(
self,
names: Union[str, tuple[str]],
color: Optional[tuple[int, int, int]] = None,
):
"""
:param name: the name of the classification
:param names: the names of the classification
:param color: the RGB color (each component between 0 and 255) of the classification. Can be None to use a random color
"""
self._name = name
if isinstance(names, str):
names = (names,)
self._names = names
self._color = (
tuple(random.randint(0, 255) for _ in range(3)) if color is None else color
)
Expand All @@ -25,7 +31,14 @@ def name(self) -> str:
"""
The name of the classification.
"""
return self._name
return ": ".join(self._names)

@property
def names(self) -> tuple[str]:
"""
The name of the classification.
"""
return self._names

@property
def color(self) -> tuple[int, int, int]:
Expand All @@ -36,8 +49,9 @@ def color(self) -> tuple[int, int, int]:

@staticmethod
def get_cached_classification(
name: str, color: Optional[tuple[int, int, int]] = None
) -> Classification:
name: Optional[Union[str, tuple[str]]],
color: Optional[tuple[int, int, int]] = None,
) -> Optional[Classification]:
"""
Return a classification by looking at an internal cache.

Expand All @@ -57,23 +71,25 @@ def get_cached_classification(
"""
if name is None:
return None

if isinstance(name, str):
name = (name,)
name = ": ".join(name)
classification = Classification._cached_classifications.get(name)
if classification is None:
classification = Classification(name, color)
Classification._cached_classifications[classification.name] = classification
return classification

def __str__(self):
return f"Classification {self._name} of color {self._color}"
return f"Classification {self.name} of color {self.color}"

def __repr__(self):
return f"Classification('{self._name}', {self._color})"
return f"Classification('{self.name}', {self.color})"

def __eq__(self, other):
if isinstance(other, Classification):
return self._name == other._name and self._color == other._color
return self.name == other.name and self.color == other.color
return False

def __hash__(self):
return hash(self._name)
return hash(self.name)
12 changes: 8 additions & 4 deletions qubalab/objects/image_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ def __init__(
if classification is not None:
if isinstance(classification, Classification):
props["classification"] = {
"name": classification.name,
"names": classification.names,
"color": classification.color,
}
else:
props["classification"] = {
"name": classification.get("name"),
"names": classification.get("names")
if "names" in classification.keys()
else (classification.get("name"),),
"color": classification.get("color"),
}
if name is not None:
Expand Down Expand Up @@ -169,7 +171,7 @@ def create_from_label_image(
connectivity: int = 4,
scale: float = 1.0,
include_labels=False,
classification_names: Union[str, dict[int, str]] = None,
classification_names: Optional[Union[str, dict[int, str]]] = None,
) -> list[ImageFeature]:
"""
Create a list of ImageFeatures from a binary or labeled image.
Expand Down Expand Up @@ -243,7 +245,9 @@ def classification(self) -> Classification:
"""
if "classification" in self.properties:
return Classification(
self.properties["classification"].get("name"),
self.properties["classification"].get("names")
if "names" in self.properties["classification"].keys()
else (self.properties["classification"].get("name"),),
self.properties["classification"].get("color"),
)
else:
Expand Down
35 changes: 32 additions & 3 deletions tests/objects/test_image_feature.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import geojson
import math
import numpy as np
from qubalab.objects.image_feature import ImageFeature
from qubalab.objects.classification import Classification
from qubalab.objects.object_type import ObjectType
from qubalab.objects import (
ObjectType,
ImageFeature,
Classification,
geojson_features_from_string,
)


def test_geometry():
Expand Down Expand Up @@ -685,3 +688,29 @@ def test_nucleus_geometry_when_set_after_creation():
nucleus_geometry = image_feature.nucleus_geometry

assert nucleus_geometry == expected_nucleus_geometry


def test_imagefeature_handles_classification_names():
string = """
{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"objectType":"annotation","classification":{"names":["a","b"]}}}
"""
feature = geojson_features_from_string(string)
ifeature = ImageFeature.create_from_feature(feature)
assert (
ifeature.classification.names
== feature["properties"]["classification"]["names"]
)
assert ifeature.classification.name == ": ".join(
feature["properties"]["classification"]["names"]
)


def test_imagefeature_handles_classification_name():
string = """
{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"objectType":"annotation","classification":{"names":["a","b"]}}}
"""
feature = geojson_features_from_string(string)
ifeature = ImageFeature.create_from_feature(feature)
assert ifeature.classification.name == ": ".join(
feature["properties"]["classification"]["names"]
)