spacr.qt.screens.image_import

Import Images — a pile of files from any microscope, read before it is written.

spacr.image_import does the work; this screen exists for the step the instruction that asked for it calls “the single most important requirement”: showing the proposal. spaCR’s import path before this asked the user to pick a filename convention from a closed list — cellvoyager, cq1, or write your own regular expression — and gave them no way to see whether it worked until masks came out wrong. Of ten real acquisition layouts, two parsed and eight recovered nothing.

So the middle of this screen is the parse, one row per file, with the filename beside the fields it was parsed into. A field read as a channel is visible there in a second and invisible everywhere else.

Layout:

┌──────────────────────────────────────────────────────────────────┐
│ Images   /data/raw                                  [Choose…]    │
│ Sample [400] ☑ Read inside each file                [Scan]       │
├──────────────────────────────────────────────────────────────────┤
│ file                     plate   well  field channel z   t       │
│ A01/fld1_DAPI.tif        plate1  A01   1     1       1   1       │
│ A01/fld1_GFP.tif         plate1  A01   1     2       1   1       │
├──────────────────────────────────────────────────────────────────┤
│ token  value   is channel   (editable — one answer per value)    │
│ 0      DAPI    1                                                 │
│ 0      GFP     2                                                 │
├──────────────────────────────────────────────────────────────────┤
│ 8 files, 4 axes resolved …  ! what is still unanswered           │
├──────────────────────────────────────────────────────────────────┤
│ /data/raw_spacr  plate1  ☑link ☐tiles  [Save…][Load…][Import]    │
└──────────────────────────────────────────────────────────────────┘

