spacr.spacrops
==============

.. py:module:: spacr.spacrops






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

.. py:class:: spacrStitcher(detector: str = 'ORB', nfeatures: int = 6000, max_keypoints: Optional[int] = 2000, downsample: float = 0.5, ransac_thresh_px: float = 3.0, allow_scale: bool = False, allow_rotation: bool = False, outline_source: str = 'otsu', canny: Tuple[int, int] = (40, 120), blur_sigma: float = 0.0, dilate_ksize: int = 0, line_thickness: int = 1, outline_alpha: float = 1.0, outdir: str = './sbs_out', save_qc: bool = True, save_stitched_default: bool = True, all_scores: bool = False, score_threshold: Optional[float] = None, verbose: bool = False, feature_cache_mode: str = 'disk', feature_cache_dir: Optional[str] = None, max_ram_features: int = 256, n_workers_features: Optional[int] = None, pair_batch_size: int = 8000, stream_csv: bool = True, opencv_threads: int = 1, arr_axes: str = 'AUTO', mip: bool = False, z_index: int = 0, t_index: int = 0, squeeze_singleton: bool = True)

   Pairwise stitcher with downsampled scoring and whole-well mosaic assembly.

   Robustness features for very large datasets:
     - feature_cache_mode: "disk" (default) writes DS features to disk with an LRU RAM cap
     - pair_batch_size:    limit number of concurrent pair futures
     - stream_csv:         write pairwise rows immediately (low RAM)
     - opencv_threads:     limit OpenCV internal threading to avoid oversubscription

   Transform control
   -----------------
     allow_scale:    if True  → use RANSAC affine (rotation+scale+translation)
                     if False → enforce unit scale (see allow_rotation)
     allow_rotation: if True  → rotation+translation
                     if False → translation-only

   Mosaic
   ------
     Uses topology-aware pruning + maximum spanning tree on pairwise scores
     to build a cohesive grid. Exports mosaic image and a mosaic.csv manifest.

   Axis & Z handling
   -----------------
     arr_axes : str in {"AUTO"} or a string over {T,C,Z,Y,X} (e.g. "CZYX", "CYX", "ZYX").
                Determines how to interpret multi-dimensional TIFFs.
     mip      : bool. If True and Z exists, perform max-intensity projection over Z.
     z_index  : int  (if mip=False) choose Z slice index.
     t_index  : int  choose time index if T exists.


   .. py:attribute:: detector
      :value: ''



   .. py:attribute:: nfeatures
      :value: 6000



   .. py:attribute:: max_keypoints
      :value: None



   .. py:attribute:: downsample


   .. py:attribute:: ransac_thresh_px


   .. py:attribute:: allow_scale
      :value: False



   .. py:attribute:: allow_rotation
      :value: False



   .. py:attribute:: outline_source
      :value: ''



   .. py:attribute:: canny
      :value: (40, 120)



   .. py:attribute:: blur_sigma


   .. py:attribute:: dilate_ksize
      :value: 0



   .. py:attribute:: line_thickness
      :value: 1



   .. py:attribute:: outline_alpha


   .. py:attribute:: outdir
      :value: b'.'



   .. py:attribute:: save_qc
      :value: True



   .. py:attribute:: save_stitched_default
      :value: True



   .. py:attribute:: all_scores
      :value: False



   .. py:attribute:: score_threshold
      :value: None



   .. py:attribute:: verbose
      :value: False



   .. py:attribute:: feature_cache_mode
      :value: ''



   .. py:attribute:: n_workers_features


   .. py:attribute:: pair_batch_size
      :value: 8000



   .. py:attribute:: stream_csv
      :value: True



   .. py:attribute:: arr_axes
      :value: ''



   .. py:attribute:: mip
      :value: False



   .. py:attribute:: z_index
      :value: 0



   .. py:attribute:: t_index
      :value: 0



   .. py:attribute:: squeeze_singleton
      :value: True



   .. py:method:: set_meta_regex(pattern: Union[str, re.Pattern])

      Replace the filename regex used to parse well, site, channel and magnification.



   .. py:method:: prepare_features(paths: List[str], channel_index: int, num_workers: Optional[int] = None)

      Precompute keypoints and descriptors for ``paths`` and cache them.

      In ``disk`` mode every result is flushed to the on-disk feature store
      with only an LRU-sized subset kept in RAM; in ``ram`` mode results
      are held in memory only.

      :param paths: image files to feature.
      :param channel_index: channel to read from multi-channel TIFFs.
      :param num_workers: worker thread count; defaults to ``self.n_workers_features``.
      :returns: None.



   .. py:method:: stitch_pair(pathA: str, pathB: str, channel_index: int = 0, score_threshold: Optional[float] = None, save_stitched: Optional[bool] = None, force_no_qc: bool = False, qc_only_if_score_ge: Optional[float] = None) -> Optional[Dict]

      Estimate the B-to-A affine, score the overlap, and optionally write the stitched pair.

      :param pathA: reference image path.
      :param pathB: image to align to ``pathA``.
      :param channel_index: channel used for feature matching. Default ``0``.
      :param score_threshold: minimum edge-ZNCC score required to save the stitched TIFF; falls back to ``self.score_threshold``.
      :param save_stitched: override for whether to write the stitched image; falls back to ``self.save_stitched_default``.
      :param force_no_qc: suppress QC overlay images regardless of settings.
      :param qc_only_if_score_ge: emit QC overlays only when the pair score meets this cutoff.
      :returns: dict of pair metrics and output paths, or ``None`` when matching/RANSAC fails.



   .. py:method:: run_folder(folder: str, csv_path: str, *, channel_index: int = 0, exts: Tuple[str, Ellipsis] = ('.tif', '.tiff'), recursive: bool = False, same_well_only: bool = True, max_site_gap: int = 3, n_workers: int = 8, stitch: bool = True, score_threshold: Optional[float] = None, meta_regex: Optional[Union[str, re.Pattern]] = None, mosaic: bool = False, mosaic_out: Optional[str] = None, mosaic_min_score: Optional[float] = None, mosaic_csv_out: Optional[str] = None, mosaic_all_channels: bool = False, mosaic_channel_count: Optional[int] = None, mosaic_channel_index_order: Optional[List[int]] = None, qc_pairs_threshold: int = 1000, qc_only_above_threshold_when_many: bool = True) -> str

      Score every candidate pair in ``folder`` and optionally build a whole-well mosaic.

      When the number of candidate pairs exceeds ``qc_pairs_threshold``, QC
      overlays are skipped in the first pass and reissued only for pairs
      whose score clears the auto-threshold in the second pass.

      :param folder: directory of tiles to scan.
      :param csv_path: destination CSV for pairwise metrics.
      :param channel_index: channel used for feature scoring. Default ``0``.
      :param exts: accepted image extensions.
      :param recursive: recurse into subfolders. Default ``False``.
      :param same_well_only: restrict pairings to the same parsed well. Default ``True``.
      :param max_site_gap: maximum site-index gap between candidate neighbours. Default ``3``.
      :param n_workers: worker threads for the scoring pass. Default ``8``.
      :param stitch: save stitched TIFFs for pairs above the score threshold.
      :param score_threshold: minimum score required to save the stitched image.
      :param meta_regex: override the filename regex before scanning.
      :param mosaic: build a single-channel mosaic once pair scoring finishes.
      :param mosaic_out: output TIFF path for the mosaic (single-channel mode).
      :param mosaic_min_score: minimum edge score used when pruning mosaic edges.
      :param mosaic_csv_out: output CSV path for the mosaic manifest.
      :param mosaic_all_channels: build a CYX mosaic across all channels.
      :param mosaic_channel_count: force this many channels for the multi-channel mosaic.
      :param mosaic_channel_index_order: explicit channel order for the multi-channel mosaic.
      :param qc_pairs_threshold: pair count above which QC plotting is gated.
      :param qc_only_above_threshold_when_many: enable the second-pass, score-gated QC output.
      :returns: path to the pairwise-metrics CSV.



   .. py:method:: build_multichannel_mosaic_from_manifest(manifest_csv: str, out_tif: str, out_png: Optional[str] = None, channel_indices: Optional[List[int]] = None, blend: str = 'max', tmp_dir: Optional[str] = None, preview_downsample: int = 8)

      Build a CYX BigTIFF mosaic from a 'mosaic.csv' manifest written by
      spacrStitcher.render_mosaic_from_csv(...).

      Expected CSV columns (per tile row):
        - path, H, W,
          M00, M01, M02,
          M10, M11, M12,
          canvas_x, canvas_y, best_pair_score

      .. rubric:: Notes

      * Uses the 2x3 affine in the manifest (already includes global offset).
      * If `channel_indices` is None, infers the channel count from the first valid tile.
      * `blend="max"` takes per-pixel max across overlapping tiles;
        `blend="overwrite"` writes the latest tile over earlier ones where coverage>0.
      * If `tmp_dir` is provided (or available from self.feature_cache_dir), the output
        workspace uses a disk memmap to reduce RAM.



   .. py:method:: render_mosaic_from_csv(csv_path: str, out_tif: str, out_png: Optional[str] = None, channel_index: int = 0, min_score: Optional[float] = None, *, angle_tol_deg: float = 30.0, step_tol_frac: float = 0.25, rot_tol_deg: float = 5.0, scale_tol: float = 0.03, cap_one_per_dir: bool = True, out_csv: Optional[str] = None) -> Tuple[str, Optional[str]]

      Assemble a mosaic image (and manifest) from a pairwise-metrics CSV.

      Pass ``out_tif=None`` together with an ``out_csv`` to run in
      manifest-only mode (transforms are computed and written but no
      image is rendered).

      :param csv_path: input pairwise metrics CSV.
      :param out_tif: mosaic TIFF output path (``None`` for manifest-only).
      :param out_png: optional preview PNG output path.
      :param channel_index: channel used for rendering. Default ``0``.
      :param min_score: minimum pair score kept for the mosaic (auto-detected when ``None``).
      :param angle_tol_deg: tolerance for classifying edges into R/L/U/D bins.
      :param step_tol_frac: fractional tolerance around the estimated grid step.
      :param rot_tol_deg: max allowed rotation when rotation is disabled.
      :param scale_tol: max allowed scale deviation when scaling is disabled.
      :param cap_one_per_dir: keep at most one edge per direction per tile.
      :param out_csv: optional path to the per-tile manifest CSV.
      :returns: tuple ``(out_tif, out_png)``.



   .. py:method:: mosaic_all_channels_from_csv(csv_path: str, out_tif: Optional[str], *, min_score: Optional[float] = None, channel_count: Optional[int] = None, channel_index_order: Optional[List[int]] = None, angle_tol_deg: float = 30.0, step_tol_frac: float = 0.25, rot_tol_deg: float = 5.0, scale_tol: float = 0.03, cap_one_per_dir: bool = True, out_csv: Optional[str] = None) -> Optional[str]

      Assemble a multi-channel CYX mosaic by reusing pairwise transforms across channels.

      Pass ``out_tif=None`` with an ``out_csv`` to run in manifest-only
      mode (compute transforms and canvas geometry, write manifest, skip
      rendering).

      :param csv_path: input pairwise metrics CSV.
      :param out_tif: mosaic TIFF output path (``None`` for manifest-only).
      :param min_score: minimum pair score kept for the mosaic (auto-detected when ``None``).
      :param channel_count: force this many channels; auto-detected when ``None``.
      :param channel_index_order: explicit channel index ordering for the output.
      :param angle_tol_deg: tolerance for classifying edges into R/L/U/D bins.
      :param step_tol_frac: fractional tolerance around the estimated grid step.
      :param rot_tol_deg: max allowed rotation when rotation is disabled.
      :param scale_tol: max allowed scale deviation when scaling is disabled.
      :param cap_one_per_dir: keep at most one edge per direction per tile.
      :param out_csv: optional per-tile manifest CSV path.
      :returns: path to the mosaic TIFF (or ``None`` in manifest-only mode).



