smappy-smlm 0.1.0__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.
Files changed (78) hide show
  1. smappy_smlm-0.1.0/LICENSE +29 -0
  2. smappy_smlm-0.1.0/MANIFEST.in +8 -0
  3. smappy_smlm-0.1.0/PKG-INFO +361 -0
  4. smappy_smlm-0.1.0/README.md +334 -0
  5. smappy_smlm-0.1.0/THIRD_PARTY_NOTICES.md +41 -0
  6. smappy_smlm-0.1.0/ci/smoke.py +51 -0
  7. smappy_smlm-0.1.0/csrc/filters.hpp +163 -0
  8. smappy_smlm-0.1.0/csrc/fit.cpp +239 -0
  9. smappy_smlm-0.1.0/csrc/group.cpp +41 -0
  10. smappy_smlm-0.1.0/csrc/group.hpp +75 -0
  11. smappy_smlm-0.1.0/csrc/linalg.hpp +99 -0
  12. smappy_smlm-0.1.0/csrc/lm.hpp +175 -0
  13. smappy_smlm-0.1.0/csrc/maxima.hpp +41 -0
  14. smappy_smlm-0.1.0/csrc/models.hpp +266 -0
  15. smappy_smlm-0.1.0/csrc/parallel.hpp +45 -0
  16. smappy_smlm-0.1.0/csrc/render.cpp +100 -0
  17. smappy_smlm-0.1.0/csrc/render.hpp +173 -0
  18. smappy_smlm-0.1.0/examples/camera_evolve512.yaml +13 -0
  19. smappy_smlm-0.1.0/pyproject.toml +67 -0
  20. smappy_smlm-0.1.0/setup.cfg +4 -0
  21. smappy_smlm-0.1.0/setup.py +114 -0
  22. smappy_smlm-0.1.0/src/smappy/__init__.py +81 -0
  23. smappy_smlm-0.1.0/src/smappy/api.py +183 -0
  24. smappy_smlm-0.1.0/src/smappy/camera.py +24 -0
  25. smappy_smlm-0.1.0/src/smappy/cli/__init__.py +6 -0
  26. smappy_smlm-0.1.0/src/smappy/cli/camera_args.py +43 -0
  27. smappy_smlm-0.1.0/src/smappy/cli/drift.py +132 -0
  28. smappy_smlm-0.1.0/src/smappy/cli/fit.py +92 -0
  29. smappy_smlm-0.1.0/src/smappy/cli/live.py +87 -0
  30. smappy_smlm-0.1.0/src/smappy/cli/view.py +52 -0
  31. smappy_smlm-0.1.0/src/smappy/detect.py +284 -0
  32. smappy_smlm-0.1.0/src/smappy/drift.py +537 -0
  33. smappy_smlm-0.1.0/src/smappy/filter.py +181 -0
  34. smappy_smlm-0.1.0/src/smappy/group.py +267 -0
  35. smappy_smlm-0.1.0/src/smappy/io/__init__.py +0 -0
  36. smappy_smlm-0.1.0/src/smappy/io/calibration.py +224 -0
  37. smappy_smlm-0.1.0/src/smappy/io/cameras_mat.py +204 -0
  38. smappy_smlm-0.1.0/src/smappy/io/hdf5.py +137 -0
  39. smappy_smlm-0.1.0/src/smappy/io/ndtiff.py +386 -0
  40. smappy_smlm-0.1.0/src/smappy/io/queue_source.py +184 -0
  41. smappy_smlm-0.1.0/src/smappy/io/tiff.py +277 -0
  42. smappy_smlm-0.1.0/src/smappy/io/watch.py +260 -0
  43. smappy_smlm-0.1.0/src/smappy/live.py +308 -0
  44. smappy_smlm-0.1.0/src/smappy/locs.py +208 -0
  45. smappy_smlm-0.1.0/src/smappy/lut.py +170 -0
  46. smappy_smlm-0.1.0/src/smappy/metadata.py +139 -0
  47. smappy_smlm-0.1.0/src/smappy/pipeline.py +257 -0
  48. smappy_smlm-0.1.0/src/smappy/psf.py +141 -0
  49. smappy_smlm-0.1.0/src/smappy/rcc.py +365 -0
  50. smappy_smlm-0.1.0/src/smappy/render.py +499 -0
  51. smappy_smlm-0.1.0/src/smappy/roi.py +93 -0
  52. smappy_smlm-0.1.0/src/smappy/spatial.py +242 -0
  53. smappy_smlm-0.1.0/src/smappy/viewer.py +945 -0
  54. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/PKG-INFO +361 -0
  55. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/SOURCES.txt +76 -0
  56. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/dependency_links.txt +1 -0
  57. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/entry_points.txt +5 -0
  58. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/requires.txt +11 -0
  59. smappy_smlm-0.1.0/src/smappy_smlm.egg-info/top_level.txt +1 -0
  60. smappy_smlm-0.1.0/tests/test_api.py +154 -0
  61. smappy_smlm-0.1.0/tests/test_append.py +186 -0
  62. smappy_smlm-0.1.0/tests/test_calibration.py +57 -0
  63. smappy_smlm-0.1.0/tests/test_detect_roi.py +149 -0
  64. smappy_smlm-0.1.0/tests/test_drift.py +143 -0
  65. smappy_smlm-0.1.0/tests/test_filter.py +104 -0
  66. smappy_smlm-0.1.0/tests/test_fitter.py +111 -0
  67. smappy_smlm-0.1.0/tests/test_group.py +200 -0
  68. smappy_smlm-0.1.0/tests/test_live.py +224 -0
  69. smappy_smlm-0.1.0/tests/test_locs_io.py +111 -0
  70. smappy_smlm-0.1.0/tests/test_metadata.py +44 -0
  71. smappy_smlm-0.1.0/tests/test_ndtiff.py +208 -0
  72. smappy_smlm-0.1.0/tests/test_queue_source.py +237 -0
  73. smappy_smlm-0.1.0/tests/test_rcc.py +35 -0
  74. smappy_smlm-0.1.0/tests/test_render.py +220 -0
  75. smappy_smlm-0.1.0/tests/test_save_image.py +62 -0
  76. smappy_smlm-0.1.0/tests/test_spatial.py +53 -0
  77. smappy_smlm-0.1.0/tests/test_viewer.py +516 -0
  78. smappy_smlm-0.1.0/tests/test_watch.py +188 -0
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 smappy contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,8 @@
1
+ # The extensions are compiled from the sdist, so everything the compiler reads
2
+ # has to be in it: setuptools ships the .cpp files it was told to build, but not
3
+ # the headers they include, and without these the sdist builds nowhere.
4
+ include csrc/*.hpp
5
+ include ci/*.py
6
+ include examples/*.yaml
7
+ recursive-include tests *.py
8
+ include THIRD_PARTY_NOTICES.md
@@ -0,0 +1,361 @@
1
+ Metadata-Version: 2.4
2
+ Name: smappy-smlm
3
+ Version: 0.1.0
4
+ Summary: Single-molecule localization fitting pipeline (Python port of the SMAP fast-simple workflow)
5
+ Author-email: Jonas Ries <ries@embl.de>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/ries-lab/SMAPpy
8
+ Project-URL: Source, https://github.com/ries-lab/SMAPpy
9
+ Keywords: SMLM,super-resolution,localization microscopy,PALM,STORM
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.22
18
+ Requires-Dist: scipy>=1.8
19
+ Requires-Dist: tifffile>=2022.5
20
+ Requires-Dist: h5py>=3.6
21
+ Requires-Dist: pyyaml>=5.4
22
+ Provides-Extra: viewer
23
+ Requires-Dist: matplotlib>=3.4; extra == "viewer"
24
+ Provides-Extra: image
25
+ Requires-Dist: pillow>=9.0; extra == "image"
26
+ Dynamic: license-file
27
+
28
+ # smappy
29
+
30
+ Python implementation of the SMAP single-molecule fitting pipeline: camera
31
+ conversion, filtering, peak finding, ROI cutting, maximum-likelihood fitting
32
+ with a Gaussian or experimental (cubic-spline) PSF, and streaming output to
33
+ HDF5. Reads SMAP `_3Dcal.mat` calibration files, Micro-Manager TIFF stacks and
34
+ NDTiff datasets (pycro-manager).
35
+
36
+ See [NOTES.md](NOTES.md) for the design decisions and open questions.
37
+
38
+ ## Install
39
+
40
+ pip install smappy-smlm[viewer]
41
+
42
+ The distribution is `smappy-smlm` because `smappy` on PyPI is an unrelated
43
+ package; the import name is `smappy` either way. From a checkout:
44
+
45
+ /usr/bin/python3 -m venv .venv # native arm64 on Apple silicon
46
+ .venv/bin/python -m pip install ".[viewer]"
47
+
48
+ That builds the C++ extensions and installs the `smappy-fit`, `smappy-live`,
49
+ `smappy-view` and `smappy-drift` commands. For work on smappy itself, `-e` and
50
+ `pytest` instead; `scripts/*.py` run from a checkout without installing
51
+ anything.
52
+
53
+ ## Use
54
+
55
+ import smappy
56
+
57
+ locs = smappy.fit(data, out="OUT.h5",
58
+ camera={"conversion": 6.7, "offset": 400,
59
+ "pixelsize_um": 0.127},
60
+ calibration="..._3dcal.mat")
61
+ smappy.view("OUT.h5")
62
+
63
+ `data` is a path to an acquisition, an image source, an array of frames, or any
64
+ iterable of `(first_frame, block)` -- so images already in memory need no file.
65
+ A path may be a Micro-Manager TIFF series or an NDTiff dataset directory; which
66
+ one it is follows from what is there, and nothing above `open_stack` has to
67
+ know.
68
+ `camera` is a dict of the fields, a `CameraMetadata` or the path of a YAML
69
+ config, and overrides whatever the image metadata says; `calibration` is a
70
+ `_3dcal.mat`, and without one the fit is Gaussian and there is no z. The table
71
+ is returned whether or not it is also written; `collect=False` streams to the
72
+ file alone, for an acquisition too long to hold in memory.
73
+
74
+ `smappy.fit` only assembles the stages, and they are equally available on their
75
+ own -- this is the same fit written out:
76
+
77
+ from smappy.io.tiff import open_stack, camera_metadata
78
+ from smappy.io.calibration import load_spline_calibration
79
+ from smappy.detect import DoGFilter, DynamicCutoff, PeakFinder
80
+ from smappy.psf import SplinePSF
81
+ from smappy.pipeline import FitSettings, fit_stack
82
+
83
+ source = open_stack("...MMStack_Default.ome.tif")
84
+ camera = camera_metadata(source, overrides={"conversion": 6.7, "offset": 400,
85
+ "pixelsize_um": 0.127})
86
+ model = SplinePSF(load_spline_calibration("..._3dcal.mat"))
87
+ finder = PeakFinder(DoGFilter(1.2), DynamicCutoff(1.7))
88
+
89
+ locs, engine = fit_stack(source.frames(chunk=200), camera, finder, model,
90
+ FitSettings(roisize=13, output_unit="nm"))
91
+
92
+ To render an image from a localization table:
93
+
94
+ from smappy.filter import LocFilter
95
+ from smappy.render import (FieldOfView, RenderSettings, DisplaySettings,
96
+ render_locs)
97
+
98
+ keep = LocFilter(locs, loc_precision_nm=(None, 20), logl_rel=(-2, 0))
99
+ fov = FieldOfView.around(locs["x_nm"], locs["y_nm"], pixelsize=10.0)
100
+ image = render_locs(locs, fov, RenderSettings(mode="precision"), select=keep)
101
+ rgb = DisplaySettings(lut="hot", gamma=0.7).apply(image)
102
+
103
+ `mode` is `"hist"`, `"gauss"` (one sigma for all) or `"precision"` (sigma from
104
+ the localization precision, the default in SMAP). Set `color_field` to colour
105
+ by z or any other column instead of by density. Rendering and display are
106
+ separate on purpose: contrast, gamma and the colour map change without
107
+ re-rendering.
108
+
109
+ To merge localizations of the same emitter across consecutive frames:
110
+
111
+ from smappy.group import group, GroupSettings
112
+
113
+ grouped, group_index = group(locs, GroupSettings(dx=50.0, dt=1))
114
+
115
+ `grouped` carries the same columns, combined by SMAP's per-column rules
116
+ (positions weighted by precision, z by its own error, photons summed and their
117
+ errors added in quadrature, precisions added in inverse quadrature), plus
118
+ `n_in_group`.
119
+
120
+ To look at the result:
121
+
122
+ smappy.view(locs) # a table, or the path of a saved file
123
+
124
+ The image and the controls open as two windows. The image window holds nothing
125
+ but the image, so it can be resized to whatever the screen allows -- any shape,
126
+ filled edge to edge: a wide window shows more x rather than putting bands beside
127
+ a square image, and pixels stay square throughout. Since the rendered pixel
128
+ size follows the canvas, a larger window is a *finer* image, not a scaled-up
129
+ one. Closing it closes both; closing the controls leaves the image alone.
130
+
131
+ Scroll or pinch to zoom about the cursor, `+`/`-` to zoom about the centre, drag
132
+ to pan, `r` to reset. Panning and zooming re-render as they go, so a gesture
133
+ fills in what it exposes instead of dragging a stale image around. Type a
134
+ minimum and maximum to filter on localization precision, z, PSF size, relative
135
+ log-likelihood and frame; an empty box means "no bound", and the data range is
136
+ shown beside each row. A window opens with a precision cut at 25 nm, relative
137
+ log-likelihood above -1.5 and z within +-500 nm; each is written into its box,
138
+ so what has been filtered out is visible rather than hidden in a default.
139
+ The "grouped" box switches to the grouped table, which is built on first use and
140
+ keeps its own filter; "additive" switches field colouring to SMAP's composite,
141
+ where overlapping colours add (red over cyan saturates to white).
142
+
143
+ "colour by" selects a plain intensity image or one coded by z, frame,
144
+ localization precision or photons, with the range typed into the "colour" row
145
+ (empty ends fall back to the data's own). The range is always explicit, so the
146
+ same z means the same colour at every zoom and after every block of a live fit;
147
+ the LUT follows the choice -- `hot` for intensity, `turbo` for a coded field.
148
+ Needs matplotlib (`pip install matplotlib`).
149
+
150
+ Or from the command line:
151
+
152
+ smappy-fit DATA OUT.h5 \
153
+ --camera camera.yaml --cal CAL_3dcal.mat --units nm
154
+
155
+ The camera is stated in a YAML config (`examples/camera_evolve512.yaml`) or
156
+ directly on the command line -- `--pixelsize 0.127 --conversion 6.7 --offset
157
+ 400` does the same thing without a file, and either overrides what the image
158
+ metadata says. A lab that keeps a SMAP `*_cameras.mat` can pass it with
159
+ `--cameras` for the conversion and the per-camera metadata rules, but nothing
160
+ requires one. In Python the same layers are `camera_metadata(source, presets,
161
+ overrides)`, where `overrides` is a `CameraMetadata`, a dict or a YAML path and
162
+ wins over everything else.
163
+
164
+ ## NDTiff
165
+
166
+ pycro-manager writes NDTiff: a directory with an `NDTiff.index` and one or more
167
+ `*NDTiffStack*.tif`. The index is a flat table giving, per image, the file and
168
+ the byte offset of its pixels, so images are read by seeking and the TIFF page
169
+ chain is never walked: opening costs about 7 us per frame against the ~120 us a
170
+ page walk takes, so a 46 k-frame dataset is ready in 0.3 s rather than 5 s. `open_stack` returns an `NDTiffSource` for such a directory and
171
+ an `ImageSource` for a Micro-Manager series; everything downstream is the same.
172
+
173
+ The reader is a port of SMAP's MATLAB loader (`shared/imageloaders/`), including
174
+ the parts that are not in any specification: the index table is zero-padded, the
175
+ bytes per pixel are more reliably derived from where the metadata starts than
176
+ from the declared pixel type, and an interrupted acquisition leaves records
177
+ describing images that were never written -- those are dropped rather than read
178
+ as noise.
179
+
180
+ That last rule is also what makes a growing dataset safe to read: a record is
181
+ used only once the bytes it points at are there. Following an acquisition is
182
+ therefore just re-reading the index, with none of the care a growing TIFF page
183
+ chain needs, and `live_fit.py` takes an NDTiff directory exactly as it takes a
184
+ TIFF.
185
+
186
+ ## Drift correction
187
+
188
+ Sample drift is estimated with [COMET](https://github.com/gpufit/Comet), which
189
+ maximises the overlap of localizations between time windows -- no fiducials, no
190
+ reference structure. It is an optional dependency; the source is vendored:
191
+
192
+ .venv/bin/python -m pip install -e externaltools/Comet/Python_interface
193
+
194
+ smappy-drift OUT.h5 \
195
+ --filter loc_precision_nm - 20 --filter logl_rel -2 - \
196
+ --frames-per-window 500 --max-drift 300 --plot
197
+
198
+ The drift is estimated from the localizations that pass the `--filter` ranges --
199
+ the same limits the viewer takes -- and then subtracted from **all** of them,
200
+ including the ones the filter hides: a filter is a view, the correction is a
201
+ coordinate change. The result is written as `OUT_driftc.h5`, an ordinary
202
+ localization file the viewer opens unchanged, with the drift curve kept in a
203
+ `/drift` group.
204
+
205
+ From Python:
206
+
207
+ from smappy.drift import DriftSettings, correct_drift, save_drift_corrected
208
+
209
+ keep = LocFilter(locs, loc_precision_nm=(None, 20), logl_rel=(-2, None))
210
+ corrected, drift = correct_drift(locs, DriftSettings(segmentation_var=500),
211
+ select=keep)
212
+ save_drift_corrected("OUT_driftc.h5", corrected, drift)
213
+
214
+ `drift.drift[f]` is `(dx, dy, dz)` in nm for frame `f`, and `drift.plot()` draws
215
+ it. z drift is estimated whenever the table has `z_nm`; `DriftSettings(use_z=
216
+ False)` keeps it lateral.
217
+
218
+ Nearly all the time is the optimizer, which evaluates a cost over every
219
+ neighbour pair a few hundred times: 1:17 for 410 k localizations and 314 M pairs
220
+ on the CPU backend (46 k frames, 92 time windows), down from 13 minutes -- see
221
+ [NOTES.md](NOTES.md) for the measurements, the noise floor they are judged
222
+ against, and what did *not* help.
223
+
224
+ `--group` estimates from grouped localizations instead, one per blink: 314 M ->
225
+ 21 M pairs and the whole correction takes **5 s**, agreeing with the full
226
+ estimate to about 1 nm (median) while being twice as noisy per window, which
227
+ costs a few percent of resolution.
228
+
229
+ **`--spline --group` is the best estimator measured so far**: the drift is
230
+ fitted as a cubic B-spline in time (no time windows, no interpolation
231
+ afterwards) from grouped localizations. 4 s on the clathrin dataset against
232
+ 2:33 for free per-window vectors, 0.6-0.7 nm noise per axis against 1.9-5.2, and
233
+ a better image out of sample. `--knot-frames` sets how finely it can bend; the default (2000) is the better
234
+ all-round choice, and finer settings buy a better-resolved transient at the
235
+ start of an acquisition at the cost of spurious wiggle where the density has
236
+ bleached away -- see [NOTES.md](NOTES.md).
237
+
238
+ `--rcc` estimates the drift by redundant cross-correlation instead -- an
239
+ independent method (ported from SMAP's `finddriftfeature`), useful as a second
240
+ opinion. On the clathrin dataset, with both estimating from grouped
241
+ localizations and at matched smoothing, the two agree to 1.2 / 1.7 / 1.6 nm rms
242
+ in x / y / z -- the level of their own noise (0.6-0.9 nm each).
243
+
244
+ `--two-stage` runs the grouped pass first and then an ungrouped one over a 30 nm
245
+ radius, which is **~9x faster than the single pass for ~99% of the improvement**
246
+ -- and, because the fine pass is bounded by its own radius, it cannot produce
247
+ the runaway time window the single pass occasionally does.
248
+
249
+ Filter before estimating, and **include a z cut**: without one the axial drift
250
+ follows the out-of-focus tail (`z_err_nm` has a 95th percentile of 108 nm).
251
+ `--filter logl_rel -2 - --filter loc_precision_nm - 15 --filter z_nm -300 300`
252
+ is a reasonable set for a 3D dataset.
253
+
254
+ ## Online: fit while the microscope writes
255
+
256
+ smappy-live DATA OUT.h5 --camera camera.yaml \
257
+ --cal CAL_3dcal.mat --update 3 --timeout 30
258
+
259
+ `DATA` is the growing Micro-Manager TIFF, or the directory it is being written
260
+ into; it does not have to exist yet. The window opens as soon as the first
261
+ frames appear and takes in new localizations every `--update` seconds; the fit
262
+ ends `--timeout` seconds after the last frame is written, which is how an
263
+ acquisition stops. `OUT.h5` is written throughout and is the result.
264
+
265
+ Everything the offline viewer offers works while this runs -- zoom, pan, the
266
+ filter boxes, contrast, grouping -- and **an update changes none of them**: new
267
+ localizations appear inside the view being looked at, under the bounds already
268
+ typed. The frame comes from the camera field of view, so the image does not
269
+ rescale as data arrives. Grouping cannot be extended, so the grouped table is
270
+ marked stale and rebuilt when it is next asked for.
271
+
272
+ From Python:
273
+
274
+ from smappy.live import LiveSettings, live_view
275
+
276
+ live_view(directory, camera, finder, model, FitSettings(output_unit="nm"),
277
+ output="OUT.h5", live=LiveSettings(update_seconds=3.0))
278
+
279
+ `LiveFit` is the same thing without a window: it runs the pipeline in a thread
280
+ and queues finished blocks, for a different front end or a headless run.
281
+
282
+ ### Frames that are never written to a file
283
+
284
+ A control program may have the images already -- pycro-manager hands each one to
285
+ a callback, a camera API returns them from a buffer. `QueueSource` is the same
286
+ `ImageSource` interface for that: the producer pushes, the pipeline reads.
287
+
288
+ source = smappy.queue_source(shape=(512, 512))
289
+
290
+ source.push(image) # from the acquisition thread, or a hook
291
+ source.close() # the acquisition ended
292
+
293
+ smappy.live_view(source, camera, finder, model, settings, output="OUT.h5")
294
+
295
+ Frames are numbered as they are pushed; `push(image, first_frame=n)` states the
296
+ acquisition's own number instead, and a gap in the numbering ends a block rather
297
+ than being papered over. `maxsize` bounds the queue for a producer that can
298
+ outrun the fit, which then waits -- the only honest answer when the alternative
299
+ is growing until memory runs out.
300
+
301
+ The lower level is there too: drive `LocalizationEngine` directly and
302
+ `push(frames)` returns localizations once enough ROIs have accumulated, `flush()`
303
+ forces a partial block. Nothing asks how many frames there will be.
304
+
305
+ ### Reading the result while it is still being fitted
306
+
307
+ `on_block(locs)` is called with each finished block as it comes out, which is
308
+ what a control loop needs -- the localizations per frame are a measure of the
309
+ blinking density, and the density is what the activation laser is there to hold
310
+ steady:
311
+
312
+ def density(locs):
313
+ frames = locs["frame"]
314
+ per_frame = len(locs) / (frames.max() - frames.min() + 1)
315
+ ... # act on it
316
+
317
+ smappy.fit(data, out="OUT.h5", camera=camera, calibration=cal,
318
+ chunk=25, on_block=density)
319
+
320
+ **`chunk` sets how often that happens**, because a block is fitted at the end of
321
+ a chunk of frames: 25 frames at 100 ms is a reading every 2.5 s. Smaller chunks
322
+ cost a little throughput and buy a shorter loop. On a real dSTORM acquisition
323
+ this reads 109, 107, 105, 104, 103, 98, 92, 93 localizations per frame over the
324
+ first 200 frames -- the density decaying as the dye bleaches, which is the signal
325
+ to act on.
326
+
327
+ `progress(engine)` is the cheaper hook: it gives the running counts, including
328
+ `stats["candidates"]`, the *detected* spots. That number needs no fit at all, so
329
+ it is available sooner and is the better control signal when the point is
330
+ density rather than positions.
331
+
332
+ ### A window is not the only output
333
+
334
+ `show` and `live_view` open a matplotlib window and want the main thread, which
335
+ a program with its own event loop cannot give them. Two ways round it:
336
+
337
+ smappy.save_image(locs, "image.png", pixelsize=10.0) # no window at all
338
+
339
+ `save_image` takes a table or a saved file, and the same `RenderSettings`,
340
+ `DisplaySettings` and filter the viewer takes, so what it writes is what the
341
+ viewer would show. It needs Pillow.
342
+
343
+ For a window, run `smappy-view FILE` as a separate process. And `LiveFit` is
344
+ `live_view` without a window: the fit in a thread, finished blocks on a queue,
345
+ for a front end of your own.
346
+
347
+ ## Scripts
348
+
349
+ | script | what it checks |
350
+ |---|---|
351
+ | `check_calibration.py` | spline coefficients against the bead stack in the same file |
352
+ | `check_stack.py` | what the image metadata provides, and what is missing |
353
+ | `check_detection.py` | filtering, peak finding and ROI cutting on real frames |
354
+ | `check_fit.py` | spline fits on real data, with and without the mirror flip |
355
+ | `fit_dataset.py` | the whole pipeline, to HDF5 (`smappy-fit`) |
356
+ | `view_locs.py` | opens the viewer on a saved localization file (`smappy-view`) |
357
+ | `drift_correct.py` | drift-corrects a saved file with COMET (`smappy-drift`) |
358
+
359
+ ## Tests
360
+
361
+ SMAPPY_TEST_CAL=/path/to/_3dcal.mat PYTHONPATH=src .venv/bin/python -m pytest tests/