spacr.qt.screens.foreign

Import Project — somebody else’s images, masks and measurements, reviewed column by column before any of it becomes a spaCR database.

spacr.foreign does the work; this screen exists for the one step that cannot be automated. Their Area is in µm², spaCR’s cell_area is in px², and the difference between an import that is right and one that is plausibly wrong by a factor of several hundred is whether a human looked at the mapping table. So the mapping table is the middle of the screen, it is editable, and every edit re-runs the conflict and unit checks in place.

Layout:

┌──────────────────────────────────────────────────────────────────┐
│ Images       /data/theirs/images                   [Choose…]     │
│ Masks   [cell ▾] /data/theirs/cell_masks   [Choose…]  [Add]      │
│   cell -> /data/theirs/cell_masks                     [Remove]   │
│ Table        /data/theirs/results.csv              [Choose…]     │
│ µm per pixel [0.65]   On conflict [Refuse ▾]      [Preview]      │
├──────────────────────────────────────────────────────────────────┤
│ their column   →  target            transform unit_in unit_out   │
│ Area (um^2)       foreign_area_um2  area      um^2   px^2        │
│ MeanIntensity     foreign_meanint…  identity                     │
│ …                                     (target/transform/units    │
│                                        are editable in place)    │
├──────────────────────────────────────────────────────────────────┤
│ NOT MAPPED (1): Notes                                            │
│ CONFLICTS (1): [shadows_spacr] 'cell_area' -> 'foreign_cell_area'│
├──────────────────────────────────────────────────────────────────┤
│ /data/imported          [Save mapping…] [Load mapping…] [Import] │
└──────────────────────────────────────────────────────────────────┘

Design notes:

  • Preview writes nothing. spacr.foreign.plan_import() scans, pairs and verifies; Import is a separate press, and it stays disabled while the plan has a blocking problem — a target that collides with a spaCR feature name, a measurement table with no object-label column, a field whose masks do not pair.

  • Editing is instant and local. A cell edit calls spacr.foreign.ImportPlan.with_column_maps(), which re-resolves the columns without touching the disk, so the unmapped list, the conflict list and the Import button update as you type.

  • Off the GUI thread. Scanning a plate of TIFFs and reading every label image takes seconds; the import takes minutes. Both go through spacr.qt.bridge.make_thread(), and the completion handler is reached through a bound method (ForeignScreen._job_settled) rather than a closure, because PipelineWorker.finished is emitted in the worker thread and a closure connected to it would build widget children there. Tests pass threaded=False.

  • No modal dialogs on any error path. Everything lands in the inline status label and the report pane. A QMessageBox would hang a headless run.

Classes

ColumnMapModel

Editable table over a list of spacr.foreign.ColumnMap.

ForeignScreen

Pick their files, review the mapping, import, read the summary.

Functions

install_folds(...)

Put Import's fold strip on screen's masthead.

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

Every filename convention Import can read, as (key, label).

object_label(→ str)

What a mask class is CALLED, as opposed to what it is keyed by.

organelle_slots_offered(→ int)

How many organelle slots a form should show, given what is filled.

Module Contents

class spacr.qt.screens.foreign.ColumnMapModel(parent=None)[source]

Bases: PySide6.QtCore.QAbstractTableModel

Editable table over a list of spacr.foreign.ColumnMap.

A model rather than a QTableWidget because a CellProfiler export has several hundred columns and building that many QTableWidgetItems freezes the window.

An edit emits mapping_edited as well as dataChanged, and the screen listens to the former. Hanging the live re-resolve off dataChanged looks equivalent and is not: re-resolving updates the per-row status, updating the status refreshes the tooltips, and refreshing tooltips emits dataChanged again — one keystroke recursed until the interpreter ran out of stack.

Parameters:

parent – parent widget.

Build an empty column mapping.

Parameters:

parent – parent object.

columnCount(parent=QModelIndex()) → int[source]

How many columns the mapping table shows.

Parameters:

parent – unused; the model is flat.

Returns:

the column count.

data(index, role=Qt.DisplayRole)[source]

One cell of the mapping table.

Parameters:
  • index – the cell.

  • role – the Qt display role.

Returns:

the cell’s value for that role, or None.

flags(index)[source]

Which cells the user may edit.

Only the target column: the source name is what the foreign file actually contains, and editing it would let a user rename their data rather than map it.

Parameters:

index – the cell.

Returns:

the Qt item flags.

