spacr.measure

Workflow inputs and outputs

Measure

Measure reads images and label planes together. Enable crop saving if you need PNG files; keep the database and source project together for streamed crops.

Open: Home → Measure.

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

Inputs

  • Images and label masks — merged/*.npy in the project; channels and integer label planes share each field array.

  • Label masks — masks/ when retained, or explicitly saved image/mask pairs. Intermediate masks may be removed by cleanup.

Outputs

  • Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route: cell, nucleus, pathogen, cytoplasm. Relevant columns, depending on the route: plateID, rowID, columnID, fieldID.

  • Object crops — data/**/*_png when save_png is enabled; png_list indexes saved crops. Supported workflows can instead stream crops from merged arrays and masks. Relevant tables, depending on the route: png_list. Relevant columns, depending on the route: png_path, prcfo.

Before this module

  • Mask: Use the same project and the correct image/mask channel indices.

  • Make Masks: Use Organize for Measure to merge images and their masks into the arrays Measure reads; standalone masks are not merged arrays.

  • External Masks: Re-measure only when needed; External Masks can already perform measurement.

  • Timelapse: Use the time-series project with stable frame/object identities.

  • Import: Import matching images and external integer masks to build merged project arrays, then open Measure on that project. Skip this step when compatible measurements have already been imported or computed. Do not append duplicate measurements to an existing imported table.

After this module

  • Annotate: Save or stream crops with stable object identities.

  • Classify: Choose the image or tabular family to match your input.

  • Image UMAP: Choose feature columns and inspect representative crops.

  • Embeddings: Retain encoder and channel-policy provenance.

  • Gate Editor: Use the actual measured feature definitions and units.

  • Graph Builder: Choose explicit variables, groups and filters.

  • QC: Review available checks and missing evidence.

  • Recruitment: Require the intended compartment intensities and identities.

  • Invasion Assay: Require two-colour stain measurements and appropriate baseline controls.

  • Replication Assay: Require explicit parasite-to-vacuole identities.

  • Motility Assay: Combine measured objects with matching tracks.

  • Feature Explorer: Define the class comparison and inspect filtering.

  • Database Browser: Inspect actual tables before exporting.

  • AnnData Export: Export compatible feature and metadata columns.

  • Dose–Response: Join the measured response to explicit doses and controls.

  • Endodyogeny size proxy: Supply the measured project roots and required object/png_list tables. Verify host-cell aggregation and area units before interpreting size bins; the Mask counts database alone is insufficient.

  • Host–Pathogen Analysis: Keep uninfected cells in Measure. Supply whole-vacuole masks, host reference intensities and optional explicit parasite-to-vacuole links; host identity alone does not define a vacuole.

API reference.

Module tutorial.

Turn masks and channels into one row per object, in a database.

WHAT IT IS FOR. Segmentation says WHERE the objects are; this module says what they are LIKE. It reads the arrays Mask wrote and produces the table every downstream question is asked of – which genes changed a phenotype, which cells to train a classifier on, which wells to believe.

WHAT IT NEEDS. A merged/ folder written by spacr.core.preprocess_generate_masks(): the intensity channels and the label masks for one field, saved together as .npy. Which masks to measure is named per object – cell_mask_dim, nucleus_mask_dim, pathogen_mask_dim and the organelle slots – and an object with no mask dimension is simply not measured, rather than measured as empty.

WHAT IT PRODUCES.

  • measurements/measurements.db, one SQLite table per object type, one row per object, keyed by the plate/row/column/field/object identity spacr.schema composes. The columns are shape, intensity, texture and SPATIAL features – how many neighbours an object has within a radius, how far the nearest one is, what fraction of its border touches another.

  • Optionally, one PNG per object (save_png), cropped by the mask. Those crops are what spacr.deep_spacr.deep_spacr() trains on and what Annotate shows, which is why the cropping lives here rather than beside the classifier: they must be cut by the same mask the measurements came from.

WHAT TO DO NEXT. Annotate or Classify, if the crops were written; Regression, if the question is which perturbation moved which measurement. Both read the database this writes and neither re-measures anything.


THREE THINGS THAT ARE NOT OBVIOUS AND ARE LOAD-BEARING:

A FIELD THAT FAILS TO MEASURE IS RECORDED, SUMMARISED AND STAMPED INTO THE DATABASE. Silence would let a regression analyse 344 of 384 wells and report a result with no sign that forty are missing, which is the failure this module is most careful about – the same reason its 3-D path refuses a volume it cannot measure correctly instead of measuring it wrongly.

THE 2-D PATH IS BIT-IDENTICAL AND DELIBERATELY SO. Mask can emit (Z, Y, X) label volumes now (see spacr.zstack), and everything about voxel spacing, volume columns and the units stamp exists so that a 3-D field is measured in real units or refused. A 2-D field takes exactly the code it took before, with spacing=None; a screen measured last year and re-measured today produces the same numbers.

THE RADIUS IS IN THE COLUMN NAME. neighbors_within_30 is a different column from neighbors_within_50, following the same precedent as homogeneity_distance_<d>, so two plates measured at different radii will not silently concatenate into one frame that means two things.

Illumination correction and a user-drawn ROI reach this module through the registries in spacr.measure_hooks rather than by editing it. Both are empty by default and both entry points return their input unchanged when they are, so an ordinary run is byte-identical to one from before they existed.

Exceptions

ManagerStartError

Raised when Measure cannot start its multiprocessing manager.

