← The cortical cell pattern

Methods supplement

How the root anatomy pipeline works

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.

Roots analysed
1,708
Quadrant images
6,211
Species
4
Stages
7

The pipeline end to end

Each stage is incremental and append-only — re-running never rebuilds existing results, so new images join the dataset without disturbing what came before.

1Root detection
Locate the root in the slide image, crop, split into quadrants
2Cell segmentation
Instance-segment every cortical cell; exclude non-cortex
2.5Quality control
Browser review of new images; reject bad segmentations
3Measurement
Per-cell area, radius and angle from the stele centre
4Cell file counting
Derivative method — detect concentric file boundaries
5Shape features
Spline fit of the cell-area profile; six descriptors
6Aggregation
Four quadrants combine into one root record
7Statistics
ANOVA, Tukey HSD, mixed-effects models

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.

One example, followed all the way through

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.

The three models, and how they were trained

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.

ModelTaskBaseEpochs
Root detectorLocate roots in the slideyolov8l100
Cell segmenterInstance-mask each cellyolov8n-seg100
Inner-part segmenterMark valid cortex vs. backgroundyolov8l-seg50

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.

Training
image size
640 px
batch
4
max detections
3,000
hue / sat / value jitter
0.02 / 0.5 / 0.4
rotation
±5°
translation
0.1
warmup
3 epochs

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.

01

Root detection and quadrant split

Finding the root in the slide, then quartering it

Input
Whole-slide microscopy image (PNG/JPG)
Output
Cropped root + four quadrant images, each with a metadata sidecar

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.

Whole slide image with a detection box drawn around the root cross-section.
a · detectThe slide, with the detector's box. A second, fainter root here is cropped but not split.
The root cross-section cropped out of the slide image.
b · cropThe winning detection, cropped to the root.
The bottom-right quadrant of the cropped root.
c · splitThe BR quadrant — the example followed through this page. Its root centre is the top-left corner.
Parameters
detector
YOLO, single class "root"
confidence
0.50
quadrants
4, split at the crop midpoint
carried forward
highest-confidence detection only

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.

02

Cell segmentation

Instance masks for every cortical cell, then three filters

Input
One quadrant image
Output
One binary mask per cell, plus stele geometry

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:

Parameters
cell confidence
0.98
max detections
3,000 per quadrant
exclusion confidence
0.25
exclusion overlap
25.0%
cell overlap limit
15.0%
morphology
open 21 px, close 21 px
The quadrant with a red dot marking the centroid of every segmented cell and a blue dot at the root centre.
298 cells segmented and surviving all three filters, each marked at its centroid. The blue dot in the top-left corner is the root centre every measurement is taken from. Note that the stele — the dense vascular tissue at upper left — carries no markers: the exclusion model has removed it.

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.

Where the model doesn't generalise

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.

2.5

Quality control

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.

Parameters
mode
interactive browser review
scope
new images only
auto thresholds
10 … 5,000 cells (available, unused)
03

Per-cell measurement

Turning masks into a radial coordinate system

Input
Cell masks for a quadrant
Output
One row per cell: position, radius, angle, area, neighbours

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.

QuadrantCentre (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):

r = √((x−cx)² + (y−cy)²) θ = atan2(y−cy, x−cx)

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.

Parameters
scale (default)
50 µm ÷ 77 px ≈ 0.6494 µm/px
per-dataset override
supported, via a scale key CSV
area
mask pixel count × scale²
neighbour expansion
20 px, max 7 neighbours

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.

04

Cell file counting — the derivative method

Detecting concentric file boundaries from the area–radius relationship

Input
All cells in a quadrant, with radius and area
Output
File count, and a file index per cell (0 = innermost)

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.

Scatter of cell area against radius with a red rolling-slope curve overlaid and green dashed lines at detected file boundaries.
The signal the method actually reads. Blue points are the 298 cells — area against radius, noisy enough that boundaries are not obvious by eye. The red curve is the rolling slope dA/dR; green dashed lines mark where its rate of change clears the threshold. Fourteen boundaries were accepted, giving 15 files.

