spacr.image_import

Workflow inputs and outputs

Import Images

Review field identities, image channels and optional external masks before writing a separate project. Image-only imports still need segmentation before measurement.

Open: Import → Import Images.

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.

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

After this module

  • Mask: Use imported image planes and identities; image-only imports still need segmentation.

API reference.

Module tutorial.

Infer what the filenames in a folder of images encode, and import the folder.

A fixed list of filename conventions – cellvoyager, cq1, custom (a user-written regular expression), auto – covers few real layouts: of the ten acquisition layouts in tests/import_corpus.py, two parse with it and eight recover nothing. Opera Phenix and ImageXpress are among the eight.

All ten layouts encode the same six facts – plate, well, field, channel, z, t – and differ only in how. So rather than matching a whole filename against one of N templates, this module reads the parts of the name that vary across the folder and infers what each varying part means.

Variation across the folder identifies the axes. A token that is the same in every file carries no information: L01 in plate1_A01_T0001F001L01A01Z01C01.tif is a constant for the whole plate and identifies nothing. A token that takes two values across eight files is an axis with two positions. Only the folder, not a single filename, shows which tokens are axes.

Markers identify which axis a varying token is. F001 and C01 both vary and are not interchangeable; the letter in front names the axis, and every convention in the corpus uses one. The marker table below is that vocabulary, and it is short because the conventions mostly share their letters.

No role is assigned without evidence. A token that varies and carries no recognisable marker is reported as unplaced, with its values, so the caller can ask the user. A partly unparseable tree imports what it can and reports the rest, rather than returning a plausible but wrong answer; the older consolidate path instead lost images without an error.

Classes

ImportPlan

A proposed import, shown to the user for checking before anything is written.

ImportResult

Counts and per-file reasons describing what apply_import() wrote.

InferredLayout

Axes inferred from a folder's filenames, and the tokens left unresolved.

InsideFile

What one file's own metadata says about the axes it holds.

TokenSlot

One position in a filename, and what was seen there across the folder.

Functions

apply_import(→ ImportResult)

Write the spaCR project plan describes.

canonical_name(→ str)

The spaCR filename for one resolved image.

infer_layout() → InferredLayout)

Infer which axes the filenames under root encode.

load_plan(→ ImportPlan)

Rebuild a plan from a file written by save_plan().

plan_import(→ ImportPlan)

Propose an import of root. Writes nothing.

read_axes_inside(→ InsideFile)

Read the page count and the C, Z and T axis sizes declared in path's own metadata.

save_plan(→ pathlib.Path)

Write plan as JSON that load_plan() can read back.

tokenise(→ List[Tuple[str, str]])

Split a name into (kind, text) runs, kind "alpha" or "digit".

Module Contents

class spacr.image_import.ImportPlan[source]

A proposed import, shown to the user for checking before anything is written.

With metadata_type, the user cannot see how filenames were parsed until masks come out wrong; a plan makes the proposal visible and correctable before it is acted on. This mirrors spacr.foreign, whose column mapping is shown on its screen and is editable in place.

Nothing here touches the destination. problems() lists what would stop an import; with_mapping() returns a new plan, resolved in memory, so an edit is applied immediately and can be discarded.

Parameters:
  • layout – what the names gave.

  • inside – per relative path, what the file’s own metadata gave.

  • mapping – user answers for axes inference could not name, as {token position: {value: index}} – e.g. {0: {"DAPI": 1, "GFP": 2}} for a tree with dye folders.

columns() → List[str][source]

file then every axis anything resolved, counts last.

Only axes present in at least one file get a column. A column of empty cells for an axis no file carries would read as “spaCR looked for a Z and found none”, which differs from “this acquisition has no Z”. The tiled tree gains a tile column and the flat OME one gains c_count; neither shows the other’s.

counts() → Dict[str, int][source]

Return the number of distinct values per axis.

A plate with the wrong number of wells or channels shows up here before anything is imported.

problems() → List[str][source]

Return every problem that would make this import wrong, as sentences.

Problems are returned, not raised, and all of them at once, so the user can fix them in one pass rather than one at a time.

rows(limit: int | None = None) → List[List[str]][source]

One row of strings per file, cells aligned to columns().

The Qt model draws these rows into a widget and table() pads them into a monospace block, so the GUI and the text output show the same columns and values.

table(limit: int = 8) → str[source]