Classes

FieldRow

One row of the FEATURES table: one field, and the files that make it.

FieldTable

Rows are fields, columns are channels and mask types.

TableAssignment

What one regex did to one set of dropped files.

Functions

assign_paths_by_regex(paths, pattern, *[, table, plate])

Sort dropped files into rows and channel/mask columns with one regex.

crop_objects_from_array(data, mask_dim[, channels, ...])

Crop every object out of an in-memory merged image+mask array.

field_table_destination(table[, dst])

Where a run of table would write, given the destination it was handed.

field_table_settings(table[, settings, dst])

The measure_crop settings this table decides, over the ones it does not.

generate_cellpose_train_set(folders, dst[, min_objects])

Copy image/mask pairs from source folders into a Cellpose training set.

generate_object_dataset(src[, object_type, channels, ...])

Build an image dataset by cropping individual objects out of the merged

get_components(cell_mask, nucleus_mask, pathogen_mask)

Map each cell to its enclosed nucleus/pathogen labels via mask lookup.

get_object_counts(src)

Return per-count-type totals and per-file averages from the measurements DB.

img_list_to_grid(grid[, titles])

Plot a grid of images with optional titles.

mask_role_of(token)

Resolve what a user typed in a mask column to a spaCR object role.

measure_crop(settings)

Extract per-object morphology/intensity measurements and (optionally) cropped PNGs from mask stacks.

measure_from_field_table(table[, settings, dst, progress])

Measure a table of hand-picked images and masks. The FEATURES entry point.

process_meassure_crop_results(partial_results, settings)

Deprecated alias for process_measure_crop_results().

process_measure_crop_results(partial_results, settings)

Save and display figures carried by completed Measure jobs.

resolve_measurement_spacing(settings, ndim[, n_z])

Return (spacing, stamp) for a measurement of ndim spatial dimensions.

resolve_n_jobs(n_jobs[, cpu_count])

Return the number of worker processes measure_crop will actually use.

resolve_pool_size(n_jobs, n_files[, start_method])

Return the worker count for a set of image fields.

save_and_add_image_to_grid(png_channels, img_path, grid)

Add an image to a grid and save it as PNG.

spatial_column_names(radius)

Return the five spatial column names for radius, in emitted order.

write_field_table_project(table, dst)

Write the table out as the folders the Mask module leaves behind.

Module Contents

exception spacr.measure.ManagerStartError[source]

Bases: spacr.errors.ConfigurationError

Raised when Measure cannot start its multiprocessing manager.

The exception message reports the active start method, underlying error, and practical remedies. No fields are measured after this error.

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

class spacr.measure.FieldRow[source]

One row of the FEATURES table: one field, and the files that make it.

A row becomes exactly one merged/<stem>.npy, so it is also one field in the measurements database.

Variables:
  • label – what the row is called in the table’s first column, taken from the filenames. It is not the database identity; well and field are.

  • channels – channel index -> source path. The indices are positions on the merged array’s channel axis, counted from zero.

  • masks – role -> source path, for the roles this row supplies. The same mask file may appear in several rows, which is how one drawn mask is measured against several acquisitions.

  • well – the well id this field is filed under. Hand-drawn fields did not come from a plate, so they share one well by default and the table shows it rather than inventing a different one per row.

  • field – the field number within that well, unique per row.

stem(plate)[source]

The plate_well_field name this row is written and measured as.

Parameters:

plate – the plate name the whole table carries.

Returns:

the stem, which spacr.schema.parse_field_stem() reads back into the plate, row, column and field the database is keyed by.

class spacr.measure.FieldTable[source]

Rows are fields, columns are channels and mask types.

This is the thing the FEATURES window edits and the only input measure_from_field_table() needs. It is deliberately Qt-free: the window drives it, and the tests drive it without a window.

Variables:
  • rows – one FieldRow per field, in table order.

  • n_channels – how many channel columns the table has.

  • roles – which mask columns it has, in spacr.crops.MASK_PLANE_ORDER order – which is the order the planes are stacked in, so the two cannot drift.

  • plate – the plate name every row’s stem starts with. It names where the files came from rather than claiming a plate that was never run.

  • channel_tokens – which channel token owns which channel column, by position: channel_tokens[i] is the token that column i means. THE TABLE REMEMBERS THIS BECAUSE THE TABLE OUTLIVES THE DROP. The ranking that turns C1/C2 into columns 0 and 1 is a property of a SET of tokens, and a user fills this table one field at a time, so without a memory the second drop would rank its own files from scratch and put C2 in column 0 beside the first drop’s C1. Empty means no column means any particular token yet – every cell was filled by browsing rather than by the regex.

is_ready()[source]

Whether the table is complete enough to measure.

mask_dims()[source]

role -> plane index on the merged array this table would write.

The masks follow the channels with no gap, which is the only layout spacr.crops.read_merged_plane_layout() accepts – it recomputes the indices from the channel count and the order and refuses a manifest that disagrees.

ordered_roles()[source]

The mask columns in merged-plane order, duplicates removed.

problems()[source]

Everything that would stop this table being measured, as sentences.

Empty means measure_from_field_table() will run. The window shows these live, so a user never presses a Run button that is going to refuse.

class spacr.measure.TableAssignment[source]

What one regex did to one set of dropped files.

Variables:
  • table – the table the files were assigned into.

  • assigned – (path, row label, column caption) for every file that landed somewhere, in the order the paths were given.

  • unassigned – (path, reason) for every file that did not. The window lists these, because a file that silently vanishes is the one failure a drag-and-drop table cannot afford.

