spacr.illumination

Workflow inputs and outputs

Illumination

Estimate and apply flat-field correction at configured stages. Keep corrected display values distinct from raw measurements and record the applied policy.

Open: Measure → Illumination.

Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.

Inputs

  • Microscope images — Source image folder; original files, supported vendor files or imported TIFFs.

Outputs

  • Microscope images — Source image folder; original files, supported vendor files or imported TIFFs.

API reference.

Module tutorial.

Illumination / flat-field correction for the measurement path.

The problem

No microscope lights a field of view evenly. A lamp profile, vignetting in the objective, a tilted condenser and dirt on the optics together make the same cell measure brighter at the centre of the field than at its edge – routinely 10-40 % between the middle and a corner on a widefield screen. Every intensity feature spaCR writes (*_mean_intensity, *_percentile_*, the radial distribution, the texture channels that are computed on intensity) therefore carries a position-dependent bias, and because objects are not distributed identically over every well, that bias does not average out: it survives into the per-well aggregate, and from there into classification and regression as an effect that looks entirely real and is entirely an artefact of the optics.

It is also the root cause of the plate-scale edge effects spacr.plate_qc already detects and reports. Detecting them is useful; removing them is what changes the answer.

The model

Per channel, per plate:

observed(y, x) = dark + flat(y, x) * true(y, x)

flat is the multiplicative illumination field, normalised so its mean is 1 – the plate’s overall intensity level is preserved, so a corrected number stays on the same scale as an uncorrected one and only its position dependence is removed. dark is the additive camera offset. The correction the hook applies is:

corrected = (observed - dark) / flat

What is estimated, and why

Retrospective, from the data itself: a per-pixel median across many fields of the plate, followed by a fit of a smooth low-order surface.

Why the median across fields. A pixel is covered by a cell in only a minority of the fields on a plate, so the across-field median at that pixel sees background almost every time and the objects drop out. Each field is first divided by its own median, so a densely-seeded field does not pull the estimate up because it has more cells in it – what is being averaged is the relative profile, not the brightness.

Why the surface fit on top. Illumination is a physically smooth, low-frequency function of position: a lamp profile plus a vignette. Fitting a low-order 2-D polynomial (default degree 4, 15 terms) to the per-pixel median imposes exactly that prior, so residual object structure and photon noise cannot leak into the gain map and be baked into every measurement on the plate. The fit is trimmed twice against a MAD threshold, so a persistent bright artefact – a fluorescent speck in the same place on every field – is rejected rather than smeared into the surface. For illumination that is genuinely not polynomial (a dust shadow, a sharply structured lamp) pass estimator='smooth': the same per-pixel median, Gaussian-smoothed at 1/16 of the short side and interpolated back to full resolution.

Why not BaSiC. BaSiC’s low-rank + sparse decomposition is the better estimator when you have hundreds of fields and a genuine dark-field to recover, but it is an iterative optimisation with its own convergence failure modes, and its dark-field term is only identifiable because of those extra assumptions. From a single acquisition, dark and flat are not separately identifiable at all: multiplying flat by a constant and absorbing it into the per-field brightness leaves every observation unchanged. Estimating a dark-field anyway – for instance from the per-pixel minimum across fields, which is the usual shortcut – returns dark plus the dimmest background, and subtracting it removes real signal. So spaCR does not guess: dark is 0 unless you supply the camera offset you measured from a dark frame, via illumination_dark.

Reaching the worker processes

spacr.measure.measure_crop() measures fields in a multiprocessing.Pool. Under spawn / forkserver each worker is a fresh interpreter with an empty hook registry, so a correction registered only in the parent applies to nothing while the run looks perfectly normal – the single worst outcome for this feature, because the user then believes their numbers are corrected. enable_illumination_correction() therefore does not merely register the hook: it writes the model path to MODEL_ENV_VAR and appends spacr.illumination:install to SPACR_MEASURE_HOOKS, which every start method inherits, so each worker installs the correction for itself. worker_delivery_status() reports whether that is actually in place, and enable_illumination_correction() prints it.

