Methods supplement
Every number on the poster comes from the stages below — from raw microscopy through cell segmentation, concentric file detection, curve fitting, and statistics. This page documents the actual algorithms and the parameter values used, not a summary of them.
Each stage is incremental and append-only — re-running never rebuilds existing results, so new images join the dataset without disturbing what came before.
A root is imaged as a cross-section, cropped, and split into four quadrants. Quadrants are the unit of segmentation; the root is the unit of analysis. Everything measured per quadrant is averaged back up to the root before any statistics are run, because quadrants from the same root are not independent samples.
Every figure on this page comes from the same quadrant — the bottom-right quarter of a maize root, well-watered, root type W2. You can watch the same 298 cells move through detection, segmentation, file counting and curve fitting, so each stage can be checked against the one before it.
Nothing here is a pretrained off-the-shelf detector
Three separate networks do three different jobs. All are YOLOv8, fine-tuned from COCO-pretrained weights on hand-annotated root imagery, and all run on CPU at inference so the pipeline needs no GPU to process new images.
| Model | Task | Base | Epochs |
|---|---|---|---|
| Root detector | Locate roots in the slide | yolov8l | 100 |
| Cell segmenter | Instance-mask each cell | yolov8n-seg | 100 |
| Inner-part segmenter | Mark valid cortex vs. background | yolov8l-seg | 50 |
Training augmentation is deliberately conservative: hue is shifted by at most 0.02 and rotation by 5°, because these images have a meaningful orientation and a physically meaningful stain colour. Aggressive augmentation would teach the model to ignore exactly the cues that distinguish a cell wall from a lumen.
The cell segmenter uses the smallest backbone of the three. A larger model was not better here — cells are small, numerous and highly repetitive, and the nano backbone at high confidence gave cleaner masks with far faster inference over thousands of quadrants.
Finding the root in the slide, then quartering it
A YOLO object detector locates root cross-sections in the slide image. Where several roots are detected, every one is cropped and saved, but only the highest-confidence detection is carried forward and split — this avoids partially-imaged roots at the frame edge entering the dataset.
The crop is halved on both axes into four quadrants, named by position:
TL_, TR_, BL_, BR_. This prefix is
load-bearing later: it tells the measurement stage which corner of the quadrant image
is the root's centre.
Each quadrant is written alongside a JSON sidecar recording the original image, the crop bounding box in both pixels and µm, and the pixel-to-µm conversion in force — so any measurement can be traced back to its source region.
Instance masks for every cortical cell, then three filters
A YOLO instance-segmentation model proposes a mask for every cell. The confidence threshold is deliberately severe — 0.98 — because a false cell distorts both the area distribution and the file-boundary detection downstream, whereas a missed cell mostly costs sample size. Masks are then refined against local pixel intensity and cleaned with morphological opening and closing.
Three filters run in sequence:
The same exclusion model supplies the two root-level geometry values — stele area and root radius — measured from its mask rather than from the cells.
The segmentation model is trained on maize and does not transfer to every species. For the tomato set, cells were instead segmented by hand and imported from COCO polygons. Those images therefore have no exclusion-model output, so stele area and root radius are recorded as unavailable rather than estimated — and tomato is automatically excluded from any analysis requiring them.
A human check before anything enters the dataset
Automatic segmentation fails occasionally — a torn section, a focus problem, a badly stained slide. Rather than let those propagate into the statistics, every newly processed image is reviewed before it joins the dataset.
The pipeline starts a local web server and opens a browser gallery of the new images with their detected cell centres drawn on. Each can be kept or rejected by eye; rejecting one deletes its measurement CSV and overlay so it never reaches the master summary. Review covers only new images — previously accepted ones are not re-litigated, which keeps the dataset stable as it grows.
A threshold-based automatic mode is also available, rejecting images outside a plausible cell count, but the results presented here were reviewed manually.
Turning masks into a radial coordinate system
Because each quadrant is one quarter of a circular cross-section, the root's centre lies at the quadrant's inner corner — the corner that was adjacent to the centre before splitting. That corner is inferred from the filename prefix. Every measurement is expressed relative to it.
| Quadrant | Centre (x, y) |
|---|---|
| TL | (width, height) |
| TR | (0, height) |
| BL | (width, 0) |
| BR | (0, 0) |
For a cell with centroid (x, y) and a centre at (cx, cy):
Area is the mask's pixel count, converted to µm² by the squared pixel scale. Cell area and radius in µm are what everything downstream uses.
Each cell also records a local density and its adjacency list — masks are dilated slightly and any cells whose dilated masks touch are recorded as neighbours.
Detecting concentric file boundaries from the area–radius relationship
Cortical cells are arranged in concentric files — rings running outward from the stele. Counting them by eye is slow and subjective, so they are detected from a signal in the data itself.
The idea. Within a single file, cells sit at a similar radius and a similar size, so plotting area against radius traces a locally smooth run. Crossing from one file into the next, cell size changes character and that run changes slope. Counting significant slope changes therefore counts the boundaries between files — and the number of files is one more than the number of boundaries.
Separating counting from assignment is deliberate. K-means needs to be told how many clusters to find and will happily split a uniform gradient into whatever number it is given; the derivative step supplies that number from the structure actually present in the image, so the clustering is constrained by evidence rather than by assumption.
An alternative counter — peak-finding on the empirical distribution of radii — is implemented
and can be selected in configuration, but derivative is the method used for all
results presented here.
One curve per quadrant, reduced to six numbers
Cell size is not constant across the cortex: it rises from the stele, peaks, then declines toward the epidermis — often with a small rise again at the very edge. That profile is the phenotype of interest, so it is fitted and reduced to six numbers.
Radius and area are each normalised to 0–1 within the quadrant, which removes absolute size so that curve shape can be compared between a large root and a small one. Cells are binned by radius, bin means are taken, and a cubic smoothing spline is fitted through them and evaluated on 200 points.
Writing ŷ for the fitted curve, p for the peak and m for
the post-peak minimum:
| Feature | Definition |
|---|---|
| Peak height | max ŷ |
| Peak position | argmax ŷ |
| Rise slope | (ŷₚ − ŷ₀) ÷ (xₚ + 0.001) |
| Decay slope | (ŷₘ − ŷₚ) ÷ (xₘ − xₚ + 0.001) |
| Minima position | argmin ŷ, searched after the peak |
| Outer-rise magnitude | max(ŷ after m) − ŷₘ |
The minimum is deliberately searched only after the peak. The decay slope is therefore measured from the peak to that minimum rather than to the curve's end — so it describes the decline itself and is not diluted by any recovery at the epidermis, which the outer-rise magnitude captures separately.
Every feature is computed twice, against two different x-axes:
radius_* — x is the cell's normalised radial position. Uses
every cell independently of file detection.file_* — x is the normalised cell-file index, using the mean
area of each file. Depends on stage 4 having found enough files.The two answer different questions — one about distance, one about developmental order — and they are reported separately throughout.
code/measurements/visualize_splines.pyWhy the analysis unit is the root, not the image
Quadrant filenames carry their root's identity: stripping the TL_/TR_/
BL_/BR_ prefix yields a root identifier shared by all four. Every
per-quadrant measurement is averaged across whichever quadrants are available to give one row
per root.
This matters statistically. Four quadrants of one root are four views of the same organism, not four independent observations; treating them as independent inflates the effective sample size and shrinks p-values artificially. All models are therefore fitted at root level — 1,285 rows rather than 4,511.
| Species | Roots |
|---|---|
| Zea mays | 1,192 |
| Pearl millet | 66 |
| Tomato | 19 |
| Sorghum | 8 |
What was tested, and how significance was established
Differences between root types and between water treatments are tested with one-way ANOVA at root level, with effect size reported as η² — the proportion of variance the factor explains. Where ANOVA is significant, Tukey HSD gives all pairwise comparisons with family-wise error controlled at 0.05, so multiple comparisons do not inflate the false-positive rate. A Kruskal–Wallis test is computed alongside as a non-parametric check.
To compare profile shape between groups, all binned points from a group are pooled and a single curve is fitted to them — both a cubic polynomial (reporting R², peak and minimum) and the same smoothing spline used per-root. This is done by root type, treatment, species, and population.
Two questions need plant identity accounted for, since several roots may come from one plant:
The first asks whether a shape feature is merely a by-product of root size; the second partitions the remaining variance across experimental factors. Plant enters as a random intercept; where the mixed model fails to converge, an OLS fit is reported and flagged as such.
Part two
The seven stages above produce the headline numbers. These run alongside them, on the same measurements, asking questions the cell-area profile alone cannot answer.
Treating the cortex as a graph rather than a set of points
Everything above treats a cell as a point at a radius. But cells are packed against each other, and which cells touch which is itself anatomy. Every cell therefore also carries an adjacency list.
Adjacency is measured from the masks, not from centroid distance — two cells are neighbours if their masks touch when slightly dilated. This is stricter and more meaningful than a distance cutoff: cells that merely lie near each other across a gap are not counted, while genuinely abutting cells are, regardless of their size.
Each cell stores the IDs of its neighbours and its degree — the number of cells it touches. In a perfectly regular tissue every interior cell would have six; departures from that are what the analysis looks at.
The graph supports a question the radial coordinate cannot answer: how many cells do you cross walking from the epidermis to the stele, and how far is it in µm?
Because the walk is greedy and forbids revisiting, it terminates at a local radial minimum — which in a well-segmented cortex is the innermost cell file. Paths are drawn back onto the image so a wrong turn is visible rather than silent.
Two relationships are then plotted across the whole dataset: degree against radial position, and degree against cell area. Both are computed raw, binned (20 bins), and as a rolling average (50-cell window), with normalised variants so that roots of different absolute size can be overlaid. The question is whether larger cells systematically touch more neighbours, or whether contact number is set by position in the cortex instead.
code/measurements/neighbor_analysis.py · mask_neighbors.py · analyze_degree_vs_area.pyWhere the tissue is packed, and where it isn't
Cell centres are binned onto a polar grid — radial bins by angular bins — and each cell is given a local density from the number of cells within a fixed radius of it. That grid supports three views: a polar projection map, a radial profile (density against distance from the centre) and an angular profile (density against direction).
The angular profile matters because a quadrant is a quarter of a circle: if segmentation were failing on one side, the angular profile would show it as a dip rather than hiding it in a whole-image average.
The grid also drives air pocket detection. Regions in the lowest density decile that also sit in the highest cell-area decile are flagged — the signature of intercellular space, where few, large cells occupy a region that should be densely packed. In maize this is how aerenchyma would present.
Letting the data choose the shape descriptors
The six spline features are chosen by hand, which risks describing what was expected rather than what is there. Functional PCA is the check on that. Every root's normalised curve is resampled onto a common grid and treated as a single observation — a whole function, not a set of summary numbers — and PCA is run over those functions.
The output is a set of principal component functions: the dominant ways curves actually vary across the dataset, ranked by how much variance each explains, plus a score per root on each component. If the leading components look like "peak moves outward" and "peak gets taller", the hand-picked features are capturing the real axes of variation. If they look like something else, the feature set is missing something.
Mean cell size file by file
Once cells carry a file index, the simplest possible summary is the mean cell area of each file, plotted against file number — a profile that needs no spline and no normalisation. These are produced per image, pooled across the dataset, and normalised so roots with different file counts can be overlaid and compared by group.
This is the most direct view of the phenomenon the whole project is about, and it serves as a sanity check on the fitted curves: the spline is meant to be a smooth description of exactly this, so where the two disagree, the spline is over-smoothing.
A companion regression asks whether file number predicts cell size within a root, and whether the strength of that relationship differs between groups.
code/cell_file/cell_file_group_analysis.py · cell_file_regression.py · statistics/cell_area_per_file.pyChecking that a global relationship holds inside every group
A relationship fitted across the whole dataset can be produced entirely by group differences — Simpson's paradox, where a trend that holds overall reverses within every subgroup. The stratified analysis guards against that by refitting the same relationships separately within each treatment, root type and population, and comparing coefficients across strata.
Interaction plots complement this by showing group means for one factor split by another, so a treatment effect that exists only in one root type is visible rather than averaged away. A two-way ANOVA with an interaction term tests the same thing formally.
code/statistics/stratified_analysis.py · interaction_plots.py · factor_analysis.pyOne shared curve, fitted to everything
A separate track fits a single normalised pattern across all images and asks how far each individual departs from it, rather than describing each root independently. Cells are sampled along fixed angles, splines are fitted to those samples, and a cubic model of the pooled data supplies the reference. A clustered variant groups roots first and fits one pattern per cluster, testing whether the dataset is better described by several distinct patterns than by one.
Part three
Using the measurements to predict, rebuild, and eventually to test whether the arrangement does structural work.
How little of a root do you need to measure to predict the rest?
A practical question follows from the anatomy: if measuring a whole root is expensive, which cheap measurements predict the rest? Candidate predictors are drawn from measurements obtainable without the full spline pipeline — first, last and middle cell-file areas, their ratio, overall mean cell area, and total cell count.
An exhaustive search evaluates every combination of one, two or three of those predictors against every target, fitting polynomial regressions (degree 1–3 for single predictors, 1–2 for combinations) on a fixed 75/25 train–test split, and ranks by test R². Geometry is deliberately barred from the predictor pool so that no target is predicted from another geometry measurement — which would report a near-perfect score that reflects a definition rather than a discovery.
Stele area and stele diameter are related by A = π(d/2)². A model predicting one
from the other scores R² = 1.00 while discovering nothing. Excluding such pairs is why the
reported size R² values are lower than an unconstrained search would produce — and why they
mean something.
The headline result is that cell count predicts file count well, size predicts size well, but the six shape features remain only weakly predictable from any small combination tested — with peak height essentially unpredictable throughout. That is a genuine finding about the anatomy rather than a limitation of the search, which covered every combination available.
Running the pipeline backwards
The six shape features define a normalised curve, and that curve can be inverted back into a cross-section: absolute cell area per file, a radial thickness per ring, and a cell count per ring. An interactive tool reconstructs any root from its features, which serves as a check that the features capture what they claim to — if a reconstruction from six numbers resembles the original image, little has been lost.
Curve reconstruction uses monotone cubic (Fritsch–Carlson) interpolation between the anchor points implied by the features, chosen because it cannot overshoot — an overshoot would move the peak or minimum away from the values being reconstructed.
To test whether the observed arrangement is structural, reconstructions are exported as 3D solids. Two different solids serve two different purposes:
Both extrude to three times the mean cell diameter. Control geometries — uniform cell size, or randomised positions, each matched to the same relative density — are generated by the same code path, so a comparison against them isolates the pattern rather than the porosity.
Infrastructure that supports the pipeline without being part of it
Every stage checks what already exists and processes only what is new. The master summary is appended to, never rebuilt, and existing rows keep their original species and population — so adding a new species cannot silently relabel an old one. Rebuilding from scratch is possible but requires an explicit flag, because it would overwrite exactly that history.
Where the segmentation model does not transfer to a species, cells are annotated manually in Roboflow and exported as COCO polygons. An importer rasterises those polygons and writes the identical measurement format the model path produces — same columns, same plant-centre convention, same neighbour graph — so hand-annotated images are analysed by exactly the same code and are not a parallel path with its own quirks.
code/measurements/coco_to_measurements.pyKnown constraints on how far these results reach