spacr.measure.assign_paths_by_regex(paths, pattern, *, table=None, plate=None)[source]

Sort dropped files into rows and channel/mask columns with one regex.

The regex is matched against each file’s BASENAME. What it captures decides where the file goes:

  • one of FIELD_TABLE_FIELD_GROUPS names the row. Files sharing a field token share a row, which is what makes a four-channel field one row rather than four.

  • one of FIELD_TABLE_MASK_GROUPS sends it to a mask column, through mask_role_of().

  • one of FIELD_TABLE_CHANNEL_GROUPS sends it to a channel column.

THE CHANNEL TOKEN IS RANKED, NOT READ AS A NUMBER, and that is the one decision here worth knowing about. C1/C2/C3 and w1/w2 and 0/1/2 all have to end up as channels 0, 1, 2, and there is no reading of C1 that is right for all three – a literal read makes the first set start at channel 1 and leaves channel 0 empty for ever. So the DISTINCT channel tokens are sorted (numerically on their trailing digits) and mapped onto 0, 1, 2 … in that order. The mapping is therefore a property of the set of files, not of any one of them, which is why the window shows the assignment rather than describing the rule.

THE SET IS THE TABLE’S, NOT THE DROP’S. table remembers which token owns which column in FieldTable.channel_tokens, and a later drop is ranked against the union of what it brings and what is already there. A token that ranks before one already placed renumbers the columns and MOVES the files already in them (_renumber_channels()), so every row agrees about what channel 0 is. Ranking each drop on its own instead would put a second field’s C2 in column 0 beside a first field’s C1, and the only sign of it would be in the database.

Parameters:
  • paths – file paths to assign.

  • pattern – a regex with at least a field group and one of a channel or mask group.

  • table – an existing table to add to. A new one is built when this is None; its channel count and mask columns come from what the files turn out to hold.

  • plate – the plate name for a new table.

Returns:

a TableAssignment. Nothing is read from disk and nothing is written.

Raises:

re.error – if pattern does not compile. The window catches this and shows it under the box rather than letting it reach a run.

spacr.measure.crop_objects_from_array(data, mask_dim, channels=(0, 1, 2), min_area=0, max_area=0, mask_background=True, normalize=True, percentiles=(1, 99), buffer=10, to_rgb=True, limit=None, size=None)[source]

Crop every object out of an in-memory merged image+mask array.

This is the no-database counterpart of generate_object_dataset(), used by the Measure live preview to show what the crops will look like before a run: it reads the object labels straight from a mask slice of a single merged .npy and returns the cropped, normalised images.

Parameters:
  • data – merged array (H, W, C) — image channels then mask slices.

  • mask_dim – slice index of the object-class mask to crop by.

  • channels – image channel indices to assemble (order = RGB order).

  • min_area – smallest object area (px) to keep; 0 = no lower bound.

  • max_area – largest object area (px) to keep; 0 = no upper bound.

  • mask_background – zero pixels outside the object.

  • normalize – per-channel percentile-normalise each crop.

  • percentiles – (low, high) for normalisation.

  • buffer – padding (px) around each object’s bounding box.

  • to_rgb – assemble the chosen channels into an HxWx3 uint8 image (1→grey→RGB, 2→padded, 3→RGB, >3→first three); else keep N channels in the merged array’s own dtype.

  • limit – cap the number of objects returned.

  • size – (width, height) to resize every crop to, or None to return each object’s own bounding box. This is measure_crop’s png_size, resized THE WAY THE RUN RESIZES – the same PIL.Image.resize call at its default resampling – because this feeds the Measure preview, whose purpose is to show what a run will write. Without it the preview showed bounding boxes while the run wrote squares, and the crop-size setting looked like it did nothing.

Returns:

list of {'label', 'area', 'bbox', 'crop'} dicts, largest objects first.

Note

to_rgb=True is the one place this function leaves the working dtype, because a GUI image is 8-bit. It narrows with _crop_to_uint8() (a rescale off the dtype range), not with a clip at 255 – a clip made every pixel of an unnormalised 16-bit object come back as pure white, so the preview showed a white blob and the run it was previewing did not.

spacr.measure.field_table_destination(table, dst=None)[source]

Where a run of table would write, given the destination it was handed.

ONE ANSWER, so that the window and the run cannot disagree about it. src is one of FIELD_TABLE_DECIDED_KEYS, so the FEATURES window shows it filled in and disabled; before this existed the window derived it only when it had been given a destination, and a window opened without one showed the settings spec’s path placeholder while the run wrote beside the first channel file. A disabled box captioned “the table decides this” that names the wrong folder is worse than no box at all – it is the window telling the user where their results are not.

Parameters:
  • table – the FieldTable the run would measure.

  • dst – the destination the caller was given, or None to derive one from the table.

Returns:

the project root, or None when the table is too empty to derive one. Nothing is read from disk.

spacr.measure.field_table_settings(table, settings=None, dst=None)[source]

The measure_crop settings this table decides, over the ones it does not.

Everything the table can answer is answered from the table: the channel list, the mask plane of every object it supplies, the crop modes that are possible, the PNG channels, and src. Every other key is the user’s, taken from settings and defaulted by spacr.settings.get_measure_crop_settings() exactly as the Measure module defaults them – so the FEATURES window and the Measure module disagree about nothing.