.. py:class:: StitchedMultiAligner(detector: str = 'ORB', nfeatures: int = 6000, max_keypoints: Optional[int] = 2000, downsample: float = 0.5, ransac_thresh_px: float = 3.0, allow_scale: bool = False, allow_rotation: bool = False, outdir: str = './align_out', opencv_threads: int = 1, arr_axes: str = 'AUTO', mip: bool = False, z_index: int = 0, t_index: int = 0, squeeze_singleton: bool = True)

   Align an arbitrary number of stitched mosaics (multi-channel, arbitrary axes) to a common reference
   using the nuclei/ Hoechst channel. Saves a channel-concatenated aligned stack and a CSV manifest
   describing the mapping from input channels to output channels and the estimated transforms/scores.

   Output image axes: CYX (channels stacked in the order inputs are provided).

   :param detector: Feature detector for keypoint matching.
   :type detector: {"ORB","SIFT"}
   :param nfeatures: Feature budget for detector.
   :type nfeatures: int
   :param max_keypoints: Hard cap on kept keypoints after detection (by detector’s internal ranking).
   :type max_keypoints: Optional[int]
   :param downsample: Downsample factor for feature/score pass.
   :type downsample: float in (0,1]
   :param ransac_thresh_px: Reprojection threshold (pixels) for affine estimation (downsampled space).
   :type ransac_thresh_px: float
   :param allow_scale: If False, constrain to rotation+translation (or translation only if allow_rotation=False).
   :type allow_scale: bool
   :param allow_rotation: If False, constrain to translation only.
   :type allow_rotation: bool
   :param outdir: Output directory for images/csv.
   :type outdir: str
   :param opencv_threads: Limit OpenCV internal threading (avoid oversubscription).
   :type opencv_threads: int
   :param # Axis/Z/time handling (for TIFF reading):
   :param arr_axes:
   :type arr_axes: "AUTO" or a string over {T,C,Z,Y,X}
   :param mip: If True and Z exists, max-project Z.
   :type mip: bool
   :param z_index: If mip=False, choose Z slice.
   :type z_index: int
   :param t_index: Choose T index if T exists.
   :type t_index: int
   :param squeeze_singleton: Squeeze 1-length axes after slicing.
   :type squeeze_singleton: bool

   .. rubric:: Notes

   - Alignment is done to the first image in `paths` (reference).
   - For each image, you can provide a per-image nuclei channel index via `nuclei_channel_indices`.
     If None, defaults to 0 for all.


   .. py:attribute:: detector
      :value: ''



   .. py:attribute:: nfeatures
      :value: 6000



   .. py:attribute:: max_keypoints
      :value: None



   .. py:attribute:: downsample


   .. py:attribute:: ransac_thresh_px


   .. py:attribute:: allow_scale
      :value: False



   .. py:attribute:: allow_rotation
      :value: False



   .. py:attribute:: outdir
      :value: b'.'



   .. py:attribute:: arr_axes
      :value: ''



   .. py:attribute:: mip
      :value: False



   .. py:attribute:: z_index
      :value: 0



   .. py:attribute:: t_index
      :value: 0



   .. py:attribute:: squeeze_singleton
      :value: True



   .. py:method:: align(paths: List[str], nuclei_channel_indices: Optional[List[int]] = None, out_tif: Optional[str] = None, out_png_preview: Optional[str] = None, csv_path: Optional[str] = None) -> Tuple[str, Optional[str], Optional[str]]

      Align every stitched mosaic in ``paths`` to the first and save a CYX stack.

      Alignment uses the specified nuclei channel per image; the resulting
      stack concatenates all input channels in the order given.

      :param paths: stitched TIFFs to align; ``paths[0]`` is the reference.
      :param nuclei_channel_indices: per-image nuclei channel indices; defaults to zeros.
      :param out_tif: output aligned stack path; defaults to ``<outdir>/aligned_allc.tif``.
      :param out_png_preview: optional preview PNG path.
      :param csv_path: manifest CSV path; defaults to ``<outdir>/aligned_manifest.csv``.
      :returns: tuple ``(out_tif, out_png_preview, csv_path)``.



