Skip to content

Conversation

@Baharis
Copy link
Member

@Baharis Baharis commented Oct 6, 2025

While the beam shift calibration class is marked in doc-string as "deprecated", I find it useful for my experiments. However, I sometimes had problems getting it to work, in particular when the beam was larger. The wording across the file is inconsistent, the ways to investigate the results are very limited, and in overall while the calibration asks you if you would like to repeat it when calibrating live, there is little that can be used to evaluate whether the calibration was successful.

This PR involves mostly refactor of the calibrate_beamshift.py file intended to increase its readability. This involves several changes to the code structure; however, the overall code capability, including existing API, remains unchanged:

  • general: added a lot of type hints and expanded doc-strings;
  • general: terms center-"pixel" and beam-"shift" are now always used in this order and consistently;
  • CalibBeamShift: is now a dataclass;
  • CalibBeamShift: all use of "pixel" and "shift" are now ordered this way and consistent;
  • CalibBeamShift: beamshift_to_pixelcoord and pixelcoord_to_beamshift don't crash at list/tuple-like inputs;
  • CalibBeamShift: from_data can now store images (for plotting) instead of headers (unused);
  • CalibBeamShift: from_file/to_file now serializes to a human-readable yaml instead of pickle;
    • A new yaml dumper warrants that the 2D np.ndarrays are easily readable upon dumping (see below);
  • CalibBeamShift: simplified plot, raises rather than exiting at no data (unused, more future-proof);
  • CalibBeamShift: Added annotate_videostream used in live to show calibration results in the GUI (see below);
  • calibrate_beamshift was de-cluttered and simplified,
  • calibrate_beamshift can now use camera.calib_beamshift.delay between images (if beam shift is slow);
  • calibrate_beamshift: reimplemented instamatic.tools.printer progress bar display using tqdm.tqdm
  • calibrate_beamshift_from_image_fn: fixed (unusable due to broken import - still unsure why it needs a hole though);

This PR does not introduce any changes to API, just code quality and ease of interpretation. Existing code should work mostly just as before. Running calibration with even-sized grid should now work properly. The calibration will now be saved as a human-readable yaml file, e.g. for grid size 3:

pixels:
- [133.8375, 152.1796875]
- [-237.13828125, 150.26484374999998]
- [124.96875, -18.24140625]
- [-146.23359374999998, 122.85234375]
- [-138.0703125, 0.9070312500000001]
- [-118.11562500000001, -65.5078125]
- [109.8515625, 153.99375]
- [-255.07734375, 121.03828125]
- [-113.78203125, -157.21875]
reference_pixel: [104.0465625, 436.61460937500004]
reference_shift: [60029, 61754]
shifts:
- [-50.0, -50.0]
- [-50.0, 0.0]
- [-50.0, 50.0]
- [0.0, -50.0]
- [0.0, 0.0]
- [0.0, 50.0]
- [50.0, -50.0]
- [50.0, 0.0]
- [50.0, 50.0]
transform:
- [0.06492405453091554, -0.029765340275091753]
- [-0.2283134920986265, -0.00846418035740315]

After the calibration is complete and when live asks whether it should be repeated, the GUI will now display the maximum over all collected calibration images with some dots, blue representing fit- and orange representing modeled- beam positions:

calib_mostlyRight

One note for future is that the calibration gets progressively less accurate the bigger the beam is:

calib_bitTooLargeBeam calib_tooLargeBeam

@Baharis Baharis self-assigned this Oct 6, 2025
@Baharis Baharis requested a review from stefsmeets October 6, 2025 18:16
Copy link
Member

@stefsmeets stefsmeets left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. This is probably one of the first routines I wrote, so it was due for a refresh. Happy to hear you find it useful enough to spend some time on.

I left some minor comments and type suggestions.

return c

@classmethod
def from_file(cls, fn=CALIB_BEAMSHIFT) -> Self:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def from_file(cls, fn=CALIB_BEAMSHIFT) -> Self:
def from_file(cls, fn: str =CALIB_BEAMSHIFT) -> Self:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'll make it an AnyPath i.e. Union[str, bytes, os.PathLike] though.

