Tutorial 4: Building a Pipeline#
Learning goals:
Create an
ImagePipelinethat chains multiple operationsApply the pipeline to an image
Serialize (save) the pipeline to JSON
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=Truemodifies 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.