A role the table does NOT supply is set to None rather than left out, which is how measure_crop is told not to measure it.

Parameters:
  • table – the FieldTable the user filled in.

  • settings – the user’s answers from the settings panel.

  • dst – the project root the run will write. src is its merged folder, which is where measure_crop reads fields from.

Returns:

a new settings dict. Nothing is read from disk.

spacr.measure.generate_cellpose_train_set(folders, dst, min_objects=5)[source]

Copy image/mask pairs from source folders into a Cellpose training set.

Only pairs whose mask contains at least min_objects labeled objects (background label 0 excluded) are copied. Files are renamed with their source folder name as prefix to avoid collisions.

Parameters:
  • folders – Iterable of source folders, each containing a masks/ subfolder and the raw images alongside it.

  • dst – Destination folder; imgs/ and masks/ subfolders are created if missing.

  • min_objects – Minimum number of unique object labels required in a mask for the pair to be included. Default 5.

Returns:

The finalized spacr.errors.RunLedger. Unreadable masks and failed copies are recorded on it and summarised loudly at the end, so a training set that is quietly short of pairs announces itself.

spacr.measure.generate_object_dataset(src, object_type='cell', channels=(0, 1, 2), min_area=None, max_area=None, columns=None, rows=None, fields=None, plates=None, where=None, criteria=None, output_dir=None, png_size=(128, 128), mask_background=True, normalize=True, percentiles=(1, 99), buffer=10, mask_dims=None, save_png=True, return_arrays=False, limit=None, db_path=None, verbose=True)[source]

Build an image dataset by cropping individual objects out of the merged image+mask arrays, selected by measurement and/or metadata criteria.

spaCR’s merged/ arrays store the image channels first and then one integer label-mask slice per object class (cell, nucleus, pathogen, organelle). The measurements database records, for every object, its integer object_label, the merged .npy it came from (path_name), its well/field metadata, and its features (e.g. cell_area). This function queries that database for the objects you want, then for each hit slices the object out of its array using object_label and the class mask, assembles the channels you ask for into an image, and saves a PNG.

Example — an RGB dataset from image channels 0, 2, 4 for cells larger than 10000 px² in columns 1 and 2:

generate_object_dataset(
    "/data/plate1", object_type="cell",
    channels=(0, 2, 4), min_area=10000, columns=[1, 2])
Parameters:
  • src – experiment root (the folder that holds merged/ and measurements/measurements.db), or the merged folder itself.

  • object_type – which object table + mask slice to crop. With the default mask_dims the accepted values are 'cell', 'nucleus', 'pathogen' and 'organelle'; any other value ('cytoplasm' included) raises ValueError unless mask_dims names its slice explicitly.

  • channels – image channel indices to include, in output order. Three indices → an RGB image; one → greyscale; two → padded to RGB; more than three → kept as an .npy array (and the first three saved as a PNG preview when save_png).

  • min_area – keep only objects with {object_type}_area > this. In a database measured from 2-D fields that column is a px^2 area; in one measured from 3-D volumes it is a volume, in voxels or um^3 according to the row’s measurement_units. This function crops 2-D arrays only and refuses a volumetric one, so in practice the threshold is always px^2 here – but read the stamp before carrying a number between databases.

  • max_area – keep only objects with {object_type}_area < this.

  • columns – list of plate column numbers to include (matched against columnID as 'c<N>'). rows / fields / plates behave the same for rowID ('r<N>') / fieldID ('f<N>') / plateID (raw token).

  • where – raw SQL boolean fragment ANDed onto the query, for anything the shortcuts don’t cover (e.g. "cell_eccentricity < 0.8").

  • criteria – dict of {column: (op, value)} ANDed onto the query, e.g. {"cell_area": (">", 10000), "columnID": ("in", ["c1", "c2"])}.

  • output_dir – where PNGs (and any .npy for >3 channels) are written; defaults to <root>/object_dataset/<object_type>.

  • png_size – (width, height) the crop is resized to.

  • mask_background – zero out pixels outside the object (isolate it).

  • normalize – per-channel percentile-normalise before writing.

  • percentiles – (low, high) percentiles for normalisation.

  • buffer – pixels of padding around the object’s bounding box.

  • mask_dims – dict mapping object type → its mask slice index. Defaults to spaCR’s layout {cell:4, nucleus:5, pathogen:6, organelle:7} (four image channels). Override if your arrays have a different channel count.

  • save_png – write PNG files (set False to only collect arrays).

  • return_arrays – also return the cropped arrays in the manifest.

  • limit – cap the number of objects processed (handy for previews).

  • db_path – explicit path to measurements.db (else derived from src).

  • verbose – print a short progress summary.

Returns:

a manifest list[dict]; each entry has object_label, path_name, plateID/rowID/columnID/fieldID, png_path (if saved) and array (if return_arrays).

Note

The crop keeps the merged array’s dtype. A uint16 field gives uint16 crops, in the manifest and in the .npy written for more than three channels; normalize stretches into that dtype’s full range, not into 0-255. The single narrowing to 8 bit happens in _save_object_crop(), where PIL needs it, and it rescales (_crop_to_uint8()).

It used to cast to float32, normalise into 0-255 and then np.clip(crop, 0, 255).astype(np.uint8). With normalize=False that clip hit every 16-bit pixel brighter than 255 – i.e. the whole object – so the PNG written to disk was a solid white silhouette. The datasets built from it were trained on saturated images and nothing said so.

