TO_SPAM.md of the spam-gui repository, shown as HTML.To put in spam
Everything spam-gui has to do with spam, in one place, documented enough to be handed to spam's developers as it is:
- Missing in spam: analysis functions a module needs that the released spam does not have,
written meanwhile in
src/spam_gui/tospam/. - Problems found in spam: results of spam that are wrong, with how to reproduce them, the cause, and a fix.
- To decide: code of spam-gui that computes, for images larger than the memory, what spam computes for images in memory; whether it belongs in spam is the maintainer's decision.
spam-gui implements no image analysis and never changes spam's algorithms (SPEC.md section 1): what concerns spam is written here, and that's it. spam-gui does not work around spam's problems.
Status of each entry: waiting (not given to spam yet) · reported / proposed (an issue or merge request to
spam, linked) · in spam x.y (in a release: remove it from spam-gui, require that release in pyproject.toml,
and move the entry to « Done »).
1. Missing in spam
When a module needs an analysis function that the released spam does not have, it is written in
src/spam_gui/tospam/, in folders that mirror spam's packages (tospam/DIC/ for spam.DIC: the file moves into
spam at the same place), as it would be in spam (spam's conventions and names, docstrings, unit tests; no Qt, napari
or other spam-gui file), and listed here.
spam.DIC.multimodal: the multimodal registration step by step
- File:
src/spam_gui/tospam/DIC/multimodalSteps.py· tests:tests/test_multimodalSteps.py - Why: spam's
gaussianMixtureParametersandphaseDiagramchoose the peaks, the fits and sigma all at once. The MMR module (steps 4 and 5) lets the user make each choice: these functions are the same calculations, one choice at a time. With full coverage, the phase diagram is spam's (tested:test_phaseDiagramMatchesLibraryAtFullCoverage). - Status: waiting
| Function | What it does | Used by |
|---|---|---|
jointHistogramCounts(im1, im2, greyLimits, BINS) |
joint histogram of two images, in numbers of voxels: the counts of parts of the images add up (images larger than the memory) | MMR step 4 (by parts) |
jointHistogram(im1, im2, greyLimits, BINS) |
the same, normalised (sum 1) | MMR step 4, tests |
findHistogramPeaks(hist, minDistance, maxPeaks, excludeBorder) |
the local maxima of a joint histogram, highest first | MMR step 4 |
fitGaussianPeak(hist, peak, fitDistance) |
fits a 2D elliptical Gaussian to one chosen peak, within a fit radius, with the arithmetic of gaussianMixtureParameters |
MMR step 4 |
gaussianHasMaximum(gaussianParameters) |
whether a fitted Gaussian has a maximum (a·c − b² > 0) | MMR step 4 (ready()) |
gaussianEllipsoid(gaussianParameters, BINS) |
a fitted Gaussian evaluated on the bins (to draw it) | MMR step 4 |
phaseDiagramSigma(gaussianParameters, jointHistogram, sigma, distanceType) |
the phase diagram for a given sigma and distance (maximum or Mahalanobis: DISTANCE_MAXIMUM, DISTANCE_MAHALANOBIS) |
MMR step 5 |
sigmaForFullCoverage(gaussianParameters, jointHistogram, distanceType, targetCoverage) |
the smallest sigma, on spam's scale (2, 3, …, 10, 20, …), for which the phase diagram covers the histogram | MMR step 5 (« Full coverage ») |
quantiseImage(im, greyLimits, BINS) |
an image as bin numbers (0 to BINS − 1), as multimodalRegistration expects them |
MMR steps 4 to 6, phase layers, fields saved |
2. Problems found in spam
2.1 spam.deformation.decomposeF (and decomposePhi): wrong stretch and rotation when two eigenvalues of FᵀF are equal
- spam version: 0.9.1 (
spam/deformation/deformationFunction.py), with NumPy 2.5.4, SciPy 1.18.1, Python 3.14. - Status: waiting (to report)
What is wrong. For an F whose right Cauchy tensor C = FᵀF has two equal eigenvalues (e.g., a rotation and the
same zoom along two axes: a common case, such as zooming one axis only), the right stretch tensor U is wrong, and so
are the zoom (z) and the rotation (r) derived from it: F is not R·U, and computePhi of the decomposition does
not give Φ back. A rotation alone, or a zoom alone, is right.
To reproduce:
import numpy, spam.deformation
Phi = spam.deformation.computePhi({"t": [1, 2, 3], "r": [1.5, -1, 0.8], "z": [1.01, 1, 1]})
d = spam.deformation.decomposePhi(Phi)
print(d["z"]) # [1.01, 0.6169, 1.3831] expected [1.01, 1, 1]
print(numpy.real(d["r"])) # [6.976, -1.112, 1.088] expected [1.5, -1, 0.8]
again = spam.deformation.computePhi({"t": numpy.real(d["t"]), "r": numpy.real(d["r"]), "z": d["z"]})
print(numpy.allclose(again, Phi)) # False
It also prints, at each call, two NumPy warnings:
deformationFunction.py:278: ComplexWarning: Casting complex values to real discards the imaginary part
rotationAngleDeg = numpy.rad2deg(float(rotationAngleRad))
deformationFunction.py:335: ComplexWarning: Casting complex values to real discards the imaginary part
"z": [float(U[i, i]) for i in range(3)],
Cause. decomposeF (line 232) diagonalises C with numpy.linalg.eig, then builds U = V·√D·Vᵀ. That is U only
if the eigenvectors V are orthonormal (Vᵀ = V⁻¹). eig (LAPACK geev, for general matrices) does not make them so
when eigenvalues are equal: any basis of the shared eigenspace is returned. With NumPy 2, eig also returns complex
numbers (zero imaginary parts here), hence U, R and the angle are complex, and the float(...) casts warn.
# deformationFunction.py, decomposeF
CeigVal, CeigVec = numpy.linalg.eig(C) # line 232
...
U = numpy.dot(numpy.dot(CeigVec, diagU), CeigVec.T) # right only if CeigVec is orthonormal
R = numpy.dot(F, numpy.linalg.inv(U))
Fix. C is symmetric: numpy.linalg.eigh gives real eigenvalues and orthonormal eigenvectors, always.
- CeigVal, CeigVec = numpy.linalg.eig(C)
+ CeigVal, CeigVec = numpy.linalg.eigh(C)
Checked on the example above, with the same steps as decomposeF:
| eigenvectors orthonormal | U diagonal | U·U = C | type | |
|---|---|---|---|---|
eig (now) |
no | 1.01, 0.617, 1.383 | no | complex128 |
eigh (fix) |
yes | 1.01, 1, 1 | yes | float64 |
A test for spam: for random rotations and zooms, including two equal zooms, computePhi of decomposePhi(Phi)
gives Phi back, and z is real.
Effect in spam-gui. spam-gui shows the translation, rotation and zoom of Φ from decomposePhi: in « Current Φ »,
in the result of step 6 and in the Summary of the MMR module; and step 2 (eye registration) takes its values from it
when it is set from a Φ (« Sync eye registration from Phi », a Φ loaded from a file). For such a Φ, these are wrong.
Φ itself, the registration and the files saved are not affected. spam-gui only keeps the real parts of what spam
gives (else « 1.0000+0.0000j » would be shown) and silences the ComplexWarning around its call
(modules/mmr.py, decomposePhi); it does not correct the result.
2.2 spam.DIC.multimodalRegistration: its progress only printed, in long lines
- spam version: 0.9.1 (
spam/DIC/multimodal.py, lines 262 to 277 and 548 to 552). - Status: waiting (to report)
What is wrong. With verbose=True, the registration reports its progress only by print: a line per iteration,
indented by a tab and about 150 characters long (\tIteration Number 002 LL = 4858.45 dPhi = 0.4675 Tr = … Ro = … Zo = …), then \t -> Converged (or No convergence before max iterations, Singular). A program showing the
progress (spam-gui's step 6) has to catch the standard output, cut the tabs, and still gets lines too long for a
narrow panel. The rotation (Ro) and zoom (Zo) printed come from decomposePhi: wrong in the case of 2.1.
Fix (suggested). Give each iteration's values to the caller, e.g. a callback(iteration, LL, deltaPhiNorm, Phi)
argument (or Python's logging), and keep print for verbose: a program shows what it wants, as it wants. Shorter
printed lines (no leading tab; the transformation on a line of its own) would help too.
Effect in spam-gui. Step 6 shows these lines in its log, without their leading spaces (spam-gui cuts them); at 1366×768 they are wider than the panel, and the log scrolls sideways.
3. To decide
Code of spam-gui that is not image analysis itself, but computes from the images what spam computes, for images larger than the memory (by blocks, from images mapped from their files, in threads). Whether it belongs in spam is the maintainer's decision; until then it stays where it is.
| Code | What it does | Why it may belong in spam |
|---|---|---|
core.volumes.MovedVolume |
an image moved by Φ, as spam.DIC.applyPhi, by blocks from an image mapped from its file (scipy.ndimage.affine_transform), in threads |
used to display image 1 moved, but also for the files the MMR module saves (image 1 registered, the fields): an applyPhi by blocks |
core.parts.fields |
the phase and residual fields of the whole images, a slab at a time, with spam's computeGMresidualAndPhase |
the fields as the registration computes them, for images larger than the memory |
core.parts.quantise, the joint histogram of a crop by parts (core.parts.Crop) |
quantiseImage, jointHistogramCounts a slab at a time |
the same calculations, for images larger than the memory |
core.io.readBinned |
spam.DIC.binning a few slices at a time, from a file |
binning images larger than the memory |
Done
(None yet.)