headerData(section, orientation, role=Qt.DisplayRole)[source]

One header label.

Parameters:
  • section – the row or column number.

  • orientation – which header.

  • role – the Qt display role.

Returns:

the label, or None.

map_at(row: int) → spacr.foreign.ColumnMap | None[source]

One row’s mapping, or None when row is out of range.

Parameters:

row – zero-based table row.

maps() → List[spacr.foreign.ColumnMap][source]

The mapping as it currently stands, including every edit.

rowCount(parent=QModelIndex()) → int[source]

How many source columns are waiting to be mapped.

Parameters:

parent – unused; the model is flat.

Returns:

the row count.

row_of(source: str) → int[source]

The row index for a source column name, or -1.

Parameters:

source – the foreign table’s column name, compared as a string with each mapping’s source.

setData(index, value, role=Qt.EditRole) → bool[source]

Record which spaCR column a source column maps to.

Parameters:
  • index – the cell.

  • value – what the user chose.

  • role – the Qt edit role.

Returns:

True when the mapping was taken.

set_maps(maps: List[spacr.foreign.ColumnMap] | None) → None[source]

Replace the whole mapping.

Parameters:

maps – the spacr.foreign.ColumnMap rows to show, in order; None empties the table.

set_status(status: Dict[str, str] | None) → None[source]

Attach a per-source status (mapped / uncalibrated / …).

Shown as the row’s tooltip, so a user can see why a column was renamed without leaving the table.

Parameters:

status – mapping of source column name to status word; None clears every status.

class spacr.qt.screens.foreign.ForeignScreen(parent=None, threaded: bool = True)[source]

Bases: PySide6.QtWidgets.QWidget

Pick their files, review the mapping, import, read the summary.

Parameters:
  • parent – parent widget.

  • threaded – when False every job runs inline on the calling thread. Tests use it so assertions are exact; the app leaves it True so a long import does not freeze the window.

Build the importer: images, masks, table and mapping.

Parameters:
  • parent – parent widget.

  • threaded – whether the import runs on a worker.

active_jobs() → int[source]

How many worker threads are still winding down.

add_mask_folder(object_type: str, path: str) → bool[source]

Register a mask folder for one object class.

Parameters:
  • object_type – storage name of a mask class in OBJECT_CHOICES, e.g. "cell"; surrounding whitespace is ignored.

  • path – folder holding that class’s masks; replaces any folder already registered for the class.

Returns:

False when the class is unknown or the path is blank, with the reason inline — never an exception into a GUI slot.

can_import() → bool[source]

True when the Import button is live.

clear_mask_folders() → None[source]

Forget every mask folder, so a new set does not inherit the last.

column_maps() → List[spacr.foreign.ColumnMap][source]

The mapping as edited — exactly what Import would apply.

conflict_lines() → List[str][source]

One line per conflict, blocking or not.

custom_regex() → str[source]

The pattern typed for the Custom convention.

destination_path() → str[source]

The destination currently typed in.

images_path() → str[source]

The image folder currently typed in.

is_busy() → bool[source]

True while a preview or an import is in flight.

load_mapping(path: str) → bool[source]

Read a mapping back and apply it to the plan on screen.

Parameters:

path – column-map CSV written by save_mapping() or spacr.foreign.save_column_map(), possibly edited. A read error is shown in the status line and returns False.

mapping_row_count() → int[source]

Rows in the mapping table.

mask_folders() → Dict[str, str][source]

{object_type: folder}, in spaCR’s mask-plane order.

measurements_path() → str[source]

The measurement table currently typed in.

metadata_type() → str[source]

The chosen convention’s key; NAMING_AUTO for folders.

on_conflict() → str[source]

The selected collision policy.

pixel_size() → float | None[source]

The scale as a number, or None when blank or unparseable.

None is a real answer, not a failure: a column that needs a pixel size and does not have one is reported uncalibrated rather than scaled by 1.0.

plan() → spacr.foreign.ImportPlan | None[source]

The plan currently on screen, or None.

preview() → bool[source]

Scan, pair and verify. Writes nothing.

Returns:

True when the job was started (or, unthreaded, completed); False when the inputs are unusable, with the reason in the inline status label.

remove_mask_folder(object_type: str) → bool[source]

Forget one object class’s mask folder.

Parameters:

object_type – storage name of the mask class; returns False when no folder is registered for it.

report_text() → str[source]

Whatever is in the report pane.

result() → spacr.foreign.ImportResult | None[source]