Return the proposal as a plain-text table, followed by axis counts and problems.

Example filenames are shown beside the fields they were parsed into, because a wrong assignment (field and channel swapped, say) is only visible next to the name it came from.

with_mapping(mapping: Dict[int, Dict[str, int]]) → ImportPlan[source]

Return a new plan with mapping merged in. Nothing on disk is touched.

Parameters:

mapping – further answers as {token position: {value: index}}; for each position they are added to, and override, the answers this plan already holds.

property files: Dict[str, Dict[str, object]][source]

Relative path -> every axis known, from all three sources.

Names first, then the file’s own metadata (as <axis>_count entries), then the user’s mapping, which sets channel – in that order because each is more specific than the last.

property root: pathlib.Path[source]

The folder every path in this plan is relative to.

Returns:

the root path.

property unmapped: Dict[int, List[str]][source]

Unplaced token positions whose values the mapping does not fully cover.

class spacr.image_import.ImportResult[source]

Counts and per-file reasons describing what apply_import() wrote.

Parameters:
  • destination – the plate folder written.

  • written – how many images the project now names.

  • linked – how many were symlinked rather than copied.

  • bytes_saved – source bytes not duplicated by linking.

  • skipped – source paths that were not written, with the reason.

  • stitched – fields assembled from several tiles.

  • unverified – written name -> why its tile placement could not be verified. Such a field is still written, and listed here so the user can check it.

summary() → str[source]

Return a text report of what was written, what was not, and why.

Each skipped file is listed by name with its reason (the first 20, then a count of the rest), because the fix depends on the reason – a tiled tree needs tiles_as_fields, an unwritable destination needs a different folder.

class spacr.image_import.InferredLayout[source]

Axes inferred from a folder’s filenames, and the tokens left unresolved.

Parameters:
  • root – the folder examined.

  • per_file – relative path -> {axis: value} for what was resolved.

  • slots – the token slots, resolved and unresolved alike.

  • unplaced – axes-shaped tokens that vary but carry no known marker, as {position: [values]}. The caller shows these to the user instead of guessing their axis.

  • skipped – files whose token shape did not match the majority, so nothing was claimed about them.

  • sampled – how many files were read to decide.

summary() → str[source]

One line per axis, plus what could not be placed.

property axes: set[source]

Which axes were resolved anywhere.

class spacr.image_import.InsideFile[source]

What one file’s own metadata says about the axes it holds.

Parameters:
  • pages – how many pages the file has. 1 means a plain 2-D image and nothing below matters.

  • axes – the axis letters the file declares, e.g. "CYX", "ZYX", "TYX". Empty when the file declares none.

  • sizes – {axis: length} for the non-spatial axes only, so {"c": 2} or {"z": 5}.

  • declared – whether the axes came from the file’s metadata. False with pages > 1 means the axis is unknown – a multi-page TIFF with no metadata could be Z, T or C.

property is_ambiguous: bool[source]

Whether the file has several pages and no metadata naming their axis.

class spacr.image_import.TokenSlot[source]

One position in a filename, and what was seen there across the folder.

Parameters:
  • index – position among the tokens of a name.

  • marker – the alphabetic run immediately before it, lower-cased.

  • values – every value seen at this position, in first-seen order.

  • axis – the axis it was resolved to, or "" when unplaced.

property varies: bool[source]

Whether this slot takes more than one value across the folder.

spacr.image_import.apply_import(plan: ImportPlan, destination, *, link: bool = True, plate: str = 'plate1', tiles_as_fields: bool = False, stitch_tiles: bool = True) → ImportResult[source]

Write the spaCR project plan describes.

Images are symlinked by default. consolidate copies every image to rename it, so a 300 GB plate needs 600 GB to import; renaming does not require duplicating bytes. Where a symlink cannot be created the file is copied, and the result reports how many were linked and how many bytes linking saved.

A plan with problems is refused. Every problem the plan reports can make the result wrong – an unnamed axis means images that cannot be told apart – so the plan must be fixed or its questions answered first.

Parameters:
  • plan – a plan whose ImportPlan.problems() is empty.

  • destination – the plate folder to create.

  • link – symlink rather than copy. Falls back to copying per file.

  • plate – the plate name to write into the filenames.

  • stitch_tiles –

    stitch each field’s tiles into one image for that field. On by default.

    The filename convention the core modules read is plate/well/T/field/L/A/Z/channel and has no tile slot, so four tiles of one field would produce four images with one name. A stitched field is one image with one name, so downstream modules do not need a tile axis. See spacr.image_stitch for how the placement is computed, and for what it does when the tiles cannot be aligned.

  • tiles_as_fields –

    give each tile its own field number instead.

    This takes precedence over stitch_tiles, as the more specific request. Tiles are numbered as fields per well, so anything measured per field then describes a part of one acquired field; for that reason it is off by default.

    With both off, the first tile written under a field’s name is kept and every later tile with the same name is skipped with a reason, rather than written over it.

Raises:

ValueError – when the plan still has problems.

spacr.image_import.canonical_name(entry: Dict[str, object], *, plate: str = 'plate1') → str[source]

The spaCR filename for one resolved image.

Missing axes take 1 rather than 0: spaCR’s convention is one-based, and a plate whose only timepoint is T0000 reads as a bug in the acquisition rather than as an absence.

Parameters:
  • entry – one resolved file’s axes, read by the keys plate, well, t, field, z and channel; a missing well becomes A01 and a missing or zero index becomes 1.

  • plate – the plate name used when entry has no plate key.

spacr.image_import.infer_layout(root, *, sample: int = 400, extensions: Sequence[str] = ('.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp')) → InferredLayout[source]

Infer which axes the filenames under root encode.

Parameters:
  • root – the folder to inspect.

  • sample – how many files to read before deciding. Only this many files are read, in sorted order, not the whole tree: a naming convention is visible in a few hundred names, so inspecting a 400-plate archive takes about as long as inspecting one plate.

  • extensions – which files count as images.

Returns:

an InferredLayout. Does not raise for an unrecognised tree; the result then has an empty per_file and a populated unplaced, which the caller is expected to show.

spacr.image_import.load_plan(path) → ImportPlan[source]

Rebuild a plan from a file written by save_plan().

A saved plan lets a repeated acquisition be imported without answering the same mapping questions again, and lets an import run from a script or a cluster job without the GUI.

The plan is re-derived from the folder and the saved mapping rather than from the saved per-file table: the folder may have gained images since, and a stale table would import only the files listed when it was saved.

Parameters:

path – the JSON file written by save_plan(); its root and mapping keys are read and the rest is ignored.

spacr.image_import.plan_import(root, *, sample: int = 400, mapping: Dict[int, Dict[str, int]] | None = None, read_files: bool = True) → ImportPlan[source]

Propose an import of root. Writes nothing.

Parameters:
  • root – the folder of images.

  • sample – how many files to inspect; see infer_layout().

  • mapping – answers for axes the names cannot name.

  • read_files – also open each file for its axis metadata. On by default because channels stored inside a file are not visible in its name; turn it off for a faster first look at a large archive.

spacr.image_import.read_axes_inside(path) → InsideFile[source]

Read the page count and the C, Z and T axis sizes declared in path’s own metadata.

infer_layout reads names, and a name does not carry what an acquisition put inside the file: an OME-TIFF holding two channels is one filename, and a Z-stack and a timelapse of the same field can have identical names. Only the file’s metadata distinguishes them.

No axis is guessed. A multi-page TIFF with no axis metadata could be Z, T or C, and this returns declared=False with the page count rather than picking one, so the caller can ask the user.

Parameters:

path – an image file.

Returns:

an InsideFile. A file that cannot be opened at all comes back as InsideFile(pages=0) rather than raising, so one truncated file does not stop the inspection of the rest of a folder.

spacr.image_import.save_plan(plan: ImportPlan, path) → pathlib.Path[source]

Write plan as JSON that load_plan() can read back.

The root, the mapping and the per-file table are written. The table is there so a person can review the saved plan; load_plan() re-derives the files from the folder and uses only the root and the mapping – see its docstring for why.

Parameters:
  • plan – the plan to save.

  • path – the JSON file to write; missing parent folders are created and an existing file is overwritten.

Returns:

the path written, so a caller can report it.

spacr.image_import.tokenise(name: str) → List[Tuple[str, str]][source]

Split a name into (kind, text) runs, kind "alpha" or "digit".

"plate1_A01_F001" becomes alpha/digit pairs. Separators are dropped: conventions disagree about _ versus - versus nothing, and the disagreement carries no information.

Parameters:

name – the file or folder name to split; every character that is not an ASCII letter or a digit is dropped.