spacr.measure.get_components(cell_mask, nucleus_mask, pathogen_mask)[source]

Map each cell to its enclosed nucleus/pathogen labels via mask lookup.

Parameters:
  • cell_mask – Label mask of cells.

  • nucleus_mask – Label mask of nuclei.

  • pathogen_mask – Label mask of pathogens.

Returns:

Tuple (nucleus_df, pathogen_df) where each DataFrame has one row per (cell, child) pair with columns cell_id and either nucleus or pathogen.

spacr.measure.get_object_counts(src)[source]

Return per-count-type totals and per-file averages from the measurements DB.

Reads the object_counts table from <src>/measurements/measurements.db and aggregates by count_type.

Parameters:

src – Path to the run folder containing measurements/measurements.db.

Returns:

DataFrame with columns count_type, total_object_count, and avg_object_count_per_file_name.

spacr.measure.img_list_to_grid(grid, titles=None)[source]

Plot a grid of images with optional titles.

Parameters:
  • grid (list) – List of images to be plotted.

  • titles (list) – List of titles for the images.

Returns:

fig (Figure) – The matplotlib figure object containing the image grid.

spacr.measure.mask_role_of(token)[source]

Resolve what a user typed in a mask column to a spaCR object role.

Accepts the role’s own name, the plural and the common laboratory synonyms (nuclei, parasite, mito), and the numbered organelle spelling the settings forms use on screen – Organelle 2 is organelleb, because the slots are lettered internally and numbered for the reader.

Parameters:

token – what the regex captured or the user chose, in any case.

Returns:

a role from spacr.crops.MASK_PLANE_ORDER, or None when the token names no object spaCR can measure.

spacr.measure.measure_crop(settings)[source]

Extract per-object morphology/intensity measurements and (optionally) cropped PNGs from mask stacks.

Consumes the merged/ folder produced by spacr.core.preprocess_generate_masks() (channel arrays + mask stacks saved as .npy), computes shape, intensity, texture and spatial features per cell / nucleus / pathogen / cytoplasm object, and writes them to a SQLite measurements.db. When save_png is enabled it also crops per-object PNG thumbnails, which are the training input for spacr.deep_spacr.deep_spacr().

Parameters:

settings –

Settings dict, canonicalized via spacr.settings.get_measure_crop_settings(). Key entries the function reads:

  • src (str or list) — one or more …/merged folders. A cloud address Make Masks has analysed is measured from the local folder Make Masks staged it in; a cloud folder of merged stacks is mirrored under cloud_cache first. cloud_results copies the measurements folder back to cloud storage.

  • psf_measurement_source — original (default) uses the normal rescaled/preprocessed intensities; processed adds an explicitly calibrated PSF before quantitative features. The immutable kernel reaches every worker. Source images and exported crops stay unchanged. Field provenance is saved in intensity_rescale; incompatible existing PSF measurements are refused before any rows are appended.

  • cell_mask_dim / nucleus_mask_dim / pathogen_mask_dim — channel index of each mask stack; None disables that object type.

  • cell_min_size / nucleus_min_size / pathogen_min_size / cytoplasm_min_size — pixel-area cutoffs.

  • channels — list of intensity channels to measure.

  • crop_mode — list drawn from ['cell','nucleus','pathogen', 'cytoplasm']; each entry produces one PNG per object.

  • save_png — write per-object PNG thumbnails.

  • normalize — [lower_pct, upper_pct] for PNG normalization.

  • normalize_by — 'png' (per-crop) or 'fov' (per-field).

  • timelapse, timelapse_objects, n_jobs, test_mode.

  • database_write_queue_gib — optional SQLite queued-data RAM budget from zero to 64 GiB. Omit to use Preferences (default one GiB). Zero uses disk-only buffering; overflow lives under measurements/.write_queue. Each field commits atomically and unfinished write packets remain available after failure.

  • dry_run — validate the settings, report the plan and stop; the input folders are inspected but nothing is written.

Returns:

None on a normal run, which writes measurements/measurements.db, measure_crop_settings.csv, and (if save_png) PNGs into per-object subfolders under src. When dry_run is set, the list of spacr.validate.Problem returned by spacr.validate.run_preflight(), and nothing is written.

Raises:
  • ValueError – if src is not a string or a list of strings.

  • spacr.errors.ConfigurationError – only in strict mode (settings['strict_errors'], or the SPACR_STRICT_ERRORS environment variable). The normalize, normalize_by, mask-dimension/min-size and channels type checks otherwise print a WARNING and return None without measuring anything.

Example

from spacr.measure import measure_crop
settings = {
    'src': '/data/plate01/merged',
    'cell_mask_dim': 4, 'nucleus_mask_dim': 5, 'pathogen_mask_dim': 6,
    'channels': [0, 1, 2, 3],
    'crop_mode': ['cell'], 'save_png': True,
    'normalize': [1, 99], 'normalize_by': 'png',
}
measure_crop(settings)

See also

spacr.core.preprocess_generate_masks() — upstream mask generation. spacr.io.generate_dataset() — build a training set from the PNGs. spacr.deep_spacr.deep_spacr() — train a CNN on the crops.

spacr.measure.measure_from_field_table(table, settings=None, dst=None, progress=None)[source]

Measure a table of hand-picked images and masks. The FEATURES entry point.

