Skip to content

Add scaled circular montage for fair density comparison across sample sizes - #408

Open
animmosmith wants to merge 4 commits into
405-fix-junge-slope-uninitialized-memoryfrom
407-scaled-circular-montage
Open

Add scaled circular montage for fair density comparison across sample sizes#408
animmosmith wants to merge 4 commits into
405-fix-junge-slope-uninitialized-memoryfrom
407-scaled-circular-montage

Conversation

@animmosmith

Copy link
Copy Markdown
Collaborator

Summary

Fixes #407.

`make_montage()` always fills the same fixed rectangular canvas regardless of how much data went into it, which makes visual density comparisons across datasets with different sample sizes (e.g. several depth bins with different numbers of raw images) misleading - every montage looks equally "full" no matter how much data it actually represents.

`make_montage_scaled()` packs particles largest-first within a circular boundary whose area (not radius) is controlled by `rel_scale` - the radius scales with `sqrt(rel_scale)` specifically so that area scales linearly with it. Set `rel_scale` proportional to each dataset's relative sample size (e.g. number of raw images, or total sample volume) against a shared reference, generate one montage per dataset, and place them side by side: a bin with half the raw images of another gets half the circle area to fill, so the packed density becomes a fair, directly comparable visual signal across montages.

A few other differences from `make_montage()`:

  • Grayscale output rather than RGB, since particles from monochrome instruments (e.g. holographic imaging) don't need three channels — this also means `pyopia.plotting.montage_plot()`'s existing `cmap='grey'` argument actually takes effect (currently a no-op against `make_montage`'s RGB output).
  • Gap-aware packing: each particle's silhouette is dilated by `gap` pixels before placement, so packed particles keep a visual buffer rather than touching.
  • No new plotting code needed - the existing `montage_plot()` already handles the scale reference via matplotlib, so this only needed to produce a montage image, not its own scale-bar rendering.

Test plan

  • Manually verified with synthetic particle ROIs (grayscale blob images with real internal contrast) that particles are placed correctly (non-background pixel values present) and that the circular boundary area scales as intended (rel_scale=0.25 produces ~25% of the pixel area of rel_scale=1.0, within ~0.3% of the expected ratio)
  • flake8 clean

🤖 Generated with Claude Code

@animmosmith animmosmith self-assigned this Jul 31, 2026
@animmosmith animmosmith added patch / enhancement improved functionality or patch indented for changes that require bumping only the PATCH number priority-low labels Jul 31, 2026
animmosmith added a commit that referenced this pull request Jul 31, 2026
README's "Build docs locally" section now syncs the docs dependency group
via uv before building, matching the RTD config. Bumped version to 2.16.20
so this PR is ready to merge right after #394, #404, #406, #408, #410.
@animmosmith
animmosmith requested a review from emlynjdavies July 31, 2026 14:47
@animmosmith
animmosmith changed the base branch from main to 405-fix-junge-slope-uninitialized-memory July 31, 2026 17:00
Comment thread pyopia/statistics.py Outdated
stats = stats.sort_values(by=["major_axis_length"], ascending=False)

roifiles = stats["export_name"][stats["export_name"] != "not_exported"].values
roifiles = roifiles[:max_particles]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be calling gen_roifiles() here?

animmosmith and others added 2 commits August 5, 2026 11:07
… sizes

Fixes #407.

make_montage() always fills the same fixed rectangular canvas regardless of
how much data went into it, making visual density comparisons across
datasets with different sample sizes (e.g. depth bins with different
numbers of raw images) misleading - every montage looks equally "full."

make_montage_scaled() packs particles largest-first within a circular
boundary whose area (not radius - area scales with sqrt(rel_scale) as the
radius) is controlled by rel_scale. Setting rel_scale proportional to each
dataset's relative sample size and placing the resulting montages side by
side gives a fair visual comparison: half the raw images means half the
circle area to fill.

Also outputs grayscale rather than RGB, since particles from monochrome
instruments don't need three channels, and it makes montage_plot()'s
existing cmap='grey' argument actually take effect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@animmosmith
animmosmith force-pushed the 407-scaled-circular-montage branch from 28c6790 to 43d6225 Compare August 5, 2026 10:07
Prompted by Emlyn's review comment questioning the roifiles selection here.

Truncating to the max_particles largest particles (or evenly subsampling, as
gen_roifiles() does for the older make_montage) both misrepresent the true
relative abundance of particle sizes - a scaled montage's whole purpose is a
fair visual comparison, so it should reflect the real size distribution, not
an artificially selected subset. Every exported particle is now attempted,
largest first; particles that can't find a free spot are skipped as before,
but now a warning is logged summarising how many were skipped once the
montage is complete, since that means msize needs to be increased (not
rel_scale, which exists specifically to preserve relative comparisons and
would be distorted by nudging it to fit one particular montage).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch / enhancement improved functionality or patch indented for changes that require bumping only the PATCH number priority-low

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a scaled circular montage for fair density comparison across sample sizes

2 participants