Skip to content

Commit 8df2c1b

Browse files
Baharisstefsmeets
andauthored
Safely save frames and images (#148)
* Allow server cameras to be streamable by removing precaution checks * Unify interface, naming between get_microscope/camera(_class) * Remove unused _init_attr_dict/get_attrs from microscope client/server * Do not force server-side attributes to be callable, use them first: these 3 lines took ~3h * EAFP: Allow camera to call functions whether they are registered or not * EAFP: Allow camera to call unregistered functions - fixes server cameras * Remove unnecessary print debug statement * Add **streamable** description to `config.md` documentation * Encapsulate FastADT paths in separate prop/method * Add new TrackingArtist to be used with plotting multi-runs * Working multi-tracking FastADT frame! Still needs polish * Change `ClickEvent` to dataclass, implement `ClickEvent.xy` * Optimize clicking logic in FastADT experiment * Streamline the `calibrate_beamshift_live` function * Streamline `CalibBeamShift.plot` * Improvements to `CalibBeamShift` readability (WIP) * Add option to calibrate beamshift with vsp * Fix errors, add beam center * Switch calibration output format from pickle to yaml * Allow delay during calibrate beamshift * Add necessary reflections to fix plotting * Final tweaks * Make the yaml produced by CalibBeamShift human-readable * Update src/instamatic/calibrate/calibrate_beamshift.py Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com> * Update src/instamatic/calibrate/calibrate_beamshift.py Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com> * Minor post-review type-hint improvements + ruff * Add `instamatic.utils.iterating` with `sawtooth` iterating function * Make the `click_dispatcher:ClickEvent.xy` a property * Rephrase `VideoStreamProcessor.temporary` using `blocked` context * Fix the bug where canceling FastADT did not remove its elements * Fix the bug where colors of crystal tracking repeated after 10 * Remove debug message * Attempt to generalize collecting, revert as needed * Fix: rotation speed for negative target pace is negative, rounds to 0 * Rename tracking "mode" to "algo"; if continuous, track w/ movie * Generalize FastADT run collection (+fix resulting bugs) * Revert change: use stills for continuous tracking * Minor fixes and code quality improvements * Clean, remove unused code * Fix tracking failing for beam not in the center at alignment * Fix Run.__str__, clean up code, method, call order * Log all behavior in two separate message windows. * Fix ignore msg1,2 variables in headless FastADT experiment * Update in tests `tracking_mode` -> `tracking_algo` * Add estimated time required dialog in FastADT message 2 * Display which experiment is being collected in multi-expt * Display which experiment is being collected in multi-expt * Trace variables only after everything was defined to avoid Exceptions * Fix: leaving input empty even temporarily caused exception * Fix: do not define ExperimentalFastADT.q to receive it from parent * Fix: Add LiveVideoStream, FakeVideoStream to camera init to fix instamatic.camera script * Fix: properly defined VideoStreamFrame.app as "advertised" * Move `get_frame/image` from jobs to VideoStreanFrame: to fix it/avoid crashes * Prevent repeated calls to alpha_wobble from crashing GUI * Overwriting prevention mechanism was not needed * Do not overwrite `self.wobble_stop_event` if not putting in queue * FastADT: Save tracking images to tracking/ directory * Remove development imports from videostream_processor.py --------- Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com>
1 parent c2f086a commit 8df2c1b

File tree

8 files changed

+77
-72
lines changed

8 files changed

+77
-72
lines changed

src/instamatic/camera/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from __future__ import annotations
22

33
from .camera import get_camera, get_camera_class
4-
from .videostream import VideoStream
4+
from .videostream import FakeVideoStream, LiveVideoStream, VideoStream

src/instamatic/experiments/fast_adt/experiment.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import numpy as np
1616
import pandas as pd
1717
from matplotlib import pyplot as plt
18+
from PIL.Image import Image
1819
from typing_extensions import Self
1920

2021
from instamatic import config
@@ -392,7 +393,8 @@ def determine_pathing_manually(self) -> None:
392393

393394
self.ctrl.restore('FastADT_track')
394395
Thread(target=self.collect_run, args=(run,), daemon=True).start()
395-
tracking_images = deque(maxlen=len(run))
396+
tracking_frames = deque(maxlen=len(run))
397+
tracking_images: list[Optional[Image]] = [None] * len(run)
396398
tracking_in_progress = True
397399
while tracking_in_progress:
398400
while (step := self.steps.get()) is not None:
@@ -404,15 +406,18 @@ def determine_pathing_manually(self) -> None:
404406
click_beamshift_xy = self.beamshift.pixelcoord_to_beamshift(click_beampixel_yx)
405407
cols = ['beampixel_x', 'beampixel_y', 'beamshift_x', 'beamshift_y']
406408
run.table.loc[step.Index, cols] = *click.xy, *click_beamshift_xy
407-
tracking_images.append(step.image)
409+
tracking_frames.append(step.image)
408410
if 'image' not in run.table:
409-
run.table['image'] = tracking_images
411+
run.table['image'] = tracking_frames
410412
self.runs.pathing.append(deepcopy(run))
411413

412414
self.msg1('Displaying tracking. Click LEFT mouse button to start the experiment,')
413415
self.msg2('MIDDLE to track another point, or RIGHT to cancel the experiment.')
414416
for step in sawtooth(self.runs.tracking.steps):
415417
with self.displayed_pathing(step=step):
418+
image = self.videostream_processor.image
419+
image.info['_annotated_runs'] = len(self.runs.pathing)
420+
tracking_images[step.Index] = image
416421
with self.click_listener:
417422
click = self.click_listener.get_click(timeout=0.5)
418423
if click is None:
@@ -428,6 +433,16 @@ def determine_pathing_manually(self) -> None:
428433
self.steps.put(new_step)
429434
break
430435

436+
drc = self.path / 'tracking'
437+
drc.mkdir(parents=True, exist_ok=True)
438+
with self.ctrl.cam.blocked():
439+
for step, image in zip(run.steps, tracking_images):
440+
i = f'image{step.Index:02d}_al{step.alpha:+03.0f}.png'.replace('+', '0')
441+
if image is None or image.info['_annotated_runs'] < len(self.runs.pathing):
442+
with self.displayed_pathing(step=step):
443+
image = self.videostream_processor.image
444+
self.videostream_processor.vsf.save_image(image=image, path=drc / i)
445+
431446
def collect_run(self, run: Run) -> None:
432447
"""Collect `run.steps` and place them in `self.steps` Queue."""
433448
with self.ctrl.beam.unblanked(delay=0.2):

src/instamatic/gui/ctrl_frame.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import queue
34
import threading
5+
from threading import Event
46
from tkinter import *
57
from tkinter.ttk import *
8+
from typing import Dict
69

710
from instamatic import config
811
from instamatic.utils.spinbox import Spinbox
@@ -282,19 +285,21 @@ def get_stage(self, event=None):
282285

283286
def toggle_alpha_wobbler(self):
284287
if self.var_alpha_wobbler_on.get():
285-
self.wobble_stop_event = threading.Event()
286-
self.q.put(
287-
(
288-
'ctrl',
289-
{
290-
'task': 'stage.alpha_wobbler',
291-
'delta': self.var_alpha_wobbler.get(),
292-
'event': self.wobble_stop_event,
293-
},
294-
)
295-
)
296-
else: # wobbler off
297-
self.wobble_stop_event.set()
288+
wobble_stop_event = threading.Event()
289+
wobbler_task_keywords = {
290+
'task': 'stage.alpha_wobbler',
291+
'delta': self.var_alpha_wobbler.get(),
292+
'event': wobble_stop_event,
293+
}
294+
try:
295+
self.q.put(('ctrl', wobbler_task_keywords), block=False)
296+
except queue.Full:
297+
pass
298+
else:
299+
self.wobble_stop_event = wobble_stop_event
300+
else:
301+
if self.wobble_stop_event:
302+
self.wobble_stop_event.set()
298303

299304
def stage_stop(self):
300305
self.q.put(('ctrl', {'task': 'stage.stop'}))

src/instamatic/gui/fast_adt_frame.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import annotations
22

3-
import threading
43
from functools import wraps
5-
from queue import Queue
64
from tkinter import *
75
from tkinter.ttk import *
86
from typing import Any, Callable, Optional
@@ -91,7 +89,6 @@ def __init__(self, parent):
9189
super().__init__(parent, text='Experiment with a priori tracking options')
9290
self.parent = parent
9391
self.var = ExperimentalFastADTVariables(on_change=self.update_widget)
94-
self.q: Optional[Queue] = None
9592
self.busy: bool = False
9693
self.ctrl = controller.get_instance()
9794

@@ -241,7 +238,10 @@ def update_widget(self, *_, busy: Optional[bool] = None, **__) -> None:
241238
self.tracking_step.config(state=tracking_state)
242239
self.tracking_time.config(state=tracking_state)
243240

244-
tracking_time, diffraction_time = self.estimate_times()
241+
try:
242+
tracking_time, diffraction_time = self.estimate_times()
243+
except ZeroDivisionError:
244+
return
245245
tt = '{:.0f}:{:02.0f}'.format(*divmod(tracking_time, 60))
246246
dt = '{:.0f}:{:02.0f}'.format(*divmod(diffraction_time, 60))
247247
if tracking_time: # don't display tracking time or per-attempts if zero

src/instamatic/gui/gui.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,20 @@ class MainFrame:
131131

132132
def __init__(self, root, cam, modules: list = []):
133133
super().__init__()
134+
135+
self.root = root
136+
self.app = AppLoader()
137+
134138
# the stream window is a special case, because it needs access
135-
# to the cam module
139+
# to the cam module and the AppLoader itself
136140
if cam:
137141
from .videostream_frame import module as stream_module
138142

139-
stream_module.set_kwargs(stream=cam)
143+
stream_module.set_kwargs(stream=cam, app=self.app)
140144
modules.insert(0, stream_module)
141145

142-
self.root = root
143-
144146
self.module_frame = Frame(root)
145147
self.module_frame.pack(side='top', fill='both', expand=True)
146-
147-
self.app = AppLoader()
148148
self.app.load(modules, self.module_frame)
149149

150150
self.root.wm_title(instamatic.__long_title__)

src/instamatic/gui/jobs.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77
from __future__ import annotations
88

99
import time
10-
from datetime import datetime
11-
12-
import PIL.Image
13-
14-
from instamatic.formats import read_tiff, write_tiff
15-
from instamatic.processing.flatfield import apply_flatfield_correction
1610

1711

1812
def microscope_control(controller, **kwargs):
@@ -34,37 +28,6 @@ def collect_flatfield(controller, **kwargs):
3428
flatfield.collect_flatfield(controller.ctrl, confirm=False, drc=drc, **kwargs)
3529

3630

37-
def save_frame(controller, **kwargs):
38-
frame = kwargs.get('frame')
39-
40-
module_io = controller.app.get_module('io')
41-
42-
drc = module_io.get_experiment_directory()
43-
drc.mkdir(exist_ok=True, parents=True)
44-
45-
timestamp = datetime.now().strftime('%H-%M-%S.%f')[:-3] # cut last 3 digits for ms res.
46-
outfile = drc / f'frame_{timestamp}.tiff'
47-
48-
try:
49-
flatfield, h = read_tiff(module_io.get_flatfield())
50-
frame = apply_flatfield_correction(frame, flatfield)
51-
except BaseException:
52-
frame = frame
53-
h = {}
54-
55-
write_tiff(outfile, frame, header=h)
56-
print('Wrote file:', outfile)
57-
58-
59-
def save_image(controller, image: PIL.Image.Image, **_):
60-
drc = controller.app.get_module('io').get_experiment_directory()
61-
drc.mkdir(exist_ok=True, parents=True)
62-
timestamp = datetime.now().strftime('%H-%M-%S.%f')[:-3] # cut last 3 digits for ms res.
63-
out_path = drc / f'image_{timestamp}.png'
64-
image.save(out_path, format='PNG')
65-
print('Wrote file:', out_path)
66-
67-
6831
def toggle_difffocus(controller, **kwargs):
6932
toggle = kwargs['toggle']
7033

@@ -95,8 +58,6 @@ def relax_beam(controller, **kwargs):
9558
JOBS = {
9659
'ctrl': microscope_control,
9760
'flatfield': collect_flatfield,
98-
'save_frame': save_frame,
99-
'save_image': save_image,
10061
'toggle_difffocus': toggle_difffocus,
10162
'relax_beam': relax_beam,
10263
}

src/instamatic/gui/videostream_frame.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@
22

33
import threading
44
import time
5+
from datetime import datetime
6+
from pathlib import Path
57
from tkinter import *
68
from tkinter import Label as TkLabel
79
from tkinter.ttk import *
8-
from typing import Union
10+
from typing import Optional, Union
911

1012
import numpy as np
1113
from PIL import Image, ImageTk
1214
from PIL.Image import Resampling
1315

16+
from instamatic._typing import AnyPath
17+
from instamatic.formats import read_tiff, write_tiff
1418
from instamatic.gui.base_module import BaseModule, HasQMixin
1519
from instamatic.gui.click_dispatcher import ClickDispatcher
1620
from instamatic.gui.videostream_processor import VideoStreamProcessor
21+
from instamatic.processing import apply_flatfield_correction
1722
from instamatic.utils.spinbox import Spinbox
1823

1924

@@ -221,13 +226,33 @@ def update_display_range(self, name, index, mode):
221226
except BaseException:
222227
pass
223228

224-
def save_frame(self):
229+
def save_frame(self, frame: Optional[np.ndarray] = None, path: Optional[AnyPath] = None):
225230
"""Save currently shown raw frame from the stream to a file in cwd."""
226-
self.q.put(('save_frame', {'frame': self.frame}))
231+
frame = frame if frame is not None else self.processor.frame
232+
path = path or Path(self._saving_path_template().format('frame', 'tiff'))
233+
try:
234+
flatfield, _ = read_tiff(self.app.get_module('io').get_flatfield())
235+
frame = apply_flatfield_correction(frame, flatfield)
236+
except BaseException:
237+
frame = frame
238+
write_tiff(path, frame)
239+
print('Wrote frame:', path)
227240

228-
def save_image(self):
241+
def save_image(self, image: Optional[Image.Image] = None, path: Optional[AnyPath] = None):
229242
"""Save currently shown, modified, & scaled image to a file in cwd."""
230-
self.q.put(('save_image', {'image': self.processor.image}))
243+
image = image if image is not None else self.processor.image
244+
path = path or Path(self._saving_path_template().format('image', 'png'))
245+
image.save(path, format='PNG')
246+
print('Wrote image:', path)
247+
248+
def _saving_path_template(self) -> str:
249+
try:
250+
drc = self.app.get_module('io').get_experiment_directory()
251+
drc.mkdir(exist_ok=True, parents=True)
252+
except (AttributeError, FileExistsError, PermissionError):
253+
drc = Path.cwd()
254+
timestamp = datetime.now().strftime('%H-%M-%S_%f')[:-3]
255+
return str(drc / f'{{}}_{timestamp}.{{}}')
231256

232257
def close(self):
233258
self.stream.close()

src/instamatic/gui/videostream_processor.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import Any, Iterator, Literal, Optional, Protocol, Union
99

1010
import numpy as np
11-
import PIL.Image
1211
from matplotlib.figure import Figure
1312
from PIL import Image, ImageDraw
1413

0 commit comments

Comments
 (0)