The algorithm, step by step

  1. Sort by radius. All cells in the quadrant are ordered by distance from the root centre, giving a sequence that walks outward from the stele.
  2. Local slope. A window of 17 consecutive cells slides along that sequence. At each position a straight line is least-squares fitted to area against radius, and its slope is recorded against the window's mean radius. The window shrinks for small samples — never above one third of the cell count, never below 5.
  3. Smooth. The resulting slope series is smoothed with a Savitzky–Golay filter (width ≤ 7, quadratic), which suppresses single-cell noise while preserving the position of genuine transitions — the property that matters here.
  4. Differentiate again. Taking the first difference of the smoothed slopes gives a curvature signal: large values mark where the area–radius relationship is turning.
  5. Threshold adaptively. A change counts as real when its magnitude exceeds 1.7 × the standard deviation of all changes in that quadrant. The threshold is per-image, so it adapts to noisy and clean images alike rather than assuming a fixed scale.
  6. Enforce separation. Accepted changes must be at least 2 positions apart, preventing one broad transition from being counted several times.
  7. Count. The file count is the number of accepted changes plus one, capped at 30. Quadrants with fewer than 15 cells are not analysed and return a single file.
  8. Assign cells. With the count k known, one-dimensional k-means on radius alone partitions cells into k groups. Clusters are then relabelled in ascending mean radius, so file 0 is always the innermost.
Parameters
method
derivative
slope window
17 cells (clamped 5 … n/3)
smoothing
Savitzky–Golay, width ≤ 7, order 2
change threshold
1.7 × SD of slope changes
min separation
2 positions
min cells
15
max files
30
assignment
1-D k-means on radius, relabelled inward → outward
The quadrant with every cell coloured and numbered by its assigned cell file, forming concentric bands from the stele outward.
The result mapped back onto the tissue: every cell coloured and numbered by its file, 0 at the stele through 14 at the epidermis. The bands follow the visible concentric rings — the check that matters, since the boundaries were derived from the area signal alone and never from the image.

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.

code/cell_file/cell_file_analyzer.py
05

Shape features from the cell-area profile

One curve per quadrant, reduced to six numbers

Input
Cell areas and positions for a quadrant
Output
Six shape descriptors, on two different bases

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.

Building the curve

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.

Parameters
bins
20 (or n/3 when fewer cells)
spline
cubic, k = 3
smoothing
s = 0.1 × n points
evaluated at
200 points on [0, 1]
minimum
5 points

The six features

Writing ŷ for the fitted curve, p for the peak and m for the post-peak minimum:

FeatureDefinition
Peak heightmax ŷ
Peak positionargmax ŷ
Rise slope(ŷₚ − ŷ₀) ÷ (xₚ + 0.001)
Decay slope(ŷₘ − ŷₚ) ÷ (xₘ − xₚ + 0.001)
Minima positionargmin ŷ, searched after the peak
Outer-rise magnitudemax(ŷ after m) − ŷₘ
The fitted spline of normalised cell area against normalised radius, with the peak and post-peak minimum marked.
The same quadrant as a curve. Cell area climbs from the stele to a peak at 0.31 of the way out, decays to a minimum at 0.77, then rises again toward the epidermis — the outer rise, here 0.165. The six numbers in the box are what enter the statistics; the curve itself is not stored.

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.

Two bases

Every feature is computed twice, against two different x-axes:

The two answer different questions — one about distance, one about developmental order — and they are reported separately throughout.

code/measurements/visualize_splines.py
06

Quadrants to roots

Why 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.

SpeciesRoots
Zea mays1,192
Pearl millet66
Tomato19
Sorghum8
07

Statistics

What was tested, and how significance was established

Group comparisons

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.

Curve comparisons

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.

Mixed-effects models

Two questions need plant identity accounted for, since several roots may come from one plant:

shape ~ n_files + stele_diameter + (1 | plant)
shape ~ treatment + root_type + population + (1 | 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

Analyses beyond the main pipeline

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.

Neighbour analysis

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.

Building the graph

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 cell centres of the example quadrant joined by cyan lines wherever two cells touch, each labelled with its degree.
The same quadrant as a graph: 298 cells, 864 contacts, mean degree 5.80 — close to the six of an ideal packing. Degrees run 1 to 9, and the low values sit almost entirely on the boundary, where cells have neighbours outside the crop. Interior structure is strikingly regular.
Parameters
adjacency test
dilated-mask contact
dilation
2 px inline, 20 px in batch reanalysis
max neighbours recorded
7
local density radius
100 px

Tracing a path to the stele

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?

  1. Pick a sample angle and take the cells lying within a tolerance of it.
  2. Start at the outermost of those — the cell with the largest radius.
  3. Step to whichever unvisited neighbour has the smallest radius, i.e. move inward through actual cell contacts rather than in a straight line.
  4. Repeat until no neighbour lies closer to the centre. That is the stele boundary.
  5. Record the path: how many cells it crossed, and the summed physical distance between consecutive centroids in µm.
Five coloured paths traced from the outer edge of the cortex inward to the stele across the example quadrant.
Five paths traced at 0°, 22.5°, 45°, 67.5° and 90°. Circles mark the starting cell at the epidermis, squares the stele end. They take 10 to 12 steps and cover 438–537 µm — so the cortex is a fairly consistent number of cells thick whichever way you cross it, even though the paths visibly wander sideways rather than running radially.

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.

Degree against size

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.py

Density and air pockets

Where 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).

Polar heat map of cell density across the example quadrant, showing an empty centre and a banded cortex.
Density across the example quadrant, drawn in its true polar geometry from 0° to 90°. The dark wedge at the centre is the stele, correctly empty of cortical cells. Density is broadly even around the arc — no angular dead zone — which is the check that segmentation did not fail on one side.

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.

Parameters
radial bins
20
angular bins
36 (10° each)
density radius
100 px
air pocket
density ≤ 10th pct AND area ≥ 90th pct
code/measurements/density_analysis.py

Functional PCA

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.

The mean cell-size curve with dashed curves above and below showing the effect of the first principal component.
The first component alone accounts for 52.3% of all curve variation, and it is essentially how pronounced the hump is — the dashed curves shift the whole profile up and down while leaving the peak roughly where it is. That is reassuring for the feature set: peak height is doing most of the work, and it is one of the six.
code/statistics/run_fpca.py

Cell-file profiles

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.

Mean cell area plotted against cell file number for the example quadrant.
The example quadrant with no curve fitting at all — just mean cell area for each of its 15 files, in µm². The rise, the peak partway out, and the decline are all visible directly in the raw file means, which is what the spline in stage 5 is a smooth description of.

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.py

Stratified and interaction analysis

Checking 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.

Bar chart of model fit R-squared computed separately within each root type.
The same relationship refitted inside each root type. It holds in all three, but not equally well — R² = 0.25, 0.22 and 0.47 for W2, W4 and W3. A pooled fit would have reported one number and hidden that spread. (n here counts quadrant images, not roots.)

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.py

Normalised pattern fitting

One 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.

A single averaged normalised cell-size pattern fitted across all images.
One pattern fitted across the whole dataset. Individual roots scatter around it, and how far each sits from this reference is itself a measurement — an alternative to describing every root with its own six numbers.
code/non_tda/ — non_tda_analysis · non_tda_analysis_clustered · fit_normalized_pattern

Part three

Prediction, reconstruction, and physical models

Using the measurements to predict, rebuild, and eventually to test whether the arrangement does structural work.

Predictive modelling

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.

A trap worth naming

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.

Reconstruction and physical models

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.

Export for structural testing

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.

Supporting tooling

Infrastructure that supports the pipeline without being part of it

Incremental, append-only processing

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.

Hand-annotation import

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.py

Limitations

Known constraints on how far these results reach