Using it

Off by default. From a settings dict (the keys are registered through the spacr.settings.register_defaults() seam, see illumination_settings()):

settings = get_measure_crop_settings(settings={...})
settings['illumination_correction'] = True
prepare_illumination_correction(settings)   # estimate, save, enable, QC
measure_crop(settings)

or explicitly:

model = estimate_illumination(src, channels=[0, 1, 2])
model.save('/data/plate1/illumination/illumination_model.npz')
illumination_qc(model, src, save_dir='/data/plate1/illumination')
enable_illumination_correction('/data/plate1/illumination/illumination_model.npz')

Nothing in this module runs unless one of those calls is made, and disable_illumination_correction() returns the process to a state where measure_crop measures exactly what it measured before.

In the GUI the same two routes exist and end here: the “Illumination Correction” category on the Measure panel, which is the switch thrown on the run whose numbers it changes, and the Illumination button on that screen’s masthead, which opens this module’s own settings form and Run button so the field can be estimated and QC’d without measuring the plate. Neither is a tile: the module folded into Measure and left the app registry with it.

Exceptions

IlluminationError

Illumination correction was asked for and could not be delivered.

Classes

IlluminationCorrector

The preprocessing hook that applies an IlluminationModel.

IlluminationField

The illumination estimate for one plate, for one or more channels.

IlluminationModel

Estimated illumination fields for every plate in a source folder.

PreparedIllumination

One fitted/loaded model and the stage-neutral objects derived from it.

SegmentationIlluminationSession

Apply one illumination model exactly once per segmentation field.

Functions

disable_illumination_correction(→ bool)

Turn the correction off, here and for any worker started afterwards.

enable_illumination_correction(→ str)

Turn the correction on, here and in every worker process.

estimate_illumination(→ IlluminationModel)

Estimate the illumination field from the merged fields in src.

illumination_qc(→ Dict[str, Any])

Show that the correction worked, and say by how much.

illumination_settings([settings])

Defaults for illumination correction. Registered through the seam.

install(→ str)

Install the correction in this process from the environment.

load_illumination_model(→ IlluminationModel)

Read a saved model. Thin alias for IlluminationModel.load().

load_segmentation_illumination_resume(...)

Read and validate prior segmentation illumination without side effects.

plate_of_field(→ str)

The plate a merged field belongs to, from its file name.

position_intensity_slope(→ float)

Least-squares slope of intensity against distance from the field centre.

prepare_illumination_correction(settings, *[, verbose])

Estimate, save, enable and QC the Measure correction.

prepare_illumination_model(...)

Prepare one reusable optical model without installing a Measure hook.

prepare_segmentation_illumination(...)

Prepare correction for segmentation inputs without changing raw data.

register_illumination_settings(→ bool)

Register the illumination settings through the defaults seam.