The other way into this module. measure_crop() starts from a merged/ folder a pipeline already built; this starts from a table a user filled in by dropping files onto it, writes that folder, and then calls measure_crop() ITSELF – unchanged, with no flag saying where the fields came from. The database, the crops and the folder tree are therefore the Measure module’s, because they are made by it.

Parameters:
  • table – the FieldTable the FEATURES window edited.

  • settings – the user’s answers from the settings panel. The keys the table decides are overwritten from it – see field_table_settings().

  • dst – the project root to write. Defaults to a features folder beside the first channel file of the first row, which is where a user who dropped a folder in expects to find the results. See field_table_destination(), which is the one place that default is worked out.

  • progress – called with a sentence as each stage starts, or None. It runs on whatever thread this does – the FEATURES window runs this on a worker and its callback only emits a signal. Writing the arrays and measuring them are separate stages because on a large table the second takes minutes and the first does not.

Returns:

{'destination', 'db_path', 'settings', 'stems', 'merged'}. db_path is the measurements database whether or not it exists, so a caller can report the path it was asked for.

Raises:

spacr.errors.ConfigurationError – if the table is incomplete or a file breaks the array contract. Nothing is measured in that case.

Example

from spacr.measure import (
    assign_paths_by_regex, measure_from_field_table)

found = assign_paths_by_regex(
    paths,
    r'(?P<field>fov\d+)_(?:C(?P<channel>\d+)'
    r'|(?P<mask>cell|nucleus))')
measure_from_field_table(found.table, {'save_png': True})

See also

measure_crop() – the run this delegates to, unchanged. write_field_table_project() – the folders it writes first.

spacr.measure.process_meassure_crop_results(partial_results, settings)[source]

Deprecated alias for process_measure_crop_results().

The misspelled name remains available for existing scripts and will be removed in a future major release.

Parameters:
  • partial_results – Completed Measure job tuples, passed unchanged to process_measure_crop_results() after a DeprecationWarning.

  • settings – Resolved Measure settings, passed unchanged; src identifies the output root.

spacr.measure.process_measure_crop_results(partial_results, settings)[source]

Save and display figures carried by completed Measure jobs.

Parameters:
  • partial_results – Completed job tuples. None entries are skipped; each figure is written below <src>/../results/ and then closed.

  • settings – Resolved Measure settings. src identifies the output root.

spacr.measure.resolve_measurement_spacing(settings, ndim, n_z=1)[source]

Return (spacing, stamp) for a measurement of ndim spatial dimensions.

spacing is handed straight to skimage.measure.regionprops_table() and (as sampling) to scipy.ndimage.distance_transform_edt(). stamp is the dict of MEASUREMENT_STAMP_COLUMNS written onto every row so the units are recorded rather than inferred.

2-D returns (None, px stamp) unconditionally. Even when a voxel size is configured it is not applied, so a 2-D run is numerically identical to every spaCR run before this function existed.

3-D requires a z/xy relationship and will not invent one. With anisotropic voxels an unspaced volume is not merely in unusual units: a voxel count is not proportional to a physical volume, a distance transform measures a different length along z than along x, and major_axis_length mixes the two. This mirrors spacr.zstack.resolve_anisotropy(), which raises rather than defaulting to 1.0 because “isotropic” is a claim about the microscope, not a neutral value. Set voxel_size_z_um and voxel_size_xy_um (preferred – it also gives physical units), or set anisotropy alone (correct geometry, xy-pixel units).

Parameters:
  • settings – measure settings dict; reads voxel_size_z_um, voxel_size_xy_um and anisotropy.

  • ndim – 2 or 3.

  • n_z – number of z planes behind the measurement; 1 for a 2-D field.

Returns:

(spacing, stamp). spacing is None for 2-D, a (dz, dy, dx) tuple for 3-D.

Raises:
spacr.measure.resolve_n_jobs(n_jobs, cpu_count=None)[source]

Return the number of worker processes measure_crop will actually use.

None selects spaCR’s default. Explicit values are validated and capped at the available CPU count.

Parameters:
  • n_jobs – what the user asked for. None means “pick for me”.

  • cpu_count – core count to resolve against; defaults to multiprocessing.cpu_count().

Returns:

an int in [1, cpu_count].

Raises:

spacr.errors.ConfigurationError – n_jobs is zero, negative, or not an integer. A pool of zero workers measures nothing, and quietly turning it into some other number is how a run ends up not doing what it was told.

spacr.measure.resolve_pool_size(n_jobs, n_files, start_method=None)[source]

Return the worker count for a set of image fields.

spawn and forkserver start a fresh interpreter for every worker, so their worker count is capped at the number of fields. fork keeps the requested count for compatibility.

Parameters:
  • n_jobs – the resolved worker count from resolve_n_jobs().

  • n_files – how many fields there are to measure.

  • start_method – start method name to decide against; defaults to the interpreter’s current default.

Returns:

an int >= 1.

spacr.measure.save_and_add_image_to_grid(png_channels, img_path, grid, plot=False)[source]

Add an image to a grid and save it as PNG.

