This walkthrough follows the original `examples/3_channel_example_walkthrough.ipynb` notebook. Download the [notebook](../_downloads/three-channel.ipynb) or [Python script](../_downloads/three-channel.py). The figures show saved results from this example dataset. Run the notebook to reproduce the analysis. # Three-channel walkthrough: DAPI and GFP-DnaN objects Each field contains phase contrast (`C1`), DAPI (`C2`, DNA/nucleoid), and GFP-DnaN (`C3`, replication-associated puncta). We segment cells, inspect masks and meshes, curate with the supplied SVM, detect C3 objects without channel alignment, and extract per-cell features. Run the cells from top to bottom. All images, masks, models, and outputs stay in the **three-channel example folder**. Set `RUN_DIR` in the first code cell if you moved that folder. The supplied masks let you set `RUN_SEGMENTATION = False`; masks from other tools also work when the background is 0 and distinct cells are labeled consecutively 1 through N. Omnipose uses a GPU when available and otherwise runs on CPU. See the [installation guide](../getting-started/installation.rst) for installation. ```python from pathlib import Path import sys import torch import pandas as pd import matplotlib.pyplot as plt from IPython.display import display # Import BactoScoop # Import the package installed in this notebook kernel. import bactoscoop import bactoscoop.plot as bplt # Point to the folder containing C1-C3 TIFFs, masks/, and the SVM model. # Locate the downloaded examples; keep the supplied inputs unchanged. import os import shutil from datetime import datetime EXAMPLES_DIR = os.environ.get("BACTOSCOOP_EXAMPLES_DIR") candidates = ([Path(EXAMPLES_DIR)] if EXAMPLES_DIR else [candidate for base in (Path.cwd(), *Path.cwd().parents) for candidate in (base / "examples", base)]) SOURCE_DIR = next((base / "3_channel_example" for base in candidates if (base / "3_channel_example").is_dir()), None) if SOURCE_DIR is None: raise FileNotFoundError("Open this notebook from the downloaded examples folder, " "or set BACTOSCOOP_EXAMPLES_DIR to that folder.") RUN_DIR = Path.home() / "bactoscoop_runs" / datetime.now().strftime("%Y%m%d_%H%M%S_%f") / "3_channel_example" shutil.copytree(SOURCE_DIR, RUN_DIR) MODEL_PATH = RUN_DIR / "DnaN_Timecourse.pkl" RUN_SEGMENTATION = False # Reuse supplied masks; set True for fresh Omnipose inference. PIXEL_SIZE_UM = 0.065 # Replace with your microscope calibration. PHASE_CHANNEL, DAPI, GFP_DNAN = "C1", "C2", "C3" CHANNEL_LABELS = {"C1": "Phase contrast", "C2": "DAPI", "C3": "GFP-DnaN"} if not RUN_DIR.is_dir(): raise FileNotFoundError(f"Example folder not found: {RUN_DIR}") if not MODEL_PATH.is_file(): raise FileNotFoundError(f"SVM model not found: {MODEL_PATH}") (RUN_DIR / "masks").mkdir(exist_ok=True) FIELD_IDS = bplt.list_field_ids(RUN_DIR, phase_channel=PHASE_CHANNEL) print("Fields:", FIELD_IDS) print("Image and output folder:", RUN_DIR) print("CUDA available:", torch.cuda.is_available(), "(CPU is used when False)") ``` ## 1. Inspect the input channels The field name is the shared part of each filename before `_C1`, `_C2`, or `_C3`. Display contrast is adjusted for the overview; measurements use the original image values. DAPI is cyan and GFP-DnaN is green. ```python fig = bplt.plot_multichannel_overview(RUN_DIR, FIELD_IDS[1], CHANNEL_LABELS, ) display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-01.png :alt: 1. Inspect the input channels **1. Inspect the input channels**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 2. Segment and review `RUN_SEGMENTATION=True` segments the C1 phase images with Omnipose. `mask_thresh` adjusts the mask boundary and `minsize` removes small segments. If you set the flag to `False`, the supplied masks in `masks/` are loaded instead. The review compares phase with mask outlines in the same crop. Change the field, label, center, or crop size in the review cell to inspect another region; a 700-pixel crop shows many cells, while about 150-250 pixels is useful for a close check. ```python collection = bactoscoop.ImageCollection(str(RUN_DIR), px=PIXEL_SIZE_UM) collection.load_phase_images(phase_channel=PHASE_CHANNEL) if RUN_SEGMENTATION: ok = collection.segment_images( mask_thresh=1, minsize=100, n=range(len(collection.images)), model_name="bact_phase_omni", ) if not ok: raise RuntimeError(collection.processing_summary()) collection.create_image_objects(phase_channel=PHASE_CHANNEL) # Change these values and rerun this cell to inspect another field or label. SEGMENTATION_FIELD = FIELD_IDS[0] SEGMENTATION_LABEL = None # Integer mask label, or None for a central cell. SEGMENTATION_CENTER = None # Optional (row, column), used when label is None. SEGMENTATION_CROP_PX = 700 review_image = next(image for image in collection.image_objects if image.image_name.rsplit("_", 1)[0] == SEGMENTATION_FIELD) fig = bplt.plot_segmentation_review( [review_image], crop_size=SEGMENTATION_CROP_PX, mask_label=SEGMENTATION_LABEL, center=SEGMENTATION_CENTER, ) display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-02.png :alt: 2. Segment and review **2. Segment and review**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 3. Build and inspect meshes The mask becomes a cell contour, paired mesh sides, and a midline. `join_thresh` joins nearby poles in pixels; `split_thresh` controls splitting at constrictions; `smoothing` adjusts contour smoothness; `CD_width` selects the width-based constriction measure. `save_data=True` writes `3_channel_example_meshdata.pkl` in `RUN_DIR`. Inspect the mesh on the phase image before loading fluorescence channels. ```python collection.batch_process_mesh( phase_channel=PHASE_CHANNEL, join_thresh=4, split_thresh=0.5, CD_width=False, smoothing=0.1, save_data=True, ) print("Cells with meshes:", sum(len(image.cells) for image in collection.image_objects)) ``` ### Optional: load saved meshes on a later run If you already made meshes, skip the mesh-construction cell and run `collection.batch_load_mesh("3_channel_example_meshdata.pkl", phase_channel=PHASE_CHANNEL)` here. Keep the next inspection step. ```python # Uncomment on a later run after skipping mesh construction: # collection.batch_load_mesh("3_channel_example_meshdata.pkl", phase_channel=PHASE_CHANNEL) ``` ### Choose a cell to inspect Set the field name and cell ID **in the code cell below**, then rerun it. The figure shows phase, contour, mesh, and midline; fluorescence is shown after object detection. ```python MESH_FIELD = "TL4_SGL711_M9glu_12H_XY5" MESH_CELL_ID = 30 mesh_image, _ = bplt.select_example_cell( collection.image_objects, field_id=MESH_FIELD, cell_id=MESH_CELL_ID, ) fig = bplt.plot_cell_gallery(mesh_image, MESH_CELL_ID, channel=None) display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-03.png :alt: Choose a cell to inspect **Choose a cell to inspect**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 4. Curate with the supplied SVM `DnaN_Timecourse.pkl` is the model provided with this example. It uses morphology features to reject unsuitable meshes. Green outlines are retained and red outlines are rejected. `save_curated_data=True` writes `3_channel_example_curated_meshdata.pkl` in `RUN_DIR`. Inspect the result before applying this model to other acquisitions; only load pickle files you trust. ```python #collection.batch_load_mesh("3_channel_example_meshdata.pkl", phase_channel=PHASE_CHANNEL) before = bplt.snapshot_cell_contours(collection.image_objects) before_count = sum(len(image.cells) for image in collection.image_objects) collection.curate_dataset(str(MODEL_PATH), control=False, save_curated_data=True) after_count = sum(len(image.cells) for image in collection.image_objects) print(f"SVM curation: {before_count:,} to {after_count:,} cells") fig = bplt.plot_curation_review(before, collection.image_objects ) display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-04.png :alt: 4. Curate with the supplied SVM **4. Curate with the supplied SVM**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 5. Detect GFP-DnaN objects Object detection loads C3 and finds puncta inside curated cells. `align=False` leaves C3 in its original coordinates. Tune `smoothing`, `log_sigma`, `kernel_width`, and overlap limits for your images. The detected contours are stored on cells and later contribute to object features. ```python detections = collection.batch_detect_objects( channels=[DAPI,GFP_DNAN], reset_channels=True, align=False, smoothing=0.1, log_sigma=3, kernel_width=3, min_overlap_ratio=0.001, max_external_ratio=0.5, ) print("Detected C3 objects:", len(detections)) collection.add_channels(collection.image_objects, [DAPI, GFP_DNAN]) ``` ### Inspect a detected cell Choose a curated field and cell ID directly below. The DAPI panel is cyan, GFP-DnaN is green, and the object outline shows what the detector found. If your chosen cell was removed by curation, pick an ID that remains in the green curation outlines. ```python OBJECT_FIELD = "TL4_SGL711_M9glu_12H_XY5" OBJECT_CELL_ID = 62 object_image, _ = bplt.select_example_cell( collection.image_objects, field_id=OBJECT_FIELD, cell_id=OBJECT_CELL_ID, ) fig = bplt.plot_cell_gallery( object_image,30, channel=DAPI, channel_label="DAPI", ) display(fig) plt.close(fig) fig = bplt.plot_signal_gallery( object_image, 30, channel_labels={DAPI: "DAPI", GFP_DNAN: "GFP-DnaN"}, show_objects=True, ) display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-05.png :alt: Inspect a detected cell **Inspect a detected cell**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ```{figure} ../_static/figures/three-channel-06.png :alt: Inspect a detected cell **Inspect a detected cell**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 6. Extract features `channel_method_pairs` assigns feature methods to channels. `morphological` measures cell geometry from the mesh; `profiling` measures each channel along the cell; `objects` summarizes C3 puncta. `max_mesh_size` excludes unusually large meshes. We compare the **relative** C3 object area fraction (`C3_NC_ratio`) with cell volume, avoiding an absolute fluorescence intensity comparison. ```python channel_method_pairs = [ ([None], "morphological"), ([DAPI], "profiling"), ([GFP_DNAN], "profiling"), ([GFP_DNAN], "objects"), ] collection.batch_calculate_features( channel_method_pairs, all_data=False, max_mesh_size=1000, ) features = collection.merge_dataframes() print(f"{len(features):,} cells x {len(features.columns)} columns") display(features[["image_name", "cell_id", "cell_volume", "C3_NC_ratio"]].head()) ``` ## 7. Inspect relative measurements `C3_NC_ratio` is the detected C3 object area divided by cell area. This comparison uses a within-cell fraction, so it does not require raw brightness to be comparable between fields. ```python fig, ax = plt.subplots(figsize=(5.5, 4)) valid = features[["cell_volume", "C3_NC_ratio"]].dropna() ax.scatter(valid.cell_volume, valid.C3_NC_ratio, s=12, alpha=0.45, color="#31A85E") ax.set(xlabel="Cell volume (um^3)", ylabel="C3 object area / cell area", title="GFP-DnaN object fraction vs cell volume") fig.tight_layout() display(fig) plt.close(fig) ``` ```{figure} ../_static/figures/three-channel-07.png :alt: 7. Inspect relative measurements **7. Inspect relative measurements**. Saved notebook output from `3_channel_example`; display settings are illustrative. ``` ## 8. Save features and audit `dataframe_to_parquet()` writes `3_channel_example_features.parquet`; `dataframe_to_pkl()` writes `3_channel_example_features.pkl`. Both go in `RUN_DIR` and contain one row per retained cell. A `.pkl` argument gives the exact pickle filename; a suffix without `.pkl` becomes a tag. Check `processing_summary()` for image and cell errors. ```python collection.dataframe_to_parquet() pickle_path = collection.dataframe_to_pkl() saved = pd.read_parquet(RUN_DIR / "3_channel_example_features.parquet") summary = collection.processing_summary() print("Saved Parquet and pickle:", len(saved), "rows;", pickle_path) print("Image errors:", summary["error_count"], "Cell errors:", summary["cell_error_count"]) ```