Design notes:

  • Scan writes nothing. spacr.image_import.plan_import() walks a sample of the tree and reads each file’s own axis metadata; Import is a separate press, and it stays disabled while the plan has a problem — because every problem the plan states is a way for the result to be quietly wrong, and the import is the irreversible half.

  • The proposal table is not editable, and the answer table is. A wrong guess is never wrong for one file: it is wrong for a token position, in every name that has one. So the correction is made where the mistake lives, one answer fixing every file that shares it, rather than by retyping eight thousand rows. That is also the only correction spacr.image_import.ImportPlan.with_mapping() can apply without re-walking the disk, which is what makes editing instant.

  • Everything the plan could not place is on screen, with its values, and counted. Silence about unparsed files is how a plate imports with a third of its fields missing.

  • Off the GUI thread. A scan reads every file’s header; an import of a 400-plate archive takes minutes. Both go through spacr.qt.bridge.make_thread(), and the completion handler is reached through a bound method (ImageImportScreen._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, as in spacr.qt.screens.foreign — a QMessageBox would hang a headless run.

Classes

AnswerModel

Editable table of the tokens inference could not name.

ImageImportScreen

Choose a folder, read the proposal, answer what it asks, import.

ProposalModel

Read-only table over spacr.image_import.ImportPlan.rows().

Module Contents

class spacr.qt.screens.image_import.AnswerModel(parent=None)[source]

Bases: PySide6.QtCore.QAbstractTableModel

Editable table of the tokens inference could not name.

ONE ROW PER VALUE, not per token: the question “what does DAPI mean” has a different answer from “what does GFP mean”, and a single cell holding both would have to be parsed back out of a string the user typed.

An edit emits answers_edited as well as dataChanged, and the screen listens to the former — re-resolving the plan refreshes this table’s tooltips, and a live re-resolve hung off dataChanged would recurse.

Parameters:

parent – parent widget.

Create an empty answer table model.

Parameters:

parent – parent object, or None.

answers() → List[List[str]][source]

Every row as it stands, answered or not.

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

How many columns the answer table shows.

Parameters:

parent – unused; the model is flat.

Returns:

the column count.

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

One cell of the answer 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 answer column: the value being named is what the files actually contain, and letting it be edited would let a user rename the data rather than explain 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.

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

The answers as spacr.image_import.ImportPlan.with_mapping() wants them, skipping every row still blank or not yet a number.

A HALF-TYPED ANSWER IS NOT AN ANSWER. "1" arrives one keystroke after "", and treating a blank or a stray letter as zero would write a channel nobody asked for.

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

How many unnamed values are waiting for an answer.

Parameters:

parent – unused; the model is flat.

Returns:

the row count.

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

Record the user’s answer for one value.

Parameters:
  • index – the cell.

  • value – what the user typed.

  • role – the Qt edit role.

Returns:

True when the answer was taken.

set_answer(row: int, text: str) → bool[source]

Type into one row’s answer cell, the way the editor would.

Parameters:
  • row – the question row; out of range returns False.

  • text – the answer, normally a channel number; stored stripped.

Returns:

True when the answer changed.

set_plan(plan: spacr.image_import.ImportPlan | None) → None[source]

Ask, for every token position the names could not place, what its values mean — carrying forward any answer already given.

Parameters:

plan – the import plan whose layout.unplaced values become question rows, pre-filled from its mapping; None clears the table.

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

Bases: PySide6.QtWidgets.QWidget

Choose a folder, read the proposal, answer what it asks, import.

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 scan does not freeze the window.

Build the screen and arm its drop zone.

The drop handler also takes a saved plan, so last week’s answers arrive by the same gesture this week’s images do.

Parameters:
  • parent – parent widget, or None.

  • threaded – run the scan and the import on a worker thread. Set False in tests so scan finishes before it returns.

active_jobs() → int[source]

How many worker threads are still winding down.

answer_question(row: int, channel: str) → bool[source]

Answer one row of the question table, as an editor would.

Parameters:
  • row – the question row; out of range returns False.

  • channel – the answer typed into the row, normally a channel number.

Returns:

True when the answer changed.

can_import() → bool[source]

True when the Import button is live.

destination_path() → str[source]

The destination currently typed in.

is_busy() → bool[source]

True while a scan or an import is in flight.

Whether the import will symlink.

load_plan(path: str) → bool[source]

Read a saved plan back and show it.

The folder is re-read and only the saved ANSWERS are reused, which is the point of loading one: this week’s plate may have more images than last week’s, and replaying a stale file table would silently import last week’s.

Parameters:

path – the saved plan file, read with spacr.image_import.load_plan().

Returns:

False, with the reason in the status line, when the file cannot be read.

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

The plan currently on screen, or None.

plate_name() → str[source]

The plate name, falling back to plate1 when it is blank.

problems() → List[str][source]

Everything that would make this import wrong, in plain sentences.

proposal_columns() → List[str][source]

The proposal’s columns: the axes this folder actually has.

proposal_row_count() → int[source]

Rows in the proposal table — one per image the scan read.

proposal_value(row: int, column: str) → str[source]

One parsed field, by row and column name.

Parameters:
  • row – the zero-based row of the proposal table.

  • column – the column name; an unknown name or out-of-range row gives "".

question_count() → int[source]

How many values are waiting for an answer.

questions() → List[List[str]][source]

[token, value, answer] for each unnamed value.

read_inside() → bool[source]

Whether files are opened during the scan.

report_text() → str[source]

Whatever is in the report pane.

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

The result of the last import, or None.

root_path() → str[source]

The image folder currently typed in.

run_import() → bool[source]

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

sample() → int[source]

The sample size.

save_plan(path: str) → bool[source]

Write the plan on screen where a later run can load it back.

Parameters:

path – destination file, written with spacr.image_import.save_plan().

Returns:

False, with the reason in the status line, when there is no plan yet or the write fails.

scan() → bool[source]

Work out what the folder holds. Writes nothing.

Returns:

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

set_destination(path: str) → None[source]

Where the project will be written.

Parameters:

path – the destination folder; None or empty clears it.

Whether the import symlinks rather than copies.

Parameters:

on – truthy to symlink, falsy to copy.

set_plate_name(name: str) → None[source]

The plate name every written filename carries.

Parameters:

name – the plate name; None or empty clears it.

set_read_inside(on: bool) → None[source]

Whether the scan opens each file for its own axis metadata.

Parameters:

on – truthy to read metadata inside each file.

set_root(path: str) → None[source]

Point the screen at a folder of images without opening a dialog.

Fills the destination with <folder>_spacr when it is still empty, because a destination INSIDE the folder being read is the consolidate bug this module replaces: the second run imports the first run’s output.

Parameters:

path – the folder of images to read; None or empty clears the field.

set_sample(count: int) → None[source]

How many files the scan reads before deciding.

Parameters:

count – the number of files; converted with int and held to the box’s range of 10 to 1,000,000.

set_tile_policy(policy: str) → bool[source]

Choose what happens to a field that arrives as several tiles.

Parameters:

policy – one of the values in TILE_POLICIES.

Returns:

False for a policy that is not offered, with the reason inline rather than as an exception into a GUI slot.

set_tiles_as_fields(on: bool) → None[source]

Whether each tile is given a field number of its own.

The older two-state spelling of set_tile_policy(), kept because it is what a caller who has not heard of stitching means: off is now the DEFAULT policy rather than the skip it used to be, since a stitched field is what it always wanted.

Parameters:

on – truthy selects the "fields" policy, falsy "stitch".

status_text() → str[source]

Current inline status message.

stitch_tiles() → bool[source]

Whether tiles will be assembled into the field they came from.

tile_policy() → str[source]

What will happen to tiled fields: stitch, fields or skip.

tiles_as_fields() → bool[source]

Whether tiles will be written as fields.

class spacr.qt.screens.image_import.ProposalModel(parent=None)[source]

Bases: PySide6.QtCore.QAbstractTableModel

Read-only table over spacr.image_import.ImportPlan.rows().

A model rather than a QTableWidget because a plate is tens of thousands of files and building that many QTableWidgetItems freezes the window.

Its header and its cells both come from the plan, so this screen and the text table spacr.image_import.ImportPlan.table() prints cannot disagree about what was inferred — one is the other with padding.

Parameters:

parent – parent widget.

Create an empty proposal table model.

Parameters:

parent – parent object, or None.

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

How many columns the proposal shows.

Parameters:

parent – unused; the model is flat.

Returns:

the column count.

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

One cell of the proposal.

Parameters:
  • index – the cell.

  • role – the Qt display role.

Returns:

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

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.

headers() → List[str][source]

The columns currently shown — the axes this folder actually has.

row(index: int) → List[str][source]

One row’s cells, or an empty list when index is out of range.

Parameters:

index – the zero-based row number.

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

How many proposed rows the plan holds.

Parameters:

parent – unused; the model is flat.

Returns:

the row count.

set_plan(plan: spacr.image_import.ImportPlan | None) → None[source]

Show plan’s parse, or clear the table for None.

Parameters:

plan – the import plan whose columns() and rows() fill the table, or None.

value_at(index: int, column: str) → str[source]

One cell, addressed by column NAME.

The columns depend on the folder — a tiled tree has a tile column and a flat one does not — so a caller that wants the channel must ask for “channel” rather than for column 4.

Parameters:
  • index – the zero-based row number.

  • column – the column name; an unknown name or out-of-range row gives "".

Nested helpers

ImageImportScreen.run_import._job()

Write the project in the worker thread.

Every argument is a plain value captured above, for the same reason the scan’s is: this runs off the GUI thread, and reading a checkbox from there is undefined behaviour rather than a stale answer.

spacr/qt/screens/image_import.py:1133

ImageImportScreen.scan._job()

Read the folder in the worker thread. Writes nothing.

The settings are read out of the widgets BEFORE this closes over them: a job must not touch a QLineEdit from another thread, and a user who edits the sample size mid-scan would otherwise change what the running scan is doing.

spacr/qt/screens/image_import.py:873