.. py:function:: stitch_cycle_wells(settings)

   Organize a plate of TIFFs into per-well folders and stitch each well.

   Groups images by well from filename metadata, optionally moves or
   symlinks them into per-well folders, then runs :class:`spacrStitcher`
   on each well to produce pairwise CSVs and single- or multi-channel
   mosaics.

   :param settings: dict of preprocess settings; see
       :func:`get_preprocess_ops_settings` for supported keys.
   :returns: dict with keys ``organized`` (move/symlink summary) and
       ``wells`` (per-well output paths).


.. py:function:: get_preprocess_ops_settings(settings)

   Fill in defaults for the ops preprocessing / stitching / alignment pipeline.

   :param settings: user-supplied settings dict, updated in place with defaults.
   :returns: the settings dict with all defaults populated.


.. py:class:: FOVAlignAndCropper(detector: str = 'ORB', nfeatures: int = 6000, max_keypoints: Optional[int] = 2000, downsample: float = 0.5, ransac_thresh_px: float = 3.0, allow_scale: bool = False, allow_rotation: bool = False, outdir: str = './fov_out', opencv_threads: int = 1, arr_axes: str = 'AUTO', mip: bool = False, z_index: int = 0, t_index: int = 0, squeeze_singleton: bool = True, folder_image_scale: float = 1.0)

   Align individual FOVs to a stitched mosaic and export FOV-frame stacks.

   For each FOV, saves a ``.npy`` with shape ``(C_fov + C_mosaic, H_fov, W_fov)``
   (native FOV channels stacked with the mosaic channels warped into the
   FOV frame) and a CSV row with the transform, score and mosaic bbox.

   Delegates feature/matching helpers to :class:`StitchedMultiAligner`.

   :param detector: keypoint detector (``'ORB'`` or ``'SIFT'``). Default ``'ORB'``.
   :param nfeatures: feature budget for the detector.
   :param max_keypoints: hard cap on kept keypoints after detection.
   :param downsample: downsample factor for the feature/score pass.
   :param ransac_thresh_px: RANSAC reprojection threshold (pixels, downsampled space).
   :param allow_scale: allow scale in the estimated affine.
   :param allow_rotation: allow rotation in the estimated affine.
   :param outdir: output directory. Default ``'./fov_out'``.
   :param opencv_threads: OpenCV internal thread cap.
   :param arr_axes: TIFF axis interpretation (``'AUTO'`` or string over ``TCZYX``).
   :param mip: max-project Z when present.
   :param z_index: Z slice to select if ``mip`` is false.
   :param t_index: T slice to select if T is present.
   :param squeeze_singleton: squeeze length-1 axes after slicing.
   :param folder_image_scale: default FOV-to-mosaic pixel-scale factor used by :meth:`run`.


   .. py:attribute:: outdir
      :value: b'.'



   .. py:attribute:: folder_image_scale


   .. py:method:: run(stitched_path: str, folder: str, *, stitched_nuclei_idx: int = 0, fov_nuclei_idx: int = 0, exts: Tuple[str, Ellipsis] = ('.tif', '.tiff'), recursive: bool = False, csv_path: Optional[str] = None, npy_dir: Optional[str] = None, folder_image_scale: Optional[float] = None) -> str

      Align every FOV in ``folder`` to ``stitched_path`` and export FOV-frame stacks.

      Uses the nuclei channel for alignment and writes one ``.npy`` per FOV
      containing the FOV channels plus mosaic channels warped into the
      FOV frame.

      :param stitched_path: path to the reference stitched mosaic.
      :param folder: directory of FOV TIFFs to align.
      :param stitched_nuclei_idx: nuclei channel index in the mosaic. Default ``0``.
      :param fov_nuclei_idx: nuclei channel index in each FOV. Default ``0``.
      :param exts: accepted FOV extensions.
      :param recursive: recurse into subfolders.
      :param csv_path: output manifest CSV path; defaults to ``<outdir>/fov_align_manifest.csv``.
      :param npy_dir: output folder for the per-FOV ``.npy`` stacks; defaults to ``<outdir>/npy``.
      :param folder_image_scale: FOV-to-mosaic pixel-scale factor; falls back to ``self.folder_image_scale``.
      :returns: path to the manifest CSV.



