Write Your First Custom Operation#

PhenoTypic is designed to be extended. In this tutorial you will create a custom ImageEnhancer from scratch, implement its _operate() method, and use it inside an ImagePipeline.

What you will learn:

  1. Subclass ImageEnhancer

  2. Implement _operate(self, image) -> image

  3. Add your custom operation to a pipeline

[1]:
import numpy as np
import phenotypic as pht
from phenotypic.abc_ import ImageEnhancer
from phenotypic.data import load_yeast_plate
from phenotypic.detect import OtsuDetector

Step 1: Define the Class#

The simplest operation to start with is an enhancer, since it only modifies detect_mat. You don’t need to worry about objmask, objmap, or grid state.

Let’s create an enhancer that inverts the detection matrix — making dark colonies bright and bright background dark. This is useful when your colonies are darker than the agar.

[2]:
class InvertDetectMat(ImageEnhancer):
    """Invert the detection matrix so dark features become bright."""

    def _operate(self, image):
        # Read the current detection matrix
        mat = image.detect_mat[:]

        # Invert: subtract from the maximum value
        inverted = mat.max() - mat

        # Write back
        image.detect_mat[:] = inverted

        return image

That’s the entire implementation. The _operate() contract is simple:

  1. Receive an Image (or GridImage)

  2. Read from and write to accessors

  3. Return the modified image

For an enhancer, you read detect_mat, transform it, and write it back. Never modify rgb or gray — those belong to the original image data.

Step 2: Test It#

[3]:
plate = load_yeast_plate()
plate.detect_mat.dash()

Data type cannot be displayed: application/vnd.plotly.v1+json

[4]:
inverter = InvertDetectMat()
plate = inverter.apply(plate)
plate.detect_mat.dash()

Data type cannot be displayed: application/vnd.plotly.v1+json

The detection matrix is now inverted — dark regions became bright.

Step 3: Use It in a Pipeline#

Custom operations work seamlessly with ImagePipeline — just include them in the ops list like any built-in operation.

[5]:
pipeline = pht.ImagePipeline(
    ops=[InvertDetectMat(), OtsuDetector()],
)

plate = load_yeast_plate()
result = pipeline.apply(plate)
result.dash(overlay=True)

Data type cannot be displayed: application/vnd.plotly.v1+json

Adding Parameters#

Most operations need configurable parameters. Define them in __init__ and access them via self in _operate().

[6]:
class ScaleDetectMat(ImageEnhancer):
    """Scale the detection matrix by a constant factor."""

    def __init__(self, factor: float = 2.0):
        self.factor = factor

    def _operate(self, image):
        image.detect_mat[:] = np.clip(
            image.detect_mat[:] * self.factor, 0, 1
        )
        return image


# Use it
scaler = ScaleDetectMat(factor=1.5)
print(f"Factor: {scaler.factor}")
Factor: 1.5

Summary#

You have written a custom ImageEnhancer and used it in a pipeline. The key points:

  • Subclass the appropriate ABC (ImageEnhancer, ObjectDetector, ObjectRefiner, MeasureFeatures, etc.)

  • Implement _operate(self, image) -> image

  • Access image data through accessors (image.detect_mat[:], image.objmask[:], etc.)

  • Store parameters as instance attributes in __init__

  • Your operation works automatically with ImagePipeline, JSON serialization, and the CLI

For more on each ABC type, see the how-to guides for custom detectors, custom refiners, and custom measurements.