thyra 4.3.3__tar.gz → 4.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thyra-4.3.3 → thyra-4.3.4}/.gitignore +2 -0
- {thyra-4.3.3 → thyra-4.3.4}/PKG-INFO +1 -1
- {thyra-4.3.3 → thyra-4.3.4}/pyproject.toml +1 -1
- {thyra-4.3.3 → thyra-4.3.4}/thyra/__init__.py +1 -1
- {thyra-4.3.3 → thyra-4.3.4}/thyra/alignment/teaching_points.py +434 -4
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/base_spatialdata_converter.py +15 -1
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/optical_image.py +69 -40
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/root_attrs.py +3 -1
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/mis_parser.py +42 -10
- {thyra-4.3.3 → thyra-4.3.4}/LICENSE +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/README.md +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/__main__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/alignment/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/alignment/affine.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/config.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/convert.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/base_converter.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/base_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/base_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/conversion_state.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/drop_tally.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/frames.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/mass_axis.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/mobility.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/msms.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/core/registry.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/errors.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/constants.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/document.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/imzml_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/mzpeak_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/msi_metadata_schema_v0_10.json +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/types.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/metadata/validator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/preview.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/py.typed +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/solarix/solarix_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/mzpeak/chunk_decoding.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/mzpeak/mzpeak_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/phi/phi_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/binning.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/constants.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/gaps.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/tic.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/resampling/types.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/tools/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/__init__.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/imzml_coordinate_base.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/logging_config.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.3.3 → thyra-4.3.4}/thyra/utils/windows_paths.py +0 -0
|
@@ -56,6 +56,8 @@ Archive/
|
|
|
56
56
|
tests/data/fixtures/.synthetic_tims-build-*/
|
|
57
57
|
tests/data/fixtures/.synthetic_tims-old-*/
|
|
58
58
|
*.mis
|
|
59
|
+
# ...except two public flexImaging sequence files the alignment tests read.
|
|
60
|
+
!tests/data/fixtures/fleximaging_msv000088438/*.mis
|
|
59
61
|
*.slx
|
|
60
62
|
*.tiff
|
|
61
63
|
*.ipynb
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thyra
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.4
|
|
4
4
|
Summary: A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics
|
|
5
5
|
Project-URL: Homepage, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
6
6
|
Project-URL: Repository, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "4.3.
|
|
7
|
+
version = "4.3.4"
|
|
8
8
|
description = "A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics"
|
|
9
9
|
authors = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
10
10
|
maintainers = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
@@ -207,6 +207,320 @@ class RegionMapping:
|
|
|
207
207
|
return self._get_pixel_scale_x() / 2, self._get_pixel_scale_y() / 2
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
#: Raster axis directions tried against the teaching-point frame, in order of
|
|
211
|
+
#: preference. FlexImaging's teaching frame has y pointing up the image while
|
|
212
|
+
#: raster Y counts down it, so (+1, -1) is what every measured run uses.
|
|
213
|
+
_LATTICE_AXIS_SIGNS: Tuple[Tuple[int, int], ...] = ((1, -1), (1, 1), (-1, -1), (-1, 1))
|
|
214
|
+
|
|
215
|
+
#: A spot this close to its Area's outline, in raster steps, still counts as
|
|
216
|
+
#: inside it. FlexImaging draws rectangles whose edges pass through the
|
|
217
|
+
#: outermost spots, so an exact test would reject them.
|
|
218
|
+
_AREA_EDGE_TOLERANCE_STEPS = 0.1
|
|
219
|
+
|
|
220
|
+
#: How far around the estimated reference node the integer search looks.
|
|
221
|
+
_NODE_SEARCH_RADIUS = 3
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class LatticeFit:
|
|
226
|
+
"""Where FlexImaging put each raster spot on its alignment image (D31).
|
|
227
|
+
|
|
228
|
+
FlexImaging places every spot of a run on one lattice: one raster step
|
|
229
|
+
apart in the teaching-point frame, through the ``.mis`` reference
|
|
230
|
+
point. An Area outline only selects which nodes are measured, so the
|
|
231
|
+
outline's size says nothing about the pitch.
|
|
232
|
+
|
|
233
|
+
Attributes:
|
|
234
|
+
cell_to_image: 3x3 affine from the TIC image's own coordinates to
|
|
235
|
+
alignment-image pixels. Cell ``(i, j)`` spans ``[i, i + 1) x
|
|
236
|
+
[j, j + 1)``, the SpatialData convention, so its centre
|
|
237
|
+
``(i + 0.5, j + 0.5)`` lands on the spot at raster
|
|
238
|
+
``(first_raster_x + i, first_raster_y + j)``.
|
|
239
|
+
reference_node: Raster ``(X, Y)`` of the node on the reference point.
|
|
240
|
+
axis_signs: Direction of raster X and Y in the teaching frame.
|
|
241
|
+
raster_step_um: ``(x, y)`` lattice step in micrometres.
|
|
242
|
+
spots_outside: Measured spots that fall outside their own Area.
|
|
243
|
+
nodes_unmeasured: Lattice nodes inside an Area that were not measured.
|
|
244
|
+
reference_from: ``"ReferencePoint"``, or ``"first teaching point"``
|
|
245
|
+
when the ``.mis`` has none.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
cell_to_image: np.ndarray
|
|
249
|
+
reference_node: Tuple[int, int]
|
|
250
|
+
axis_signs: Tuple[int, int]
|
|
251
|
+
raster_step_um: Tuple[float, float]
|
|
252
|
+
spots_outside: int
|
|
253
|
+
nodes_unmeasured: int
|
|
254
|
+
reference_from: str
|
|
255
|
+
|
|
256
|
+
def cell_centre(self, norm_x: float, norm_y: float) -> Tuple[float, float]:
|
|
257
|
+
"""Image pixel of the spot in TIC cell ``(norm_x, norm_y)``."""
|
|
258
|
+
x, y, _ = self.cell_to_image @ np.array([norm_x + 0.5, norm_y + 0.5, 1.0])
|
|
259
|
+
return float(x), float(y)
|
|
260
|
+
|
|
261
|
+
def cell_corners(self, norm_x: float, norm_y: float) -> List[Tuple[float, float]]:
|
|
262
|
+
"""The four image-pixel corners of TIC cell ``(norm_x, norm_y)``."""
|
|
263
|
+
corners = np.array(
|
|
264
|
+
[
|
|
265
|
+
[norm_x, norm_y, 1.0],
|
|
266
|
+
[norm_x + 1, norm_y, 1.0],
|
|
267
|
+
[norm_x + 1, norm_y + 1, 1.0],
|
|
268
|
+
[norm_x, norm_y + 1, 1.0],
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
out = corners @ self.cell_to_image.T
|
|
272
|
+
return [(float(x), float(y)) for x, y, _ in out]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _area_outline(area: Dict[str, Any]) -> Optional[List[Tuple[float, float]]]:
|
|
276
|
+
"""An Area's outline in image pixels: its polygon, or its rectangle."""
|
|
277
|
+
points = area.get("points")
|
|
278
|
+
if points and len(points) >= 3:
|
|
279
|
+
return [(float(x), float(y)) for x, y in points]
|
|
280
|
+
corners = points if points and len(points) == 2 else None
|
|
281
|
+
if corners is None and "p1" in area and "p2" in area:
|
|
282
|
+
corners = [area["p1"], area["p2"]]
|
|
283
|
+
if corners is None:
|
|
284
|
+
return None
|
|
285
|
+
(x0, y0), (x1, y1) = corners
|
|
286
|
+
return [
|
|
287
|
+
(float(x0), float(y0)),
|
|
288
|
+
(float(x1), float(y0)),
|
|
289
|
+
(float(x1), float(y1)),
|
|
290
|
+
(float(x0), float(y1)),
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class _LatticeSearch:
|
|
295
|
+
"""The whole-node search behind :func:`fit_raster_lattice`.
|
|
296
|
+
|
|
297
|
+
Holds the teaching-point affine, the reference point in stage
|
|
298
|
+
micrometres, the measured spots and each measured region's Area, and
|
|
299
|
+
scores a candidate ``(node, axis signs)`` by how many spots fall outside
|
|
300
|
+
their Area and how many Area nodes went unmeasured.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
to_stage: np.ndarray,
|
|
306
|
+
ref: np.ndarray,
|
|
307
|
+
step: Tuple[float, float],
|
|
308
|
+
positions: List[Dict[str, Any]],
|
|
309
|
+
areas: List[Dict[str, Any]],
|
|
310
|
+
tolerance_px: float,
|
|
311
|
+
) -> None:
|
|
312
|
+
from shapely.geometry import Polygon
|
|
313
|
+
|
|
314
|
+
self.to_stage = to_stage
|
|
315
|
+
self.to_image = np.linalg.inv(to_stage)
|
|
316
|
+
self.ref = ref
|
|
317
|
+
self.step_x, self.step_y = step
|
|
318
|
+
self.raster_x = np.array([p["raster_x"] for p in positions], dtype=np.float64)
|
|
319
|
+
self.raster_y = np.array([p["raster_y"] for p in positions], dtype=np.float64)
|
|
320
|
+
self.regions = np.array([p["region"] for p in positions], dtype=np.int64)
|
|
321
|
+
# Each measured region's Area as drawn, and padded by the edge
|
|
322
|
+
# tolerance for the inside test.
|
|
323
|
+
self.exact: Dict[int, Any] = {}
|
|
324
|
+
self.padded: Dict[int, Any] = {}
|
|
325
|
+
for region in np.unique(self.regions).tolist():
|
|
326
|
+
if 0 <= region < len(areas):
|
|
327
|
+
vertices = _area_outline(areas[region])
|
|
328
|
+
if vertices is not None:
|
|
329
|
+
self.exact[region] = Polygon(vertices)
|
|
330
|
+
self.padded[region] = self.exact[region].buffer(tolerance_px)
|
|
331
|
+
|
|
332
|
+
def to_px(
|
|
333
|
+
self, node: Tuple[int, int], signs: Tuple[int, int], xs: Any, ys: Any
|
|
334
|
+
) -> Tuple[Any, Any]:
|
|
335
|
+
"""Image pixels of raster nodes ``(xs, ys)`` on the lattice."""
|
|
336
|
+
stage_x = self.ref[0] + signs[0] * self.step_x * (xs - node[0])
|
|
337
|
+
stage_y = self.ref[1] + signs[1] * self.step_y * (ys - node[1])
|
|
338
|
+
m = self.to_image
|
|
339
|
+
return (
|
|
340
|
+
m[0, 0] * stage_x + m[0, 1] * stage_y + m[0, 2],
|
|
341
|
+
m[1, 0] * stage_x + m[1, 1] * stage_y + m[1, 2],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def outside(self, node: Tuple[int, int], signs: Tuple[int, int]) -> int:
|
|
345
|
+
"""Measured spots that fall outside their own (padded) Area."""
|
|
346
|
+
import shapely
|
|
347
|
+
|
|
348
|
+
count = 0
|
|
349
|
+
for region, polygon in self.padded.items():
|
|
350
|
+
sel = self.regions == region
|
|
351
|
+
px, py = self.to_px(node, signs, self.raster_x[sel], self.raster_y[sel])
|
|
352
|
+
count += int((~shapely.contains_xy(polygon, px, py)).sum())
|
|
353
|
+
return count
|
|
354
|
+
|
|
355
|
+
def unmeasured(self, node: Tuple[int, int], signs: Tuple[int, int]) -> int:
|
|
356
|
+
"""Lattice nodes inside an Area as drawn that no spot was measured on.
|
|
357
|
+
|
|
358
|
+
Against the outline as drawn: FlexImaging measures the nodes inside
|
|
359
|
+
it, and the edge tolerance would count nodes it never meant to.
|
|
360
|
+
"""
|
|
361
|
+
import shapely
|
|
362
|
+
|
|
363
|
+
count = 0
|
|
364
|
+
for region, polygon in self.exact.items():
|
|
365
|
+
sel = self.regions == region
|
|
366
|
+
measured = set(
|
|
367
|
+
zip(self.raster_x[sel].astype(int), self.raster_y[sel].astype(int))
|
|
368
|
+
)
|
|
369
|
+
# The Area's outline in raster coordinates bounds the nodes to try.
|
|
370
|
+
verts = np.array(polygon.exterior.coords)
|
|
371
|
+
stage = verts @ self.to_stage[:2, :2].T + self.to_stage[:2, 2]
|
|
372
|
+
gx = node[0] + (stage[:, 0] - self.ref[0]) / (signs[0] * self.step_x)
|
|
373
|
+
gy = node[1] + (stage[:, 1] - self.ref[1]) / (signs[1] * self.step_y)
|
|
374
|
+
xs, ys = np.meshgrid(
|
|
375
|
+
np.arange(np.floor(gx.min()), np.ceil(gx.max()) + 1),
|
|
376
|
+
np.arange(np.floor(gy.min()), np.ceil(gy.max()) + 1),
|
|
377
|
+
)
|
|
378
|
+
xs, ys = xs.ravel(), ys.ravel()
|
|
379
|
+
px, py = self.to_px(node, signs, xs, ys)
|
|
380
|
+
inside = shapely.contains_xy(polygon, px, py)
|
|
381
|
+
nodes = zip(xs[inside].astype(int), ys[inside].astype(int))
|
|
382
|
+
count += sum(1 for xy in nodes if xy not in measured)
|
|
383
|
+
return count
|
|
384
|
+
|
|
385
|
+
def candidates(self, signs: Tuple[int, int]) -> List[Tuple[int, int]]:
|
|
386
|
+
"""Nodes near where each region's centre puts the reference node."""
|
|
387
|
+
guesses = set()
|
|
388
|
+
for region, polygon in self.padded.items():
|
|
389
|
+
sel = self.regions == region
|
|
390
|
+
centre = self.to_stage @ np.array(
|
|
391
|
+
[polygon.centroid.x, polygon.centroid.y, 1.0]
|
|
392
|
+
)
|
|
393
|
+
gx = self.raster_x[sel].mean() - (centre[0] - self.ref[0]) / (
|
|
394
|
+
signs[0] * self.step_x
|
|
395
|
+
)
|
|
396
|
+
gy = self.raster_y[sel].mean() - (centre[1] - self.ref[1]) / (
|
|
397
|
+
signs[1] * self.step_y
|
|
398
|
+
)
|
|
399
|
+
guesses.add((int(round(gx)), int(round(gy))))
|
|
400
|
+
span = range(-_NODE_SEARCH_RADIUS, _NODE_SEARCH_RADIUS + 1)
|
|
401
|
+
return sorted(
|
|
402
|
+
{(gx + dx, gy + dy) for gx, gy in guesses for dx in span for dy in span}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def best(self) -> Tuple[int, int, Tuple[int, int], Tuple[int, int]]:
|
|
406
|
+
"""``(spots outside, unmeasured nodes, node, signs)`` of the best fit.
|
|
407
|
+
|
|
408
|
+
Fewest spots outside first, then fewest unmeasured nodes, then the
|
|
409
|
+
usual axis directions.
|
|
410
|
+
"""
|
|
411
|
+
scored = [
|
|
412
|
+
(self.outside(node, signs), preference, node, signs)
|
|
413
|
+
for preference, signs in enumerate(_LATTICE_AXIS_SIGNS)
|
|
414
|
+
for node in self.candidates(signs)
|
|
415
|
+
]
|
|
416
|
+
fewest = min(s[0] for s in scored)
|
|
417
|
+
n_unmeasured, _, node, signs, n_outside = min(
|
|
418
|
+
(self.unmeasured(node, signs), preference, node, signs, n_out)
|
|
419
|
+
for n_out, preference, node, signs in scored
|
|
420
|
+
if n_out == fewest
|
|
421
|
+
)
|
|
422
|
+
return n_outside, n_unmeasured, node, signs
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def fit_raster_lattice(
|
|
426
|
+
teaching_points: List[Dict[str, Any]],
|
|
427
|
+
reference_point: Optional[Tuple[float, float]],
|
|
428
|
+
raster_step: Tuple[float, float],
|
|
429
|
+
areas: List[Dict[str, Any]],
|
|
430
|
+
positions: List[Dict[str, Any]],
|
|
431
|
+
first_raster_x: int,
|
|
432
|
+
first_raster_y: int,
|
|
433
|
+
) -> Optional[LatticeFit]:
|
|
434
|
+
"""Place the run's raster on the alignment image the way FlexImaging does.
|
|
435
|
+
|
|
436
|
+
The teaching points give the image-to-stage affine. The lattice runs
|
|
437
|
+
through the reference point with the raster step. Which raster node
|
|
438
|
+
sits on the reference point is a whole-number question: it is the node
|
|
439
|
+
that puts every measured spot inside its own Area, and of those the one
|
|
440
|
+
that leaves the fewest unmeasured nodes inside the Areas. On the public
|
|
441
|
+
MassIVE MSV000088438 runs this reproduces flexImaging's own spot list
|
|
442
|
+
within 1 um.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
teaching_points: ``.mis`` teaching points, ``{"image", "stage"}``.
|
|
446
|
+
reference_point: ``.mis`` ``<ReferencePoint>`` in image pixels, or
|
|
447
|
+
``None`` to use the first teaching point (FlexImaging puts it
|
|
448
|
+
there in every file seen).
|
|
449
|
+
raster_step: ``(x, y)`` raster step in micrometres.
|
|
450
|
+
areas: ``.mis`` Areas, in file order. Region ``N`` is Area ``N``.
|
|
451
|
+
positions: Measured spots, ``{"raster_x", "raster_y", "region"}``.
|
|
452
|
+
first_raster_x: Raster X of TIC cell 0.
|
|
453
|
+
first_raster_y: Raster Y of TIC cell 0.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
The fit, or ``None`` when the inputs cannot place a lattice: fewer
|
|
457
|
+
than three teaching points, no positive step, no Area that any
|
|
458
|
+
measured region maps to.
|
|
459
|
+
"""
|
|
460
|
+
step_x, step_y = (float(raster_step[0]), float(raster_step[1]))
|
|
461
|
+
if len(teaching_points) < 3 or step_x <= 0 or step_y <= 0 or not positions:
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
points = [TeachingPoint.from_dict(tp) for tp in teaching_points]
|
|
465
|
+
image_to_stage = AffineTransform.from_points(
|
|
466
|
+
[(float(p.image_x), float(p.image_y)) for p in points],
|
|
467
|
+
[(float(p.stage_x), float(p.stage_y)) for p in points],
|
|
468
|
+
)
|
|
469
|
+
if reference_point is None:
|
|
470
|
+
reference_point = (float(points[0].image_x), float(points[0].image_y))
|
|
471
|
+
reference_from = "first teaching point"
|
|
472
|
+
else:
|
|
473
|
+
reference_from = "ReferencePoint"
|
|
474
|
+
to_stage = image_to_stage.matrix
|
|
475
|
+
ref = to_stage @ np.array([reference_point[0], reference_point[1], 1.0])
|
|
476
|
+
|
|
477
|
+
# The edge tolerance in image pixels: steps times pixels per micrometre.
|
|
478
|
+
um_per_px = 0.5 * (image_to_stage.scale_x + image_to_stage.scale_y)
|
|
479
|
+
tolerance_px = _AREA_EDGE_TOLERANCE_STEPS * min(step_x, step_y) / um_per_px
|
|
480
|
+
search = _LatticeSearch(
|
|
481
|
+
to_stage, ref, (step_x, step_y), positions, areas, tolerance_px
|
|
482
|
+
)
|
|
483
|
+
if not search.padded:
|
|
484
|
+
return None
|
|
485
|
+
n_outside, n_unmeasured, node, signs = search.best()
|
|
486
|
+
|
|
487
|
+
sx, sy = signs
|
|
488
|
+
lattice = np.array(
|
|
489
|
+
[
|
|
490
|
+
[sx * step_x, 0.0, ref[0] + sx * step_x * (first_raster_x - node[0] - 0.5)],
|
|
491
|
+
[0.0, sy * step_y, ref[1] + sy * step_y * (first_raster_y - node[1] - 0.5)],
|
|
492
|
+
[0.0, 0.0, 1.0],
|
|
493
|
+
]
|
|
494
|
+
)
|
|
495
|
+
fit = LatticeFit(
|
|
496
|
+
cell_to_image=search.to_image @ lattice,
|
|
497
|
+
reference_node=(int(node[0]), int(node[1])),
|
|
498
|
+
axis_signs=(int(sx), int(sy)),
|
|
499
|
+
raster_step_um=(step_x, step_y),
|
|
500
|
+
spots_outside=int(n_outside),
|
|
501
|
+
nodes_unmeasured=int(n_unmeasured),
|
|
502
|
+
reference_from=reference_from,
|
|
503
|
+
)
|
|
504
|
+
logger.info(
|
|
505
|
+
f"FlexImaging lattice: reference node {fit.reference_node} "
|
|
506
|
+
f"(from {reference_from}), step {step_x:g} x {step_y:g} um, "
|
|
507
|
+
f"axes {fit.axis_signs}, {n_outside} of {len(positions)} spots outside "
|
|
508
|
+
f"their Area, {n_unmeasured} unmeasured nodes inside the Areas"
|
|
509
|
+
)
|
|
510
|
+
if n_outside:
|
|
511
|
+
logger.warning(
|
|
512
|
+
f"{n_outside} of {len(positions)} measured spots fall outside their "
|
|
513
|
+
f".mis Area on the best lattice; check that the .mis belongs to this "
|
|
514
|
+
f"run. The optical alignment may be off by whole raster steps."
|
|
515
|
+
)
|
|
516
|
+
if signs != _LATTICE_AXIS_SIGNS[0]:
|
|
517
|
+
logger.warning(
|
|
518
|
+
f"FlexImaging raster axes {signs} differ from the usual (1, -1); "
|
|
519
|
+
"the optical alignment follows the Areas."
|
|
520
|
+
)
|
|
521
|
+
return fit
|
|
522
|
+
|
|
523
|
+
|
|
210
524
|
@dataclass
|
|
211
525
|
class AreaAlignmentResult:
|
|
212
526
|
"""Result of area-based alignment computation.
|
|
@@ -219,12 +533,21 @@ class AreaAlignmentResult:
|
|
|
219
533
|
first_raster_x: First raster X offset from header
|
|
220
534
|
first_raster_y: First raster Y offset from header
|
|
221
535
|
pos_to_region: Mapping from (raster_x, raster_y) to region_id
|
|
536
|
+
lattice: The teaching-point lattice (D31). ``None`` only when the
|
|
537
|
+
``.mis`` lacks what it needs, and then each region is stretched
|
|
538
|
+
over its Area's bounding box instead, which is approximate.
|
|
222
539
|
"""
|
|
223
540
|
|
|
224
541
|
region_mappings: List[RegionMapping]
|
|
225
542
|
first_raster_x: int
|
|
226
543
|
first_raster_y: int
|
|
227
544
|
pos_to_region: Dict[Tuple[int, int], int] = field(default_factory=dict)
|
|
545
|
+
lattice: Optional[LatticeFit] = None
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def cell_to_image(self) -> Optional[np.ndarray]:
|
|
549
|
+
"""The lattice's TIC-cell-to-image affine, or ``None`` without one."""
|
|
550
|
+
return self.lattice.cell_to_image if self.lattice is not None else None
|
|
228
551
|
|
|
229
552
|
def transform_point(
|
|
230
553
|
self, norm_x: int, norm_y: int
|
|
@@ -247,6 +570,9 @@ class AreaAlignmentResult:
|
|
|
247
570
|
if region_id is None:
|
|
248
571
|
return None
|
|
249
572
|
|
|
573
|
+
if self.lattice is not None:
|
|
574
|
+
return self.lattice.cell_centre(norm_x, norm_y)
|
|
575
|
+
|
|
250
576
|
# Find the region mapping
|
|
251
577
|
for mapping in self.region_mappings:
|
|
252
578
|
if mapping.region_id == region_id:
|
|
@@ -254,6 +580,33 @@ class AreaAlignmentResult:
|
|
|
254
580
|
|
|
255
581
|
return None
|
|
256
582
|
|
|
583
|
+
def cell_corners(
|
|
584
|
+
self, norm_x: int, norm_y: int
|
|
585
|
+
) -> Optional[List[Tuple[float, float]]]:
|
|
586
|
+
"""The image-pixel outline of a measured pixel, or ``None``.
|
|
587
|
+
|
|
588
|
+
Under the lattice this is the exact cell the TIC image draws, a
|
|
589
|
+
parallelogram when the photo is rotated against the stage. Without
|
|
590
|
+
one it is the axis-aligned box of the bounding-box stretch.
|
|
591
|
+
"""
|
|
592
|
+
if (norm_x + self.first_raster_x, norm_y + self.first_raster_y) not in (
|
|
593
|
+
self.pos_to_region
|
|
594
|
+
):
|
|
595
|
+
return None
|
|
596
|
+
if self.lattice is not None:
|
|
597
|
+
return self.lattice.cell_corners(norm_x, norm_y)
|
|
598
|
+
centre = self.transform_point(norm_x, norm_y)
|
|
599
|
+
half = self.get_half_pixel_size(norm_x, norm_y)
|
|
600
|
+
if centre is None or half is None:
|
|
601
|
+
return None
|
|
602
|
+
(cx, cy), (hx, hy) = centre, half
|
|
603
|
+
return [
|
|
604
|
+
(cx - hx, cy - hy),
|
|
605
|
+
(cx + hx, cy - hy),
|
|
606
|
+
(cx + hx, cy + hy),
|
|
607
|
+
(cx - hx, cy + hy),
|
|
608
|
+
]
|
|
609
|
+
|
|
257
610
|
def get_half_pixel_size(
|
|
258
611
|
self, norm_x: int, norm_y: int
|
|
259
612
|
) -> Optional[Tuple[float, float]]:
|
|
@@ -280,6 +633,13 @@ class AreaAlignmentResult:
|
|
|
280
633
|
if region_id is None:
|
|
281
634
|
return None
|
|
282
635
|
|
|
636
|
+
if self.lattice is not None:
|
|
637
|
+
m = self.lattice.cell_to_image
|
|
638
|
+
return (
|
|
639
|
+
float(np.hypot(m[0, 0], m[1, 0])) / 2.0,
|
|
640
|
+
float(np.hypot(m[0, 1], m[1, 1])) / 2.0,
|
|
641
|
+
)
|
|
642
|
+
|
|
283
643
|
# Find the region mapping and get its half-pixel size
|
|
284
644
|
for mapping in self.region_mappings:
|
|
285
645
|
if mapping.region_id == region_id:
|
|
@@ -685,19 +1045,29 @@ class TeachingPointAlignment:
|
|
|
685
1045
|
poslog_positions: List[Dict[str, Any]],
|
|
686
1046
|
first_raster_x: int,
|
|
687
1047
|
first_raster_y: int,
|
|
1048
|
+
teaching_points: Optional[List[Dict[str, Any]]] = None,
|
|
1049
|
+
reference_point: Optional[Tuple[float, float]] = None,
|
|
1050
|
+
raster_step: Optional[Tuple[float, float]] = None,
|
|
688
1051
|
) -> AreaAlignmentResult:
|
|
689
1052
|
"""Compute alignment using Area definitions from .mis file.
|
|
690
1053
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1054
|
+
With three teaching points and a raster step, every spot is placed
|
|
1055
|
+
on FlexImaging's lattice (:func:`fit_raster_lattice`, D31). Without
|
|
1056
|
+
them, each region is stretched over its Area's bounding box, which
|
|
1057
|
+
is approximate: the outline is drawn by hand and is not the extent
|
|
1058
|
+
of the spots.
|
|
694
1059
|
|
|
695
1060
|
Args:
|
|
696
1061
|
areas: List of Area dictionaries with 'name', 'p1', 'p2' keys
|
|
697
|
-
where p1 and p2 are (x, y) tuples of image pixel coordinates
|
|
1062
|
+
where p1 and p2 are (x, y) tuples of image pixel coordinates,
|
|
1063
|
+
and optionally 'points', 'type' and 'raster'
|
|
698
1064
|
poslog_positions: List of position dictionaries from poslog parsing
|
|
699
1065
|
first_raster_x: First raster X offset from header
|
|
700
1066
|
first_raster_y: First raster Y offset from header
|
|
1067
|
+
teaching_points: ``.mis`` teaching points
|
|
1068
|
+
reference_point: ``.mis`` ``<ReferencePoint>`` in image pixels
|
|
1069
|
+
raster_step: Raster step in micrometres, used when the Areas
|
|
1070
|
+
state none of their own
|
|
701
1071
|
|
|
702
1072
|
Returns:
|
|
703
1073
|
AreaAlignmentResult with region mappings and coordinate transform
|
|
@@ -773,9 +1143,69 @@ class TeachingPointAlignment:
|
|
|
773
1143
|
f"({mapping.image_max_x}, {mapping.image_max_y})"
|
|
774
1144
|
)
|
|
775
1145
|
|
|
1146
|
+
lattice = self._fit_lattice(
|
|
1147
|
+
areas,
|
|
1148
|
+
poslog_positions,
|
|
1149
|
+
regions,
|
|
1150
|
+
first_raster_x,
|
|
1151
|
+
first_raster_y,
|
|
1152
|
+
teaching_points,
|
|
1153
|
+
reference_point,
|
|
1154
|
+
raster_step,
|
|
1155
|
+
)
|
|
1156
|
+
|
|
776
1157
|
return AreaAlignmentResult(
|
|
777
1158
|
region_mappings=region_mappings,
|
|
778
1159
|
first_raster_x=first_raster_x,
|
|
779
1160
|
first_raster_y=first_raster_y,
|
|
780
1161
|
pos_to_region=pos_to_region,
|
|
1162
|
+
lattice=lattice,
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
@staticmethod
|
|
1166
|
+
def _fit_lattice(
|
|
1167
|
+
areas: List[Dict[str, Any]],
|
|
1168
|
+
positions: List[Dict[str, Any]],
|
|
1169
|
+
regions: Dict[int, Dict[str, Any]],
|
|
1170
|
+
first_raster_x: int,
|
|
1171
|
+
first_raster_y: int,
|
|
1172
|
+
teaching_points: Optional[List[Dict[str, Any]]],
|
|
1173
|
+
reference_point: Optional[Tuple[float, float]],
|
|
1174
|
+
raster_step: Optional[Tuple[float, float]],
|
|
1175
|
+
) -> Optional[LatticeFit]:
|
|
1176
|
+
"""The lattice fit, or ``None`` (with the reason logged) to fall back."""
|
|
1177
|
+
if not teaching_points or len(teaching_points) < 3:
|
|
1178
|
+
logger.warning(
|
|
1179
|
+
"The .mis has fewer than three teaching points; optical "
|
|
1180
|
+
"alignment stretches each region over its Area outline, which "
|
|
1181
|
+
"can be off by up to a raster step."
|
|
1182
|
+
)
|
|
1183
|
+
return None
|
|
1184
|
+
steps = {
|
|
1185
|
+
tuple(float(v) for v in areas[r]["raster"])
|
|
1186
|
+
for r in regions
|
|
1187
|
+
if 0 <= r < len(areas) and areas[r].get("raster")
|
|
1188
|
+
}
|
|
1189
|
+
if len(steps) > 1:
|
|
1190
|
+
logger.warning(
|
|
1191
|
+
f"The measured Areas use different raster steps {sorted(steps)}; "
|
|
1192
|
+
"one lattice cannot place them, so optical alignment stretches "
|
|
1193
|
+
"each region over its Area outline instead."
|
|
1194
|
+
)
|
|
1195
|
+
return None
|
|
1196
|
+
step = next(iter(steps)) if steps else raster_step
|
|
1197
|
+
if step is None:
|
|
1198
|
+
logger.warning(
|
|
1199
|
+
"No raster step for the optical alignment lattice; it stretches "
|
|
1200
|
+
"each region over its Area outline instead."
|
|
1201
|
+
)
|
|
1202
|
+
return None
|
|
1203
|
+
return fit_raster_lattice(
|
|
1204
|
+
teaching_points=teaching_points,
|
|
1205
|
+
reference_point=reference_point,
|
|
1206
|
+
raster_step=(float(step[0]), float(step[1])),
|
|
1207
|
+
areas=areas,
|
|
1208
|
+
positions=positions,
|
|
1209
|
+
first_raster_x=first_raster_x,
|
|
1210
|
+
first_raster_y=first_raster_y,
|
|
781
1211
|
)
|
|
@@ -53,7 +53,7 @@ import pandas as pd
|
|
|
53
53
|
import zarr
|
|
54
54
|
from anndata import AnnData
|
|
55
55
|
from numpy.typing import NDArray
|
|
56
|
-
from shapely.geometry import box
|
|
56
|
+
from shapely.geometry import Polygon, box
|
|
57
57
|
from spatialdata import SpatialData
|
|
58
58
|
from spatialdata.models import ShapesModel
|
|
59
59
|
from spatialdata.transformations import Identity
|
|
@@ -984,9 +984,23 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
984
984
|
rm = self.optical.alignment.region_mappings[0]
|
|
985
985
|
default_half_pixel = rm.get_half_pixel_size()
|
|
986
986
|
|
|
987
|
+
# Under the teaching-point lattice (D31) a pixel's polygon is the
|
|
988
|
+
# exact cell the TIC image draws, through the same matrix, so the
|
|
989
|
+
# two cannot disagree; it is a parallelogram when the photo is
|
|
990
|
+
# rotated against the stage.
|
|
991
|
+
cell_corners = getattr(self.optical.alignment, "cell_corners", None)
|
|
992
|
+
lattice = getattr(self.optical.alignment, "lattice", None)
|
|
993
|
+
|
|
987
994
|
valid_indices = []
|
|
988
995
|
for i in range(len(adata)):
|
|
989
996
|
rx, ry = int(raster_x[i]), int(raster_y[i])
|
|
997
|
+
if lattice is not None and cell_corners is not None:
|
|
998
|
+
corners = cell_corners(rx, ry)
|
|
999
|
+
if corners is not None:
|
|
1000
|
+
geometries.append(Polygon(corners))
|
|
1001
|
+
valid_indices.append(i)
|
|
1002
|
+
continue
|
|
1003
|
+
|
|
990
1004
|
img_coords = self.optical.alignment.transform_point(rx, ry)
|
|
991
1005
|
|
|
992
1006
|
if img_coords is not None:
|