Parameters:
  • png_channels (ndarray) – The crop in file order – red plane first – as spacr.crops.build_png_channels() assembles it. Written without narrowing, so a uint16 crop becomes a 16-bit PNG; a float crop is silently written as 8-bit by cv2. Four or more channels raise rather than losing one to an alpha plane.

  • img_path (str) – Where the PNG goes. Its parent folder is stamped with the format sidecar and must already exist – the caller in _measure_crop_core creates it. If it does not, the stamp fails with a printed warning, cv2.imwrite returns False, and the call returns normally having written nothing at all. A bare filename with no directory part stamps the current working directory.

  • grid (list) – Anything with append; read only when plot is true, and appended to in place, so the return value is the object that was passed in. None passes through untouched while plot is false.

  • plot (bool) – Truthiness, not identity, decides. False (the default) leaves grid completely untouched – the PNG is still written – which is why an ordinary run ends with an empty grid. True appends the crop for img_list_to_grid(): a crop of exactly dtype uint16 is appended as a high-byte narrowed uint8 copy, every other dtype is appended unchanged, and a uint8 crop is appended by reference, so a caller that reuses its buffer mutates what is already in the grid.

Returns:

grid (list) – The same object that was passed in, with the crop appended only if plot was true.

Raises:
  • spacr.crops.CropError – png_channels has more than three channels.

  • AttributeError – grid is None (or has no append) and plot is true. The PNG has already been written by then.

  • cv2.error – img_path has no extension cv2 recognises. The folder sidecar has already been written by then.

Note

The file’s colour slots hold what the mapping declares. The caller assembles png_channels in file order — red plane first — with spacr.crops.build_png_channels() and spacr.crops.resolve_png_channel_mapping(); under the legacy settings['png_dims'] list that mapping is entry 0 blue, 1 green, 2 red, so png_dims[0] lands in the file’s BLUE slot.

That is the REVERSE of what png_dims reads like, and it is LEFT THAT WAY ON PURPOSE: every crop already on disk was written with this mapping, so flipping it would silently change what each colour means and invalidate the models trained on those crops. The mapping is declared rather than corrected.

cv2.imwrite interprets a 3-channel array as BGR, so spacr.crops.to_cv2_bgr() reverses the array once, here, and cv2’s interpretation lands the red plane in the file’s red slot. It refuses more than three channels rather than letting cv2 write the fourth as an alpha plane for every reader to drop in silence.

The format is versioned: spacr.crops.stamp_crop_folder() drops a .spacr_crop_format.json sidecar into the crop folder before the first PNG lands, recording format 3 (declared_rgb). An unmarked folder means format 1 (legacy), whose bytes match format 3 for the same declared mapping, so both are read as-is; only format 2, whose stored channel order is reversed, is corrected by spacr.crops.read_crop_png(), and spacr.crops.migrate_crop_folder rewrites such a folder in place.