The result of the last import, or None.

run_import() → bool[source]

Build the project. Off the GUI thread unless threaded=False.

Returns:

True when the job was started, False when it was refused — with the reason inline.

save_mapping(path: str) → bool[source]

Write the mapping on screen to a reviewable CSV.

Parameters:

path – destination CSV file, written by spacr.foreign.save_column_map(). Returns False when there is nothing to save or the write fails.

set_custom_regex(pattern: str) → None[source]

Set the pattern the Custom convention parses by.

Parameters:

pattern – a regular expression with named groups wellID, fieldID and chanID; '' clears it.

set_destination(path: str) → None[source]

Set the destination project root.

Parameters:

path – folder the import is written to; None or empty clears the field.

set_images(path: str) → None[source]

Point the screen at their image folder without opening a dialog.

Parameters:

path – the foreign image folder. When the destination is still empty it is filled with this path plus _spacr.

set_mapping_value(row: int, key: str, value: str) → bool[source]

Edit one cell the way the delegate would.

The screen’s own edit path, exposed so a test drives the same code an editor widget does rather than reaching into the model.

Parameters:
  • row – zero-based mapping row.

  • key – column name from MAP_COLUMNS, e.g. "target"; an unknown or read-only column returns False.

  • value – new cell text; surrounding whitespace is stripped.

set_measurements(path: str) → None[source]

Point the screen at their measurement table.

Parameters:

path – the foreign measurement table file; None or empty clears the field.

set_metadata_type(key: str) → bool[source]

Choose the filename convention the files are read by.

Parameters:

key – a key from naming_choices(); '' means folders.

Returns:

False, with the reason inline, for a key the list lacks.

set_on_conflict(value: str) → None[source]

Choose refuse-or-rename for colliding targets.

Parameters:

value – "refuse" or "rename", as in CONFLICT_CHOICES; anything else raises ValueError.

set_pixel_size(value: Any) → None[source]

Set the µm-per-pixel scale; None or ‘’ means unknown.

Parameters:

value – micrometres per pixel, as a number or text; it is shown as typed and parsed later by pixel_size().

status_text() → str[source]

Current inline status message.

unmapped_columns() → List[str][source]

Their columns with no mapping, by name.

spacr.qt.screens.foreign.install_folds(screen: PySide6.QtWidgets.QWidget) → spacr.qt.widgets.fold_strip.FoldStrip | None[source]

Put Import’s fold strip on screen’s masthead.

spacr.qt.screens.foreign.naming_choices() → List[Tuple[str, str]][source]

Every filename convention Import can read, as (key, label).

The SAME list Mask’s metadata_type offers, read from spacr.regex_infer._metadata_convention_menu(), so a plate Mask can parse is a plate Import can parse and the two lists cannot drift. Folders come first because they are what Import did before it knew any convention; the vendor conventions follow in Mask’s vendor order, each labelled with its vendor and marked when spaCR is guessing about it.

Returns:

the choices, NAMING_AUTO first.

spacr.qt.screens.foreign.object_label(role: str) → str[source]

What a mask class is CALLED, as opposed to what it is keyed by.

NO USER SEES A LETTER, asked for in as many words on 2026-09-02: “i dont want to see organelle a b c d anywhere, organelle number should always be controlled by number of organelles”. The slots are stored lettered – organelle, organelleb, organellec – because a role name is the prefix of every settings key it owns and organelle2 cannot round-trip through a prcfo key without colliding with the object LABELLED 2. That is a storage decision and it has no business on screen, where a slot is a NUMBER: Organelle 1, Organelle 2.

Parameters:

role – a role from OBJECT_CHOICES.

spacr.qt.screens.foreign.organelle_slots_offered(added: Sequence[str]) → int[source]

How many organelle slots a form should show, given what is filled.

THE COUNT FOLLOWS THE ORGANELLES, not a constant. Offering four slots to a user with one organelle is three questions nobody asked, and offering exactly the ones in use plus one is what “controlled by the number of organelles” means on a form that has no separate count field: adding the first makes the second available, and so on for as many as the mask planes can carry.

Parameters:

added – the roles already mapped.

Returns:

how many organelle slots to list, at least one.

Nested helpers

ForeignScreen.preview._job()

Plan the foreign import. Off the GUI thread.

spacr/qt/screens/foreign.py:988

ForeignScreen.run_import._job()

Run the foreign import. Off the GUI thread.

spacr/qt/screens/foreign.py:1158