spacr.qt.annotate_engine
========================

.. py:module:: spacr.qt.annotate_engine

.. autoapi-nested-parse::

   Pure-Python backend for the Qt annotate screen.

   The image-processing pipeline (normalize / channel-filter / outline /
   colored border) and the SQLite-backed page fetch + background save
   worker are all Tk-free. The Qt screen wraps this with a QWidget UI.

   Semantics mirror `spacr.gui_elements.AnnotateApp` so annotations made in
   either GUI are read/written the same way from the same
   `measurements/measurements.db`.







Module Contents
---------------

.. py:function:: label_to_hex(val: Optional[int]) -> Optional[str]

   Map an annotation value to a hex border color.

   None / 0 / non-int -> None (no border).
   1 -> blue (#4A9EFF-ish), 2 -> red, 3+ -> golden-ratio hue rotation.


.. py:function:: normalize_pil(img: PIL.Image.Image, percentiles: Tuple[float, float] = (1.0, 99.0), normalize_channels: Optional[Iterable[str]] = None) -> PIL.Image.Image

   Normalize the given PIL image per-channel using percentile stretch.

   If `normalize_channels` is None or empty, the image is returned unchanged
   (aside from clipping to 8-bit range).


.. py:function:: filter_channels_pil(img: PIL.Image.Image, channels: Optional[Iterable[str]] = None) -> PIL.Image.Image

   Zero out channels not present in `channels` (e.g. ['r','g']).


.. py:function:: outline_image(base_img: PIL.Image.Image, full_img: PIL.Image.Image, outline_channels: Optional[Iterable[str]] = None, edge_sigma: float = 1.0, edge_thickness: float = 1.0, edge_transparency: float = 100.0, edge_image: bool = False, outline_threshold_factor: float = 1.0, object_size: Tuple[int, int] = (0, 0)) -> PIL.Image.Image

   Overlay per-channel object outlines on `base_img`.

   Mirrors AnnotateApp.outline_image (Tk) semantics: for every channel
   in `outline_channels`, compute an Otsu-thresholded foreground mask
   on the corresponding channel of `full_img`, extract the boundary,
   optionally dilate it, then alpha-blend it over the channel in
   `base_img` with `edge_transparency/100` opacity. Peak-normalized so
   thin edges stay visible.


.. py:function:: add_colored_border(img: PIL.Image.Image, width: int, color: str) -> PIL.Image.Image

   Return `img` with an inset colored border of `width` px.


.. py:class:: AnnotateSettings

   Every knob the Annotate screen exposes, packed into one dataclass.

   Sensible defaults let callers instantiate ``AnnotateSettings()`` and
   override just the handful of fields they care about.


   .. py:attribute:: src
      :type:  str
      :value: ''



   .. py:attribute:: db_path
      :type:  str
      :value: ''



   .. py:attribute:: annotation_column
      :type:  str
      :value: 'annotate'



   .. py:attribute:: image_size
      :type:  Tuple[int, int]
      :value: (200, 200)



   .. py:attribute:: image_type
      :type:  Optional[str]
      :value: None



   .. py:attribute:: channels
      :type:  Optional[List[str]]
      :value: None



   .. py:attribute:: percentiles
      :type:  Tuple[float, float]
      :value: (1.0, 99.0)



   .. py:attribute:: normalize_channels
      :type:  Optional[List[str]]
      :value: None



   .. py:attribute:: measurement
      :type:  Optional[Any]
      :value: None



   .. py:attribute:: threshold
      :type:  Optional[Any]
      :value: None



   .. py:attribute:: threshold_direction
      :type:  Optional[Any]
      :value: None



   .. py:attribute:: outline
      :type:  Optional[List[str]]
      :value: None



   .. py:attribute:: outline_threshold_factor
      :type:  float
      :value: 1.0



   .. py:attribute:: outline_sigma
      :type:  float
      :value: 1.0



   .. py:attribute:: edge_thickness
      :type:  float
      :value: 1.0



   .. py:attribute:: edge_transparency
      :type:  float
      :value: 100.0



   .. py:attribute:: edge_image
      :type:  bool
      :value: False



   .. py:attribute:: object_size
      :type:  Tuple[int, int]
      :value: (0, 0)



   .. py:attribute:: grid_rows
      :type:  int
      :value: 5



   .. py:attribute:: grid_cols
      :type:  int
      :value: 5



   .. py:property:: page_size
      :type: int


      Number of thumbnails per page (``grid_rows * grid_cols``, min 1).


.. py:function:: ensure_annotation_column(db_path: str, column: str) -> None

   Add `column` INTEGER to `png_list` if missing and index png_path.


.. py:function:: count_rows(db_path: str, image_type: Optional[str] = None) -> int

   Return the number of ``png_list`` rows, optionally filtered by ``image_type``.

   :param db_path: path to ``measurements.db``; missing files count as 0.
   :param image_type: optional substring to filter ``png_path`` on.


.. py:function:: fetch_page(db_path: str, annotation_column: str, offset: int, page_size: int, image_type: Optional[str] = None) -> List[Tuple[str, Optional[int]]]

   Read one page of (png_path, annotation) rows in insertion order.


.. py:function:: fetch_filtered_paths(db_path: str, annotation_column: str, measurements: List[str], thresholds: List[float], directions: List[str], image_type: Optional[str] = None) -> List[Tuple[str, Optional[int]]]

   Return ALL (png_path, annotation) rows matching every one of the
   measurement/threshold/direction triples.

   Rows come from a merge of png_list with the measurement tables (via
   spacr.io._read_and_join_tables) — same code path as the Tk app —
   filtered on png_path substring when `image_type` is given.
   Callers paginate the returned list themselves.


.. py:function:: class_counts(db_path: str, annotation_column: str) -> List[Tuple[int, int]]

   Return sorted list of (class_value, count) for annotated rows.


.. py:function:: clear_column(db_path: str, annotation_column: str) -> None

   Null every value in ``annotation_column`` of ``png_list``.

   :param db_path: path to ``measurements.db``; missing files are ignored.
   :param annotation_column: column to reset.


.. py:function:: find_last_annotated_offset(db_path: str, annotation_column: str, page_size: int, image_type: Optional[str] = None) -> Optional[int]

   Return the page-aligned offset of the last annotated row, or None.


.. py:class:: SaveWorker(db_path: str, annotation_column: str)

   Runs in a daemon thread; consumes {png_path: annotation} batches
   from a Queue and commits them to the DB in coalesced transactions.


   .. py:attribute:: db_path


   .. py:attribute:: annotation_column


   .. py:method:: start() -> None

      Spawn the daemon writer thread if it isn't already running.



   .. py:method:: stop(wait: bool = True) -> None

      Signal the writer to exit; when ``wait`` is True block up to 5 s.



   .. py:method:: submit(batch: dict) -> None

      Enqueue a copy of the batch for saving.



   .. py:property:: busy
      :type: bool


      True while the writer thread is inside a commit.


   .. py:property:: pending_batches
      :type: int


      Number of submitted-but-not-yet-committed batches.


   .. py:property:: last_save_ts
      :type: Optional[float]


      POSIX timestamp of the most recent successful commit, or ``None``.