Crops are still uint16, so these are 16-bit PNGs and no intensity is discarded at write time. The narrowing to 8 bit happens once, on read, in spacr.crops.narrow_to_uint8(), which always takes the HIGH BYTE (// 256) — replacing PIL’s two incompatible rules (high byte for an RGB PNG, a clip at 255 for a single-channel one, which returned solid white for any crop brighter than that).

spacr.measure.spatial_column_names(radius)[source]

Return the five spatial column names for radius, in emitted order.

The radius is baked into neighbors_within_<r> – the same precedent as homogeneity_distance_<d> and percentile_<p>. Two plates measured at different radii therefore produce different columns and will not concat.

Parameters:

radius – neighbourhood radius; truncated with int() for the neighbors_within_<r> name. The other four names do not depend on it.

Returns:

list of five column names.

spacr.measure.write_field_table_project(table, dst)[source]

Write the table out as the folders the Mask module leaves behind.

This is the whole of what the FEATURES button adds to Measure: it turns a table of hand-picked files into stack/, masks/ and merged/ exactly as spacr.core.preprocess_generate_masks() would have left them, down to the plane-layout manifest, so the run that follows is an ORDINARY measure run and not a second code path that has to be kept in step with this one.

Parameters:
Returns:

{'destination', 'merged', 'stack', 'masks', 'stems'}.

Raises:

spacr.errors.ConfigurationError – if the table is incomplete, or if any file breaks the uint16 array contract measure_crop reads. Nothing is written past the field that failed.

Nested helpers

_calculate_radial_distribution._calculate_average_intensity(distance_map, single_channel_image, num_bins, region_mask)

Calculate the average intensity of a single-channel image based on the distance map.

Only pixels inside region_mask (the cell) are binned. The previous version multiplied the distance map by the cell mask instead, which set every pixel outside the cell to distance 0 and dumped the whole field background into bin 0 — so rad_dist_..._bin_0 measured background, not the innermost shell, and inverted the meaning of the feature.

Parameters:
  • distance_map (numpy.ndarray) – Distance from the object boundary.

  • single_channel_image (numpy.ndarray) – The single-channel image.

  • num_bins (int) – The number of bins for the radial distribution.

  • region_mask (numpy.ndarray) – Boolean mask of the parent cell.

Returns:

numpy.ndarray – The radial distribution of average intensities. Bins with no pixels are NaN rather than a meaningless 0.

spacr/measure.py:2662

_cell_cycle_by_well._fractions(phases, prefix='')

Count and phase fractions of one group of nuclei.

spacr/measure.py:5432

_cellprofiler_tables.field_masks(stem)

{object type: label image} of one field, read once.

spacr/measure.py:10104

_cellprofiler_tables.lookup(role, image_numbers, xs, ys)

The spaCR label under each centre in role’s mask.

A centre outside the image cannot identify an edge object; the check comes before rounding so negative subpixel positions stay unmatched.

spacr/measure.py:10115

_commit_measure_packet.dispatch(operation)

Save one approved operation to the packet’s central database.

Parameters:

operation – approved helper name, positional arguments and keywords.

spacr/measure.py:8850

_confluency_phase_features.log_sd(window)

Log of the local standard deviation in window, in noise units.

spacr/measure.py:3825

_extended_regionprops_table._gini(array)

NaN-safe Gini coefficient of an intensity array.

spacr/measure.py:2047

_extended_regionprops_table._masked_intensity(region)

Pixels inside a region across old and new scikit-image names.

spacr/measure.py:2079

_morphological_measurements._all_masks()

Object type -> label image, for the masks this run actually has.

spacr/measure.py:1357

_morphological_measurements._props(mask)

regionprops_table + (3-D only) the explicitly-named volume columns.

spacr/measure.py:1329

_morphological_measurements._with_bystanders(frame, mask, pathogen_links)

Merge the bystander block onto the CELL props frame.

A cell is infected if it holds a pathogen, a bystander if it holds none but sits within the reach of one that does, and distal otherwise. Without the split the last two are the same row, and the uninfected control is a mixture whose variance hides the effect every infection comparison is looking for.

THE REACH IS DERIVED FROM THIS FIELD’S OWN CELLS, as a multiple of their median diameter, so it means the same thing at 20x and 63x.

Props on the LEFT, for the reason _with_spatial gives.

spacr/measure.py:1416

_morphological_measurements._with_distances(frame, name)

Merge the object-distance block onto a props frame.

Props on the LEFT for the reason _with_spatial gives: ‘label’ has to keep column position 0. A non-empty frame means name’s mask holds labels, so _all_masks already carries it.

spacr/measure.py:1366

_morphological_measurements._with_spatial(frame, mask)

Merge the spatial block onto a props frame. Props on the LEFT.

spacr/measure.py:1399

_phases_by_xgboost._model()

A fresh classifier with the fixed phase-calling parameters.

spacr/measure.py:5072

_pin_cupy_cudart_headers._pinned(libname, *args, **kwargs)

The pinned runtime header directory for cudart, else the finder’s answer.

spacr/measure.py:817

_save_viability_figures._keep(fig, name)

Save one figure under the viability results folder.

spacr/measure.py:8191

_stain_cut._share(threshold)

Share of the plate’s objects above threshold.

spacr/measure.py:7247

_torch_intensity_table.host(tensor)

Copy a tensor to a NumPy array.

spacr/measure.py:2365

_two_population_fit._mixture(v)

The fitted two-population density at v.

spacr/measure.py:7172

_two_population_fit._near(v)

How many values lie within reach of v.

spacr/measure.py:7180

_wound_axis.inside(offset)

Mark axis samples inside the image at the given normal offset.

spacr/measure.py:5905

_wound_closure_tables.planes(items=items)

Yield this field’s wound-analysis planes in time order.

spacr/measure.py:6966

_wound_fronts.running(front)

Running median of the good fronts, carried over rows with none.

spacr/measure.py:6121

assign_paths_by_regex.first(names)

The first of names the pattern actually defines.

spacr/measure.py:12059

generate_object_dataset._in(colname, values, prefix)

An IN (...) clause and its parameters, built safely.

Placeholders rather than interpolation: the values come from a settings file, and a formatted list is an injection waiting for a filename with a quote in it.

spacr/measure.py:11460

measure_crop.job_callback(result)

Save returned figures and report field computation progress.

Parameters:

result – index, average duration, surviving labels, figures, error text and an optional queued-write ticket. An integer zero labels value records the original field failure. SQLite success is recorded separately by the writer after committing all scientific rows. Other backends keep their existing synchronous worker verdict.

spacr/measure.py:9575

measure_crop.make_error_callback(job_file)

Bind the filename into the pool’s error callback.

apply_async hands the error callback only the exception, so the file has to be closed over. Without this hook a worker that died outright vanished entirely: the exception sat on an AsyncResult nobody read, and the run still printed “Successfully completed run”.

Parameters:

job_file – The .npy filename of the field, as it appears in files – a bare basename, not a path joined onto settings['src']. It is used unchanged as the ledger key and as the reported_files entry, so anything else silently loses the match against files in the finally sweep and the field is filed a second time as “field produced no result”.

Returns:

A one-argument callable suitable as the error_callback of Pool.apply_async; it takes the exception and returns None. Call it as make_error_callback(file)(exc) when raising the exception yourself, which is what the retry loop does on the last attempt – the ledger counts fields, not tries, so a field that failed twice and then worked must not be reported here at all.

spacr/measure.py:9599

measure_crop.make_error_callback._on_error(exc)

Record one worker’s failure against the file that caused it.

spacr/measure.py:9624

measure_crop.record_verdict(item, error=None, stage='measure')

Count each field once, after its actual final outcome.

spacr/measure.py:9560

measure_crop.write_callback(item, ticket, error)

Record a field only after its packet’s final SQL verdict.

spacr/measure.py:9571

measure_from_field_table.say(message)

Report a stage, if anyone asked to hear about them.

Guarded because the caller is a window that may be closed while this is still running: the FEATURES window’s callback emits a Qt signal, and a worker parked past its widget’s destruction raises RuntimeError from the emit. A run must not fail because nobody is listening to it any more.

spacr/measure.py:12426

resolve_measurement_spacing._positive(name)

One spacing value, refused unless it is a positive number.

A zero or negative spacing makes every physical measurement wrong by a factor nobody can recover afterwards, so it is refused rather than defaulted.

spacr/measure.py:458