Basic Usage

Importing Terms as classes

The pathology-image-features library is structured according to the ontological terms which are defined in the Pathology Image Features Ontology. Therefore, to import a class, you can follow the subclass hierarchy to determine the path:

For example, the class “image data set” (OBI:0003327) has the following parent term structure:

  • information content entity (IAO:0000030)
    • data entity (IAO:0000027)
      • homogenous data collection (IAO:0000100)
        • image data set (OBI:0003327)

Therefore, it can be imported from pathology-image-features using the following:

from pathology_image_features.ontology.classes.information_content_entity.data_entity.homogenous_data_collection.image_data_set import image_data_set

image_data_set_instance = image_data_set()

However, as that method results in very long paths, “image data set” (as well as all other terms) can be imported directly from the ontology graph using the following:

from pathology_image_features.ontology import PIFO

image_data_set = PIFO.to_python('http://purl.obolibrary.org/obo/OBI_0003327')
image_data_set_instance = image_data_set()

Adding Instance attributes

Properties can be added to certain classes using the following equivalent methods:

from pathology_image_features.ontology import PIFO

image_data_set = PIFO.to_python('http://purl.obolibrary.org/obo/OBI_0003327')

# Method 1: As keyword arguments on initialization
image_data_set_instance = image_data_set(has_filepath = '/path/to/slide.svs')
# Method 2: As attributes after initialization
image_data_set_instance = image_data_set()
image_data_set_instance.has_filepath = '/path/to/slide.svs'
# Method 3: Using a construct method (implemented for some classes)
image_data_set_instance = image_data_set.construct(filepath = '/path/to/slide.svs')

Construct methods enable the “construction” of properties linking instances typically described in a Python data type serialization. For example, to fully define a “FeatureCollection” (GEO:FeatureCollection) you could use the following:

from pathology_image_features.ontology import PIFO

FeatureCollection = PIFO.to_python('http://www.opengis.net/ont/geosparql#FeatureCollection')
Feature = PIFO.to_python('http://www.opengis.net/ont/geosparql#Feature')
Geometry = PIFO.to_python('http://www.opengis.net/ont/geosparql#Geometry')

feature_collection_instance = FeatureCollection()
for feature in feature_collection_dict['features']:
    geometry_instance = Geometry(asGeoJSON = json.dumps(feature['geometry']))
    feature_instance = Feature(hasGeometry = geometry_instance, member = feature_collection_instance)

# OR:
feature_collection_instance = FeatureCollection.construct(feature_collection_dict)

Dynamic Subclass creation

Any time you are working with an object which could more accurately be described as a “type of” some base class rather than an “instance of” some base class, it might be more appropriate to use a subclass of that class.

Creating a subclass of a term in pathology-image-features is as simple as subclassing the base class and adding any additional properties or methods you need.

For example, this is how you would create a new “type of” image data set:

class MyImageDataSet(image_data_set):
    label = "My Image Data Set"
    definition = "A custom type of image data set"

    def custom_method(self, *args,**kwargs):
        # This is a new method not present in the base class
        pass

    @classmethod
    def construct(cls, *args, **kwargs):
        # This is a new method which overrides an existing method in the base class
        return super().construct(*args, **kwargs)


# OR using types.new_class:
import types
my_image_data_set = types.new_class(name = "MyImageDataSet", bases = (image_data_set,))

Data Transformations

One of the primary uses of pathology-image-features is the creation of interoperable computational pathology pipelines. These are built off subclasses of the “data transformation” (OBI:0200000) class and the “image data set analysis” (OBI:0003355) class.

While some of the methods which are included in the “data transformation” class are required in order to enable downstream functionality, the following methods may be updated in order to implement custom behavior:

For example, the following code would be an implementation of a data transformation which accesses the “has_specified_numeric_value” of an input, adds 2, and returns a new “value specification” instance.

class Add2toNumber(data_transformation):
    label = "Add 2 to Number"
    definition = "This process adds 2 to the specified_numeric_value of an input data entity."

    def _pull(self, input_data_item):
        """The _pull method is used to access data of an item stored in some property.
        """
        return input_data_item.has_specified_numeric_value

    def _run(self, input_data_item):
        """"The _run method is where the primary functionality of the data transformation is implemented.
        """
        input_value = self._pull(input_data_item)
        output = input_value + 2
        return output

    def _cast_output(self, input, output, output_dir):
        """The _cast_output method is used to create a new data item from the output of the _run method.
        """

        # Adding properties to the new value specification instance
        output_value_spec = value_specification(
            has_specified_numeric_value = output, # This is the output of the _run method
            is_specified_output_of = self, # This specifies that this new value specification is the output of this process.
            is_about = input, # This specifies that this new value specification is about the input data item.
        )

        # Serializing the new value specification to the output directory if required.
        namespace, name = PIFO.split_uri(output_value_spec.iri)
        output_value_spec.serialize(filepath = os.path.join(output_dir, f'{name}.ttl'))

        return output_value_spec

As a further example, the following math equation can be implemented as a data transformation:

  • Output = ((Input + 2) ** 2) / 3

# Creating a parent class of numeric operations, subclasses only need to update the _run method
class NumericOperation(data_transformation):
    label = 'Numeric Operation'
    definition = "Parent class of mathematical operations performed on numeric values."

    def _pull(self, input_data_item):
        return self.has_specified_numeric_value

    def _cast_output(self, input, output, output_dir):
        output_value_spec = value_specification(
            has_specified_numeric_value = output,
            is_specified_output_of = self,
            is_about = input,
        )

        # Serializing the new value specification to the output directory if required.
        namespace, name = PIFO.split_uri(output_value_spec.iri)
        output_value_spec.serialize(filepath = os.path.join(output_dir, f'{name}.ttl'))

        return output_value_spec

class Add2toNumber(NumericOperation):
    label = 'Add 2 to Number'
    definition = 'This process adds 2 to the specified_numeric_value of an input data entity.'

    def _run(self, input):
        input = self._pull(input)
        return input + 2

class SquareOfNumber(NumericOperation):
    label = 'Square of Number'
    definition = 'This process squares the specified_numeric_value of an input data entity.'

    def _run(self, input):
        input = self._pull(input)
        return input ** 2

class DivideBy3(NumericOperation):
    label = 'Divide by 3'
    definition = 'This process divides the specified_numeric_value of an input data entity by 3.'

    def _run(self, input):
        input = self._pull(input)
        return input / 3

# By default, transformations part order is assumed to be the order in which they are provided (no PEMDAS here!)
combined_transformation = data_transformation(
    has_part = [
        Add2toNumber(),
        SquareOfNumber(),
        DivideBy3(),
    ]
)

# Alternatively, you can specify the order of operations using the 'preceded by' property.
add_2_to_number = Add2toNumber()
square_of_number = SquareOfNumber(preceded_by = add_2_to_number)
divide_by_3 = DivideBy3(preceded_by = square_of_number)

# Shuffling has no effect on the output.
from numpy import random
combined_transformation = data_transformation(
    has_part = random.shuffle([
        add_2_to_number,
        square_of_number,
        divide_by_3,
    ])
)