.. py:function:: align_image_to_stitch(stitch_dst_root: str, align_src: str, *, meta_regex: str = '(?P<mag>\\d+X)_c(?P<chan>\\d+)_?(?P<well>[A-H]\\d{1,2}).*?Site[-_](?P<site>\\d+)\\.(?:tif|tiff)$', well_group: str = 'well', channel_index: int = 0, relative_scale: float = 2.0, downsample: float = 0.5, nfeatures: int = 4000, ransac_thresh_px: float = 3.0, allow_scale: bool = False, allow_rotation: bool = False, qc_outlines: bool = False, recursive_align_src: bool = True, exts: tuple = ('.tif', '.tiff')) -> Dict[str, Dict[str, str]]

   Align each well's higher-mag FOVs to that well's stitched mosaic.

   For every well with a mosaic at
   ``<stitch_dst_root>/<WELL>/_stitch/mosaic_allc.tif``, groups images
   from ``align_src`` by well and runs :class:`FOVAlignAndCropper` to
   produce per-well crop manifests and ``.npy`` crops.

   :param stitch_dst_root: root folder containing per-well stitched mosaics.
   :param align_src: folder of higher-magnification images to align.
   :param meta_regex: regex extracting well/site metadata from filenames.
   :param well_group: named capture group holding the well identifier.
   :param channel_index: nuclei channel index. Default ``0``.
   :param relative_scale: FOV-to-mosaic pixel-scale factor. Default ``2.0``.
   :param downsample: feature-pass downsample factor. Default ``0.5``.
   :param nfeatures: keypoint budget for the detector.
   :param ransac_thresh_px: RANSAC reprojection threshold in pixels.
   :param allow_scale: allow scale in the estimated affine.
   :param allow_rotation: allow rotation in the estimated affine.
   :param qc_outlines: emit QC outline overlays.
   :param recursive_align_src: recurse when scanning ``align_src``.
   :param exts: accepted image extensions.
   :returns: mapping ``{well: {'mosaic': path, 'align_folder': path, 'manifest_csv': path}}``.


.. py:function:: ops_preprocess(settings)

   Run the full ops preprocessing pipeline: per-genotype stitching + phenotype alignment.

   Iterates over one or more genotype folders, calls
   :func:`stitch_cycle_wells` on each, then aligns the phenotype images
   to the resulting mosaics via :func:`align_image_to_stitch`.

   :param settings: dict of preprocessing settings; see
       :func:`get_preprocess_ops_settings` for supported keys.
   :returns: dict with ``stitch`` (per-genotype summaries), ``align``
       (per-genotype align results) and ``npy_out_root`` (npy output root path).


