UC5_IHC_DEBRIEF

UC5 — Per-object CD11b morphometric cohort (control vs 4-oxo-RA): debrief + recipe

Driven live through a local Galaxy (v26.2.dev0) via the Playwright MCP + tools API, per /drive-scenario. Scenario input: UC5_IHC_issue.md (GitHub issue #28). Goal: object-level IHC morphometrics as a collection map-over, embedded on-graph, extracted to a reusable workflow.


TL;DR

The object-level pipeline runs end-to-end as a clean 6-element collection map-over and extracts to a 0-warning, 10-step, byte-identical-on-re-run workflow — the extraction/engineering result is solid.

The science is NOT clean (corrected after adversarial review). The faithful-to-IWC pipeline (eosin channel + per-image Otsu) gave a biologically inverted result; switching to DAB channel + a single fixed optical-density threshold flips it to the “expected” direction (4-oxo-RA: CD11b⁺ area 3.03%→0.12%, objects 242→67) — but this magnitude is confounded, not a validated effect. The treatment batch’s DAB channel is ~10× dynamic-range-compressed (per-object max DAB ≈0.12 vs control ≈1.50), so a fixed 0.1 threshold sits at treatment’s ceiling and mechanically forces it near-empty regardless of true biology. The honest signal is small and normalization-dependent — a stain-normalization re-derivation (below) collapses the apparent effect: from 26× (raw) to 3.7× (hematoxylin-mean loading control) down to ~1.2× (hematoxylin-p99 control, i.e. essentially none). One of three treated replicates (treatment_3) is a clear non-responder (it equals/exceeds controls after normalization). Conclusion: no robust drug effect can be claimed from this data+method — at best a weak, normalization-choice-dependent trend.

Stain-normalization re-derivation (added per user request, after review)

Attempted the genuine fix — real stain normalization before measuring DAB. Findings:

Bottom line: the drug-effect magnitude is not identifiable from this 6-ROI, two-batch dataset with these tools — it swings from “huge” to “none” depending on an unconstrained normalization choice, and 1/3 treated replicates contradicts it. A defensible claim needs matched-acquisition data (same scanner/staining batch) or a calibration target, not post-hoc normalization. The re-derivation was done off-graph (numpy/skimage) because no reliable on-graph stain-normalization / image-math tool is installed; wiring it on-graph would encode a non-result.


What was done

  1. Inputs. Built one list collection IHC ROIs (6 × 600×600 tiff) from the shipped Zenodo URLs via /api/tools/fetch — identifiers control_1..3 (Zenodo 20271100), treatment_1..3 (20157596).
  2. GO/NO-GO probe (1 image). Deconvolve → split → threshold → label → features on control_1, over all 3 HED channels, to (a) confirm multi-object label maps and (b) settle the stain channel.
  3. Cohort map-over (faithful-to-IWC). Ran the IWC chain over all 6: color deconvolution (rgb2hed) → split (axis=C) → extract channel index=1 → Otsu threshold → label (CCA) → per-object features.
  4. Found the confound (see below); switched to the principled DAB pipeline and re-ran the cohort.
  5. On-graph aggregation. collapse_dataset (add sample-id column) → datamash (per-sample count / stained-area / median morphology) → ggplot2_heatmap2 cohort figure; masks → PNG via graphicsmagick_image_convert for embedding.
  6. Page. Built a notebook embedding the input collection, control-vs-treatment segmentation masks, a per-object feature table, the per-sample summary, and the cohort heatmap — every artifact on-graph.
  7. Extraction. Selective extraction by job_ids (the clean DAB chain only, excluding probe/eosin cruft) → 10-step, 0-warning .ga; re-invoked on the same input for the science-identical check.

The central finding: a batch confound, and the principled fix

Symptom. With the faithful IWC pipeline (channel index=1, per-image Otsu), treatment ROIs produced more “objects” than control (1881–2790 vs 105–288) — the opposite of the expected drug effect.

Diagnosis (per-channel pixel stats on the deconvolved images). All three treatment HED channels — including hematoxylin (nuclear counterstain, present regardless of drug) — are ~10× weaker than control (e.g. H-channel max 1.8 → 0.20). Treatment originals never reach black (min RGB ~40 vs 0), i.e. compressed optical-density range → uniformly weak deconvolution → per-image Otsu on the near-blank treatment stain channel thresholds sensor noise into thousands of 1–2 px speckles. A cross-batch acquisition artifact, not biology. The object/distribution view is what exposed it (a single per-image % area would have hidden or inverted it) — which still showcases the paper’s “distributions over one number” thesis, with a methods-caution payoff.

Principled fix.

Result (principled DAB pipeline):

armmean objectsmean % stained areamedian obj areaper-object max DAB intensity
control (vehicle/DMSO)2423.03 %~21 px~1.50
treatment (4-oxo-RA)670.12 %~9 px~0.12

The ~26× area / ~3.6× object reduction is the headline, but it is confounded — read the caveat. The “max DAB intensity” column (1.50 vs 0.12) is not a clean biological readout; it is the fingerprint of the ~10× cross-batch DAB compression, and a fixed 0.1 threshold applied at treatment’s ceiling re-encodes that compression as the result. Treatment “objects” are mostly noise (52–73% are 1–2 px) and their median intensity (≈0.105) sits right at the 0.1 floor. Eccentricity is similar across arms (~0.87–0.92). The defensible effect, controlling for equal hematoxylin, is the ~2× lower mean DAB OD — suggestive of a real but modest reduction in infiltrate amount; the ~26× figure overstates it.


Recipe (how to redo)

Preconditions: /drive-scenario env (backend 8080, dev 5173, no test-tools flag), ihc-morphometrics-tools.yml installed (all imgteam + devteam/column_maker + bgruening/text_processing + iuc/collection_element_identifiers

  1. Upload the 6 ROIs as a named list via /api/tools/fetch (targets → hdca, elements src:url).
  2. Color deconvolution map-over: ip_color_deconvolution, convtype=rgb2hed.
  3. Split map-over: ip_split_image, axis=C, squeeze=false → nested list:list (6×3 channels).
  4. Extract DAB channel: __EXTRACT_DATASET__ with which=by_index, index=2 (0-based), batched with map_over_type:"list" to map the outer list → flat list of 6 DAB channels.
  5. Threshold map-over: ip_threshold, th_method|method_id=manual, threshold1=0.1.
  6. Label map-over: ip_binary_to_labelimage (v0.7.3), setup|method=cca.
  7. Features map-over (paired linked batch): ip_2d_feature_extraction, setup|mode=with-intensities, labels=<labelmaps>, setup|intensities=<DAB channels>, full feature set incl. mean_intensity.
  8. Aggregate (on-graph): collapse_dataset (one_header=true, add_name=true, place_name=same_multiple) → datamash (grouping=1; count/sum/median ops) → ggplot2_heatmap2 (zscore_cond|zscore=cols, PNG).
  9. Embed: graphicsmagick_image_convert masks → PNG; build the Page with history_dataset_collection_display, history_dataset_as_image, history_dataset_display directives.
  10. Extract: POST /api/workflows from_history_id + curated job_ids (clean DAB chain only) + dataset_collection_ids=[1]. Re-invoke via POST /api/workflows/{id}/invocations for the identity check.

Table 1 metrics

metricvalue
steps10 (1 input collection + 9 tools)
map-over steps6 (deconvolve → split → extract → threshold → label → features)
reduction/scalar steps3 (collapse, datamash, heatmap)
exposed outputsterminal heatmap (no extra exposed)
dangling inputs0
report/extraction warnings0
science-identical re-runbyte-identical per-sample summary (invocation 0a248a1f62a0cc04, history eaa9b06464bd346f, 39/39 jobs ok)

Figures


Changes to make next time

Caveats (the central one first — added after adversarial review)