Tutorial 4: Building a Pipeline#

Learning goals:

  1. Create an ImagePipeline that chains multiple operations

  2. Apply the pipeline to an image

  3. Serialize (save) the pipeline to JSON

  4. Reload the pipeline from JSON and re-apply it


In Tutorial 3 you applied enhancers and a detector one by one. That works, but it quickly becomes tedious when your workflow has many steps. An ImagePipeline lets you chain them into a single reusable workflow – one object that remembers every operation, every parameter, and applies them in the correct order every time.

Imports#

[1]:
import phenotypic as pht
from phenotypic.data import load_yeast_plate
from phenotypic.enhance import GaussianBlur, CLAHE
from phenotypic.detect import OtsuDetector

Load the plate#

[2]:
plate = load_yeast_plate()

Create a pipeline#

An ImagePipeline accepts a list of operations via the ops parameter. Operations are applied in order – enhancers first, then the detector. The pipeline packages them into a single object you can apply, save, and share.

[3]:
pipeline = pht.ImagePipeline(
    ops=[
        GaussianBlur(sigma=2.0),
        CLAHE(clip_limit=0.01),
        OtsuDetector(),
    ],
    name="my_first_pipeline",
)

Inspect the pipeline#

Printing a pipeline shows its full configuration as structured JSON – including every operation and its parameters. This is handy for double-checking your setup before you run anything.

[4]:
print(pipeline)
{
  "version": "0.14.0b5",
  "name": "my_first_pipeline",
  "desc": null,
  "reset": false,
  "pipe_cfgs": {
    "GaussianBlur": {
      "class": "GaussianBlur",
      "params": {
        "sigma": 2.0,
        "mode": "reflect",
        "cval": 0.0,
        "truncate": 4.0
      }
    },
    "CLAHE": {
      "class": "CLAHE",
      "params": {
        "kernel_size": null,
        "clip_limit": 0.01
      }
    },
    "OtsuDetector": {
      "class": "OtsuDetector",
      "params": {
        "ignore_zeros": false,
        "ignore_borders": true
      }
    }
  },
  "meas": {},
  "post": {}
}

Apply the pipeline#

Call .apply() to run each operation in sequence. The method returns the processed image. By default, inplace=False, so the original plate is left unchanged – a fresh copy is made internally.

[5]:
result = pipeline.apply(plate)
result.dash(overlay=True)

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

Well done – you have detected colonies on the plate in a single call!

Notice that the original plate still has no detection results. That is the safety of inplace=False (the default). If you want to modify the original image directly, pass inplace=True:

pipeline.apply(plate, inplace=True)  # modifies plate directly

The default inplace=False is safer – it keeps your raw data intact so you can experiment freely.

Serialize the pipeline to JSON#

A key benefit of pipelines is reproducibility. You can save the complete configuration – operations, parameters, everything – to a JSON file. Share it with collaborators or reload it months later to get the exact same results.

[6]:
pipeline.to_json("my_pipeline.json")
[6]:
'{\n  "version": "0.14.0b5",\n  "name": "my_first_pipeline",\n  "desc": null,\n  "reset": false,\n  "pipe_cfgs": {\n    "GaussianBlur": {\n      "class": "GaussianBlur",\n      "params": {\n        "sigma": 2.0,\n        "mode": "reflect",\n        "cval": 0.0,\n        "truncate": 4.0\n      }\n    },\n    "CLAHE": {\n      "class": "CLAHE",\n      "params": {\n        "kernel_size": null,\n        "clip_limit": 0.01\n      }\n    },\n    "OtsuDetector": {\n      "class": "OtsuDetector",\n      "params": {\n        "ignore_zeros": false,\n        "ignore_borders": true\n      }\n    }\n  },\n  "meas": {},\n  "post": {}\n}'

You can also get the JSON as a string (without saving to a file) by calling to_json() with no arguments. Let’s peek at what it looks like:

[7]:
print(pipeline.to_json())
{
  "version": "0.14.0b5",
  "name": "my_first_pipeline",
  "desc": null,
  "reset": false,
  "pipe_cfgs": {
    "GaussianBlur": {
      "class": "GaussianBlur",
      "params": {
        "sigma": 2.0,
        "mode": "reflect",
        "cval": 0.0,
        "truncate": 4.0
      }
    },
    "CLAHE": {
      "class": "CLAHE",
      "params": {
        "kernel_size": null,
        "clip_limit": 0.01
      }
    },
    "OtsuDetector": {
      "class": "OtsuDetector",
      "params": {
        "ignore_zeros": false,
        "ignore_borders": true
      }
    }
  },
  "meas": {},
  "post": {}
}

Every operation class and its parameters are captured. The PhenoTypic version is recorded too, so you will get a warning if you later load the pipeline with a different version.

Reload the pipeline from JSON#

Use the from_json() class method to reconstruct a pipeline from a saved file. The loaded pipeline is identical to the original.

[8]:
loaded = pht.ImagePipeline.from_json("my_pipeline.json")
print(loaded)
{
  "version": "0.14.0b5",
  "name": "my_first_pipeline",
  "desc": null,
  "reset": false,
  "pipe_cfgs": {
    "GaussianBlur": {
      "class": "GaussianBlur",
      "params": {
        "sigma": 2.0,
        "mode": "reflect",
        "cval": 0.0,
        "truncate": 4.0
      }
    },
    "CLAHE": {
      "class": "CLAHE",
      "params": {
        "kernel_size": null,
        "clip_limit": 0.01
      }
    },
    "OtsuDetector": {
      "class": "OtsuDetector",
      "params": {
        "ignore_zeros": false,
        "ignore_borders": true
      }
    }
  },
  "meas": {},
  "post": {}
}

Apply the reloaded pipeline#

Let’s confirm that the reloaded pipeline produces the same result. We load a fresh plate and apply the pipeline we just deserialized.

[9]:
result2 = loaded.apply(load_yeast_plate())
result2.dash(overlay=True)

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

Same result – full reproducibility. Whether you ran the pipeline today or reload it next year, the output is identical.

Clean up#

[10]:
import os
os.remove("my_pipeline.json")

Summary#

You have built your first ImagePipeline, applied it to a plate, saved it to JSON, and reloaded it. Here is what you learned:

  • ``ImagePipeline(ops=[…])`` chains operations into a single reusable object.

  • ``.apply(image)`` runs every operation in sequence and returns the processed image.

  • ``inplace=False`` (default) keeps your original data safe; inplace=True modifies it directly.

  • ``.to_json(filepath)`` saves the full pipeline configuration to a file.

  • ``ImagePipeline.from_json(filepath)`` reconstructs an identical pipeline from that file.

Pipelines are the core workflow tool in PhenoTypic – they make your analysis reproducible and shareable.


Next up: Tutorial 5: Working with Grid Plates – learn how grid plates let you analyze individual wells in an arrayed format.