validate_measurement_illumination_inputs(→ Dict[str, ...)

Fail closed before Measure corrects pixels a second time.

validate_segmentation_illumination_resume(→ Dict[str, Any])

Validate a preprocess=False mask resume without writing anything.

worker_delivery_status(→ Tuple[bool, str])

Whether the correction will actually reach measure_crop's workers.

Module Contents

exception spacr.illumination.IlluminationError[source]

Bases: spacr.errors.ConfigurationError

Illumination correction was asked for and could not be delivered.

A spacr.errors.ConfigurationError, not a per-field data error: every failure this class reports (no fields to estimate from, a model that does not cover the plate being measured, a channel the model was never estimated for) is wrong for the whole run, and the alternative – measuring on quietly uncorrected pixels – is the outcome this module exists to prevent.

Initialize self. See help(type(self)) for accurate signature.

class spacr.illumination.IlluminationCorrector(model: IlluminationModel, *, on_missing: str = 'error', verbose: bool = True)[source]

The preprocessing hook that applies an IlluminationModel.

Registered through spacr.measure_hooks.register_preprocessing_hook(), so it is handed exactly the array the intensity measurements see – the channels named by settings['channels'], selected out of the merged stack, before a single feature is computed.

The dtype round trip is this class’s decision, and it is made here rather than in the hook machinery on purpose (see spacr.measure_hooks.apply_preprocessing_hooks()). Integer input is corrected in float32 and returned by rounding to nearest and then clipping to the dtype’s range:

  • round, not truncate. Truncation would shave a mean of 0.5 counts off every corrected pixel. Averaged over a 500-pixel object that does not wash out – it is a systematic, one-directional shift of exactly the kind this feature exists to remove. Rounding is unbiased.

  • clip, and count. A gain above 1 at the edge of the field can push a near-full-scale pixel past the top of a uint16. Clipping is the only option that keeps the dtype the hook contract requires, but silently clipping real signal is a lie about the data, so every pixel that was below full scale before the correction and lands at full scale after it is counted and reported. Pixels that were already saturated are not counted: they were destroyed by the microscope, not by this class.

Float input is returned in its own float dtype with no rounding and no clipping at all – there is nothing to round to and no range to leave.

Parameters:
  • model – the estimated IlluminationModel.

  • on_missing – 'error' (default) or 'skip' for a field whose plate the model does not cover. The default is to fail the field: a half-corrected table is worse than a failed one.

  • verbose – print the clipping reports.

Arm a corrector over a fitted illumination model.

Parameters:
  • model – the fitted model to divide fields by.

  • on_missing – what to do with a field the model has no profile for – 'error' fails the field, 'skip' measures it uncorrected and counts it.

  • verbose – report the first few clipping events.

Raises:

IlluminationError – if on_missing is neither of the two.

__call__(channel_arrays: numpy.ndarray, context) → numpy.ndarray[source]

Return channel_arrays corrected, in the same shape and dtype.

Parameters:
report() → str[source]

One line summarising what this corrector has done so far.

class spacr.illumination.IlluminationField[source]

The illumination estimate for one plate, for one or more channels.

Parameters:
  • plate – the plate key, or ALL_PLATES when the model was estimated across every plate at once.

  • channels – source channel indices, in the order they index flatfield’s first axis. These are indices into the merged stack, i.e. exactly the values in settings['channels'].

  • flatfield – (C, Y, X) float32 multiplicative field, normalised so each channel’s mean is 1.0. corrected = (observed - dark) / flatfield.

  • dark – per-channel additive offset subtracted before dividing. Zero unless the user supplied a measured camera offset – see the module docstring for why it is not estimated.

  • n_fields – how many fields the estimate was made from.

  • estimator – 'polynomial' or 'smooth'.

  • degree – polynomial degree, or 0 for the smooth estimator.

  • bin_size – the binning factor the per-pixel median was computed at. Illumination is low-frequency, so binning costs nothing and buys both the memory to hold many fields at once and a quieter statistic.

  • floored – pixels the fitted surface had to be floored at (see FLAT_FLOOR_FRACTION). Non-zero means the fit went negative somewhere and the estimate should be looked at before it is trusted.

  • darkfield – optional (C, Y, X) spatial background subtracted on top of dark. Only a vendor profile that ships its own background surface carries one; an estimated field never does.

dark_stack(channels: Sequence[int]) → numpy.ndarray[source]

(C,) additive offsets for channels, ready to broadcast.

Parameters:

channels – source channel indices in the order required by the array being corrected.

describe() → str[source]

One line per channel: range, non-uniformity and how it was made.

gain_stack(channels: Sequence[int]) → numpy.ndarray[source]

(Y, X, C) multiplicative gains 1 / flatfield for channels.

Shaped for the array a preprocessing hook is handed, so applying the correction is one broadcast multiply.

Parameters:

channels – source channel indices, in the order they appear along the last axis of the array being corrected.

index_of(channel: int) → int[source]

Position of source channel along flatfield’s first axis.

Parameters:

channel – a merged-stack channel index.

Raises:

IlluminationError – if the model was never estimated for it. Correcting the channels that happen to be present and leaving the rest alone would put corrected and uncorrected numbers in the same table.

nonuniformity() → Dict[int, float][source]

Per channel, (p98 - p2) / mean of the field, as a fraction.

The headline “how uneven is this microscope” number: 0.30 means the bright and dim ends of the field of view differ by 30 % of the mean, and therefore so does the same cell measured in those two places.

property shape: Tuple[int, int][source]

(Y, X) pixel shape of the estimated field.

class spacr.illumination.IlluminationModel[source]

Estimated illumination fields for every plate in a source folder.

Parameters:
  • fields – plate key -> IlluminationField. A model estimated with per_plate=False holds the single key ALL_PLATES, which matches every plate.

  • meta – provenance – source folders, channels, when it was estimated, the settings it was estimated with. Written into the .npz and read back, so a model on disk can always say what produced it.

describe() → str[source]

Every field’s IlluminationField.describe(), one per line.

field_for(plate: str) → IlluminationField[source]

The IlluminationField that applies to plate.

Parameters:

plate – plate key whose estimated illumination field is needed.

Raises:

IlluminationError – when nothing in the model covers it. This is deliberately not a fall back to “some other plate’s field”: illumination differs between acquisition sessions, which is the whole reason the default is one field per plate.

classmethod load(path: str) → IlluminationModel[source]

Read a model written by save().

Parameters:

path – the .npz file.

Raises:

IlluminationError – when the file is missing or is not a spaCR illumination model. A worker that cannot load the model must say so rather than measure uncorrected pixels.

save(path: str) → str[source]

Write the model to path as a compressed .npz.

The path is what enable_illumination_correction() puts in the environment, and what each worker process loads: the model has to be on disk for a spawn worker to be able to see it at all.

Parameters:

path – destination file. Parent folders are created.

Returns:

the absolute path written.

property per_plate: bool[source]

Whether the model holds one field per plate rather than one field.

class spacr.illumination.PreparedIllumination[source]

One fitted/loaded model and the stage-neutral objects derived from it.

The saved model is deliberately not tagged measurement or segmentation: both stages may reuse the same optical estimate. The stage that applies it owns that provenance separately.

Parameters:
  • model – loaded or newly estimated illumination model.

  • corrector – corrector configured with the requested missing-plate policy, but not registered as a Measure preprocessing hook.

  • model_path – absolute path of the saved model.

  • model_sha256 – digest of the exact saved bytes at model_path.

  • qc_artifacts – QC figure paths written while preparing the model.

class spacr.illumination.SegmentationIlluminationSession(prepared: PreparedIllumination, *, provenance_path: str, pipeline_style: str, resume: bool = False)[source]

Apply one illumination model exactly once per segmentation field.

Correction always receives a private copy. correct() records that an in-memory input was corrected; mark_completed() is deliberately separate and is the only operation that persists a field id. A pipeline therefore marks a field only after its durable NPZ/mask output exists.

Parameters:
  • prepared – model/corrector returned by prepare_illumination_model().

  • provenance_path – destination segmentation_application.json.

  • pipeline_style – 'v1' or 'v2' for the audit record.

  • resume – restore explicitly completed fields from an existing compatible record without rewriting it; absence is an error. False starts a fresh regenerated-output session and atomically replaces any old completion claim with an explicit prepared record.

Open a segmentation-input correction session and stamp its provenance.

Parameters:
  • prepared – the validated illumination model and its QC artefacts.

  • provenance_path – where the application record is written.

  • pipeline_style – which segmentation pipeline this corrects for.

  • resume – continue a previous session, reading back which fields it already completed. Without it a fresh record is written stating that no field has yet been corrected – a new preprocessing run invalidates any earlier completion claim, and saying so explicitly is what stops a half-finished run being read as a finished one.

correct(field_id: str, channel_arrays: numpy.ndarray, context) → numpy.ndarray[source]

Correct a private copy of one raw field, refusing a second pass.

Parameters:
  • field_id – identifier of the raw field, compared as a string; a field already corrected or marked complete raises IlluminationError.

  • channel_arrays – raw field intensities, (Y, X, C) or (Z, Y, X, C); a copy is corrected and the input is left unchanged.

  • context – the spacr.measure_hooks.PreprocessingContext passed to the prepared corrector; its file name selects the plate’s model.

finish(expected_fields: Iterable[str]) → None[source]

Mark the journal stage done after every expected field is durable.

Parameters:

expected_fields – identifiers of every field the run should have completed; the set, compared as strings, must equal the completed set exactly or IlluminationError is raised.

mark_completed(field_id: str) → bool[source]

Persist field_id after its corrected pipeline output is durable.

Parameters:

field_id – identifier of a field already passed to correct(), compared as a string; an uncorrected field raises IlluminationError.

Returns:

True when the record changed, False when the same completed field was marked again.

property applied_fields: Tuple[str, ...][source]

Field ids corrected during this process, in stable order.

property completed_fields: Tuple[str, ...][source]

Durably completed field ids in stable order.

spacr.illumination.disable_illumination_correction() → bool[source]

Turn the correction off, here and for any worker started afterwards.

Unregisters the hook and removes this module’s entry from SPACR_MEASURE_HOOKS – leaving any other extension’s entries alone – plus the two model variables.

Returns:

True if a correction was registered and has been removed.

spacr.illumination.enable_illumination_correction(model, *, path: str | None = None, on_missing: str = 'error', verbose: bool = True) → str[source]

Turn the correction on, here and in every worker process.

Three things happen, and the third is the one that matters:

  1. the model is saved to disk if it is not already there – a spawn worker can only reach it through the file system;

  2. MODEL_ENV_VAR and ON_MISSING_ENV_VAR are set;

  3. spacr.illumination:install is appended to SPACR_MEASURE_HOOKS (appended, not assigned – another extension may already be in there), and the registry is then consulted so that this process installs the hook through that same environment route.

Parameters:
  • model – an IlluminationModel, or the path to a saved one.

  • path – where to save model when it is not already a path. Defaults to <first src>/../illumination/illumination_model.npz.

  • on_missing – 'error' or 'skip'; see IlluminationCorrector.

  • verbose – print what was enabled and whether workers will see it.

Returns:

the registry key the hook is registered under.

Raises:

IlluminationError – when the model cannot be saved or loaded.

spacr.illumination.estimate_illumination(src, channels: Sequence[int], *, per_plate: bool = True, estimator: str = 'polynomial', degree: int = 4, max_fields: int = 50, grid: int = 256, dark: float = 0.0, verbose: bool = True) → IlluminationModel[source]

Estimate the illumination field from the merged fields in src.

Retrospective: the data corrects itself. See the module docstring for what is estimated and why.

Parameters:
  • src – the merged field folder measure_crop reads, or a list of them (settings['src'] accepts both).

  • channels – merged-stack channel indices to estimate, i.e. settings['channels']. One field is estimated per channel: two fluorophores go through different filters and vignette differently.

  • per_plate – one field per plate (default) or one for everything. Per plate is the default because illumination differs between acquisition sessions – lamp age, a re-seated filter cube, a different objective – and pooling two sessions estimates neither.

  • estimator – 'polynomial' (default) or 'smooth'.

  • degree – polynomial degree. 4 gives 15 terms: enough for a lamp profile plus a vignette and a tilt, far too few to fit a cell.

  • max_fields – fields per plate to estimate from, sampled evenly across the sorted file list. 50 is well past the point where the across-field median stops moving, and bounds the memory.

  • grid – the per-pixel median is computed on a binned grid whose long side is at most this. Illumination is low-frequency, so binning loses nothing, quiets the photon noise and is what makes 50 fields fit in memory. The fitted surface is returned at full resolution.

  • dark – additive camera offset subtracted before dividing, in raw counts. Not estimated – see the module docstring.

  • verbose – print one line per estimated field.

Returns:

an IlluminationModel.

Raises:

IlluminationError – when src holds no merged fields, or a field is unreadable in a way that would make the estimate meaningless.

spacr.illumination.illumination_qc(model: IlluminationModel, src, *, channels: Sequence[int] | None = None, save_dir: str | None = None, max_fields: int = 25, verbose: bool = True, stage: str | None = None) → Dict[str, Any][source]

Show that the correction worked, and say by how much.

Three things, per plate and per channel:

  • the estimated field as an image, so a lamp profile that is really a dirty objective is visible rather than inferred;

  • the position-versus-intensity trend before and after, measured on the same fields the estimate came from – the curve that should be flat after correction and is not before it;

  • a number: the residual slope of intensity against distance from the centre of the field, before and after, and the percentage of that bias the correction removed.

Parameters:
  • model – the estimated model.

  • src – the merged field folder(s) to measure the trend on.

  • channels – channels to report; defaults to the model’s own.

  • save_dir – where the figure goes. Defaults to <first src>/../illumination. Pass '' to skip the figure and compute only the numbers.

  • max_fields – fields per plate to measure the trend over.

  • verbose – print the per-channel summary.

  • stage – optional consumer label such as 'segmentation_input'. When supplied it appears in the figure title and filename, preventing segmentation and measurement QC artifacts from being confused.

Returns:

{plate: {channel: {...metrics...}}} with, per channel, slope_before, slope_after, bias_removed_pct, nonuniformity_pct, gain_min, gain_max and n_fields; plus, when a figure was written, one extra key '_figures' mapping each plate to the path of its PNG.

spacr.illumination.illumination_settings(settings=None)[source]

Defaults for illumination correction. Registered through the seam.

Parameters:

settings – values to seed, exactly like every set_default_* in spacr.settings.

Returns:

the settings dict with the illumination defaults applied.

spacr.illumination.install() → str[source]

Install the correction in this process from the environment.

This is the zero-argument installer SPACR_MEASURE_HOOKS names, and the only route that survives a spawn / forkserver worker: the worker is a fresh interpreter, so it imports this module and calls this function for itself, reading the model from MODEL_ENV_VAR.

Returns:

the registry key the hook was registered under.

Raises:

IlluminationError – if the environment does not name a readable model. Refusing loudly is the point – a worker that cannot load the model must not go on to measure uncorrected pixels.

spacr.illumination.load_illumination_model(path: str) → IlluminationModel[source]

Read a saved model. Thin alias for IlluminationModel.load().

Parameters:

path – the .npz written by IlluminationModel.save().

spacr.illumination.load_segmentation_illumination_resume(settings: Mapping[str, Any], *, provenance_path: str, pipeline_style: str, expected_fields: Iterable[str], verbose: bool | None = None) → PreparedIllumination[source]

Read and validate prior segmentation illumination without side effects.

This is the preprocess=False entry point. The application record is authoritative: its exact model path is loaded and its digest, metadata, pipeline style, and completed-field set are checked before the caller may trust existing normalised mask NPZ files. No model is fitted, no QC or application record is written, no pixels are corrected, and no Measure hook is installed.

Parameters:
  • settings – run settings; illumination_correction must be true, and illumination_model (when set), illumination_on_missing and verbose are read.

  • provenance_path – path of the prior segmentation application record (JSON); its model_path is resolved relative to this file’s folder when not absolute.

  • pipeline_style – segmentation pipeline style, 'v1' or 'v2' (case-insensitive); any other value raises IlluminationError.

  • expected_fields – identifiers of the mask fields already on disk; the record’s completed-field set must equal them exactly.

spacr.illumination.plate_of_field(file_name: str) → str[source]

The plate a merged field belongs to, from its file name.

spaCR names merged fields <plateID>_<wellID>_<fieldID>.npy (see spacr.io), so the plate is the first underscore-separated token. A name with no underscore is its own plate, which keeps a hand-assembled folder working instead of silently pooling it.

Parameters:

file_name – file name or stem, with or without directories.

spacr.illumination.position_intensity_slope(intensities: Sequence[float], coordinates: numpy.ndarray, shape: Tuple[int, int]) → float[source]

Least-squares slope of intensity against distance from the field centre.

This is the number illumination correction exists to drive to zero, and the same statistic is used on pixels (the QC images) and on objects (the scientific claim: the same cell must not measure brighter in the middle of the field).

Intensities are divided by their own mean and the radius is normalised so that 0 is the centre of the field and 1 is a corner, so the slope reads as the fraction of the mean intensity gained or lost between the centre of the field and its corner. -0.30 means a corner object measures 30 % of the mean below a central one.

Parameters:
  • intensities – one value per position.

  • coordinates – (N, 2) array of (row, col) positions in pixels.

  • shape – (Y, X) shape of the field the positions live in.

Returns:

the slope, or 0.0 when there is nothing to fit.

spacr.illumination.prepare_illumination_correction(settings: Mapping[str, Any], *, verbose: bool | None = None)[source]

Estimate, save, enable and QC the Measure correction.

The one call a pipeline makes before measure_crop, and the one the Illumination button on the Measure masthead runs on its own – the model and its QC figures in minutes, before committing hours to the measure run that will reuse them through illumination_model.

It does nothing at all – and returns None – unless settings['illumination_correction'] is True, which is NOT the shipped default: the correction is opt-in, so a run that never mentions it is measured uncorrected. Being asked to run with the switch off is a no-op worth hearing about rather than a silent one, because from a settings form it looks exactly like a Run button that does nothing, so a verbose call says which switch was not thrown.

Parameters:
  • settings – a measure_crop settings dict. Reads illumination_correction, illumination_model, illumination_estimator, illumination_degree, illumination_per_plate, illumination_max_fields, illumination_dark, illumination_on_missing, illumination_qc, illumination_vendor_profile, illumination_vendor_channel_map, plus src and channels.

  • verbose – overrides settings['verbose'].

Returns:

the IlluminationModel that was enabled, or None.

spacr.illumination.prepare_illumination_model(settings: Mapping[str, Any], *, src=None, channels: Sequence[int] | None = None, qc_stage: str | None = None, verbose: bool | None = None) → PreparedIllumination | None[source]

Prepare one reusable optical model without installing a Measure hook.

This is the direct consumer of the illumination_* settings. It estimates, loads or reads a vendor flat-field profile into the model once, ensures a fitted model is saved, hashes the exact saved bytes, optionally writes stage-labelled QC, and builds a corrector. Applying that corrector belongs to the caller’s stage.

Parameters:
  • settings – settings carrying the illumination controls. A non-empty illumination_vendor_profile replaces the estimate with the vendor’s own flat field; illumination_vendor_channel_map optionally assigns persisted channels to vendor channels/planes. illumination_model still wins over both.

  • src – optional raw field folder override. Defaults to settings['src'].

  • channels – optional persisted intensity-axis positions. Defaults to settings['channels'].

  • qc_stage – optional stage label included in QC filenames/titles.

  • verbose – override settings['verbose'].

Returns:

a prepared model/corrector, or None when correction is off.

spacr.illumination.prepare_segmentation_illumination(settings: Mapping[str, Any], *, src=None, channels: Sequence[int] | None = None, pipeline_style: str, verbose: bool | None = None) → SegmentationIlluminationSession | None[source]

Prepare correction for segmentation inputs without changing raw data.

The returned session is not a Measure hook. V1/V2 adapters hand it raw field copies, then explicitly mark fields complete after their durable segmentation output exists. The application record is stored beside the current run’s source folder, even when the optical model is shared from an external path, so two runs cannot overwrite one another’s completion set.

Parameters:
  • settings – run settings carrying the illumination controls read by prepare_illumination_model(), and src when src is not given.

  • pipeline_style – segmentation pipeline style, 'v1' or 'v2' (case-insensitive); any other value raises IlluminationError.

spacr.illumination.register_illumination_settings(replace: bool = False) → bool[source]

Register the illumination settings through the defaults seam.

Called once at import. Uses spacr.settings.register_defaults() rather than appending to spacr/settings.py, so this module owns its own knobs.

Types, tooltips and the module description are contributed; categories deliberately are not. spacr.settings.categories is one shared, ordered map that every settings panel walks, and its growth is guarded by an exact-equality test against a hand-kept list. A key contributed at import time is in that map only in a session that imported this module, so contributing categories would make that test’s result depend on which files pytest was pointed at.

The illumination keys ARE filed under a heading – “Illumination Correction” in spacr.settings.categories – and Measure’s panel offers every one of them, because measure_crop calls prepare_illumination_correction() itself and these are the keys that call reads. The heading is written in that map by hand for the reason above: it has to exist for every process that groups the Measure settings, not only for one that happened to import this module.

Parameters:

replace – re-register over an existing registration.

Returns:

True if it registered, False if it was already registered.

spacr.illumination.validate_measurement_illumination_inputs(settings: Mapping[str, Any], *, src=None) → Dict[str, Dict[str, Any]][source]

Fail closed before Measure corrects pixels a second time.

Segmentation is allowed to correct only its private model input. If a mask run ever records that it instead changed the persisted intensities, Measure must not install another gain over those pixels: doing so squares the optical field while producing entirely plausible numbers. A missing application record is the legacy/raw case and remains valid; a present record must prove the current segmentation-input-only contract.

This check is deliberately read-only. It neither creates an illumination folder nor repairs a malformed record, and it does nothing when Measure’s own illumination correction is off.

Parameters:
  • settings – resolved Measure settings.

  • src – optional merged-folder override; defaults to settings['src'] and accepts the same folder-or-list shape.

Returns:

absolute application-record paths mapped to their validated JSON objects; an empty dict means no segmentation record was present.

Raises:

IlluminationError – when a present record is unreadable or cannot prove that persisted intensity pixels remain raw.

spacr.illumination.validate_segmentation_illumination_resume(prepared: PreparedIllumination, *, provenance_path: str, pipeline_style: str, expected_fields: Iterable[str]) → Dict[str, Any][source]

Validate a preprocess=False mask resume without writing anything.

Normalised mask NPZ files cannot prove which intensity state Cellpose saw. A bypassed preprocessing stage therefore proceeds only when a prior application record names the same model bytes and pipeline style and covers exactly the fields already on disk. This function never fits a model, creates a record, corrects pixels, or updates the run journal.

Parameters:
  • prepared – the loaded model and corrector whose model path and digest the record must name.

  • provenance_path – path of the prior segmentation application record (JSON) to read.

  • pipeline_style – segmentation pipeline style the record must match, 'v1' or 'v2' (case-insensitive).

  • expected_fields – identifiers of the mask fields already on disk; the record’s completed-field set, compared as strings, must equal them exactly.

Returns:

the validated existing application record.

Raises:

IlluminationError – for an absent or incompatible record, model, pipeline style, or completed-field set.

spacr.illumination.worker_delivery_status(start_method: str | None = None) → Tuple[bool, str][source]

Whether the correction will actually reach measure_crop’s workers.

The failure this answers is silent by construction: a hook registered only in the parent process is a no-op in every spawn worker, the run completes, and the numbers are uncorrected while the user believes they are not.

Parameters:

start_method – the pool start method to judge against. Defaults to whatever SPACR_START_METHOD selects, falling back to the platform default – i.e. what spacr.measure.measure_crop() will use.

Returns:

(ok, message). ok is False whenever a field could be measured uncorrected without anything saying so.

Nested helpers

_lenient_profile_json._value(match)

Quote a bare profile value while preserving JSON booleans and null.

spacr/illumination.py:884