return c

def to_file(self, fn=CALIB_BEAMSHIFT, outdir='.'):
def to_file(self, fn=CALIB_BEAMSHIFT, outdir: AnyPath = '.') -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def to_file(self, fn=CALIB_BEAMSHIFT, outdir: AnyPath = '.') -> None:
def to_file(self, fn: str=CALIB_BEAMSHIFT, outdir: AnyPath = '.') -> None:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to from_file, I'd prefer to make it an AnyPath, though in this case it actually is appended to outdir so it is less straightforward. Is there any reason to split outdir and fn here? Other than consistency across configs, a single param would be cleaner, see CalibStageRotation.to_file.

Copy link
Member

@stefsmeets stefsmeets Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I wrote this, the goal for splitting fn and outdir was to be able to have a global default for fn. Not sure if I still like that choice so feel free to change it. I agree that it makes more sense to specify the directory or full path here.

import yaml


def _numpy_2d_representer(dumper: yaml.Dumper, array: np.ndarray) -> yaml.nodes.SequenceNode:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used for dumping simple coordinates like the beam centers and shifts and not images, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, images are removed before dumping to yaml to avoid duplicating several MB worth of data. I though about including image paths in calib instead, but this proved to require a lengthier rework I could not afford. Nota bene, without this, serialized ndarrays are really disgusting:

pixels:
- - 129.90703125000002
  - 3.225
- - -13.1015625
  - 232.8046875
- - 8.969531250000001
  - -21.970312500000002
- - -131.015625
  - 245.80546875000002
...

Copy link
Member

@stefsmeets stefsmeets Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, just wondering. Storing image arrays in a yaml is something I tried at some point, but it was a real pain to work with. I usually convert to/from list but a dumper also works.

I think this is where libraries like attrs (or dataclasses) really shine. You can have a schema in the form of your dataclass, so you can decouple structuring/unstructuring (i.e. reading/writing yaml) from your data definition.

Baharis and others added 3 commits October 7, 2025 13:20
Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com>
Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com>
@stefsmeets
Copy link
Member

stefsmeets commented Oct 7, 2025

One note for future is that the calibration gets progressively less accurate the bigger the beam is:

The beam center algorith assumes a well-formed peak:

def find_beam_center(

With a large beam, I could imagine the contrast is quite flat without an obvious peak in the center of the bundle. You may have better luck first applying a large gaussion filter before using the algorithm. Or try a different algorithm based on thresholding and blob-finding: https://scikit-image.org/docs/stable/auto_examples/features_detection/plot_blob.html#sphx-glr-auto-examples-features-detection-plot-blob-py

@stefsmeets
Copy link
Member

Btw, the fact that you can render the calibration results directly in the gui is really cool.

@Baharis
Copy link
Member Author

Baharis commented Oct 7, 2025

I read this morning that apparently the phase correlation method loses accuracy very quickly with growing peak size. In general, I do not consider this PR to be a "fix", I mostly just needed to identify when the calibration goes wrong because I noticed my FastADT manual tracking was misbehaving. This PR is mostly to address that.

In general, I would like to 1) decouple data collection and interpretation logic, 2) run interpretation in separate thread while next image is collected, 3) implement additional data collection methods, 4) have several methods for interpretation and 5) auto-choose the method with best fit. Before that I should probably study calib_directbeam (similar logic) and autocred (some beam size calibration) more though. Unfortunately I have higher priorities now.

As for rendering in GUI, I am pretty happy with the flexibility VideoStreamProvider offers, especially given I managed to get it to run without losing much performance, and I will keep referencing this Q&A :).

Given is mostly non-intrusive tested it on my TEM (will keep testing this week), I intend to have it merged sometime this week to make space for #145 that will benefit from it :).

@Baharis Baharis merged commit e4d83ca into instamatic-dev:main Oct 11, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants