Skip to content

Commit 941750b

Browse files
authored
Bugfixes to support FEI Titan microscope and camera (#154)
* E-RC: added commented fix to camera client eval dict, may be needed if image is too large * Attempts at hot-fixing beamshift calibration * E-RC: take into account the factor of binning when tracking @ FastADT * Revert "E-RC: take into account the factor of binning when tracking @ FastADT" This reverts commit 24cc9fa. * E-RC: take into account the factor of binning when tracking @ FastADT + display them properly * E-RC: fix the coordinate order in ctrl frame toggle_rmb_beam callback * E-RC: Refactor annotate_videostream to use binsize parameter * E-RC: patch instamatic RPC to allow get_movie generator * E-RC: Fixes to type passed, generator unpacking * ER-C: remove unused socket protocol, comments, debug statements * ER-C: allow handling generators (get_movie) by the cam server * Update documentation with relevant changes from https://sites.google.com/view/instamatic-on-titan * Update `docs/requirements.txt` to prevent ongoing version skew
1 parent 2350cab commit 941750b

11 files changed

Lines changed: 76 additions & 27 deletions

File tree

docs/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ This file holds the specifications of the camera. This file is must be located t
207207
: Give the interface of the camera interface to connect to, for example: `timepix`/`emmenu`/`simulate`/`gatan`/`merlin`. Leave blank or set to `None` to load the camera specs, but do not load the camera module (this also turns off the videostream gui).
208208

209209
**dead_time**
210-
: Set the dead time (i.e. the gap between acquisitions) of the detector; if this value (`camera.dead_time`) is not set but required, Instamatic might attempt to use `CalibMovieDelays.dead_time` value calibrated via `instamatic.calibrate_movie_delays` instead. Typically, Instamatic will not run this calibration automatically: the user needs to either set `camera.dead_time` or call `instamatic.calibrate_movie_delays` themselves.
210+
: Set the dead time (i.e. the gap between acquisitions) of the detector. This value is especially important for cameras what work remotely or otherwise feature dead time significant when compared to typical data collection time. If `camera.dead_time` is not set but required, Instamatic might attempt to use `CalibMovieDelays.dead_time` value calibrated via `instamatic.calibrate_movie_delays` instead. Typically, Instamatic will not run this calibration automatically: the user needs to either set `camera.dead_time` or call `instamatic.calibrate_movie_delays` themselves.
211211

212212
**default_binsize**
213213
: Set the default binsize, default: `1`.

docs/network.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use_cam_server: False
4040
4141
## Example 2
4242
43-
This is an example where the microscope and camera PCs should be controlled through an intermediate support PC.
43+
In this example, the microscope and camera are controlled by a dedicated computer(s), distinct from an intermediate support PC running the Instamatic GUI. This scenario is preferred if, for any reason, Instamatic can not be fully installed on the Microscope/Camera PC such as when using [instamatic-tecnai-server](https://github.com/instamatic-dev/instamatic-tecnai-server) in lieu of the full Instamatic.
4444
4545
If your camera can be controlled directly through TCP/IP, such as the MerlinEM or ASI Cheetah (via `serval`), do not use `instamatic.camserver`, but connect directly to the IP. For example, for Merlin.
4646

@@ -72,6 +72,8 @@ cam_server_port: 8088
7272
cam_use_shared_memory: False
7373
```
7474

75+
A case of a setup where the Microscope PC supports both the TEM and a camera server via the Tecnai server while the main GUI runs on a separate Support PC is partially discussed [here](https://sites.google.com/view/instamatic-on-titan).
76+
7577
## Example 3
7678

7779
If your camera cannot be controlled through TCP/IP, you might try this solution. This seems to be a common setup for TFS/FEI microscopes.

docs/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ mkdocs
33
mkdocs-jupyter
44
mkdocs-gen-files
55
mkdocs-material
6-
mkdocstrings[python]
6+
mkdocstrings>=0.26
7+
mkdocstrings-python>=1.10

docs/setup.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ If you are using a JEOL TEM, make sure `instamatic` is installed on a computer w
88

99
## FEI
1010

11-
For FEI microscopes, `instamatic` must be installed on the microscope control PC. Alternatively, it can be installed on both the microscope PC and the camera PC, running `instamatic.temserver` on the microscope PC, and establishing a connection over the local network. See the config documentation for how to set this up.
11+
For FEI microscopes, `instamatic` must be installed on the microscope control PC. Alternatively, it can be installed on both the microscope PC and the camera PC, running `instamatic.temserver` on the microscope PC, and establishing a connection over the local network. If any server PC does not support modern software, [instamatic-dev/instamatic-tecnai-server](https://github.com/instamatic-dev/instamatic-tecnai-server) is a drop-in replacement that requires Python 3.4 only.
12+
13+
See the config documentation for how to set this up.
1214

1315
## Development version
1416

@@ -32,7 +34,7 @@ In order of priority:
3234
### __2. Set up the microscope interface__
3335
Go to the config directory from the first step.
3436

35-
In `config/settings.yaml` define the camera interface you want to use. You can use the autoconfig tool or one of the example files and modify those. You can name these files anything you want, as long as the name under `microscope` matches the filename in `config/microscope`
37+
In `config/settings.yaml` define the camera interface you want to use. You can use the autoconfig tool or one of the example files and modify those. You can name these files anything you want, as long as the name under `microscope` matches the filename in `config/microscope`.
3638

3739
### __3. Set up the magnifications and camera lengths__
3840
In the config file, i.e `config/microscope/jeol.yaml`, set the correct camera lengths (`ranges/diff`) and magnifications for your microscopes (`ranges/lowmag` and `ranges/mag1`). Also make sure you set the wavelength. Again, the autoconfig tool is your best friend, otherwise, the way to get those numbers is to simply write them down as you turn the magnification knob on the microcope.
@@ -41,10 +43,10 @@ In order of priority:
4143
Specify the file you want to use for the camera interface, i.e. `camera: timepix` points to `config/camera/timepix.yaml`. In this file, make sure that the interface is set to your camera type and update the numbers as specified in the config documentation. If you do not want to set up the camera interface at this moment, you can use `camera: simulate` to fake the camera connection.
4244

4345
### __5. Make the calibration table__
44-
For each of the magnfications defined in `config/microscope/jeol.yaml`, specify the pixel sizes in the file defined by `calibration: jeol`, corresponding to the file `calibration/jeol.yaml`. For starters, you can simply set the calibration values to 1.0.
46+
For each of the magnifications defined in `config/microscope/jeol.yaml`, specify the pixel sizes in the file defined by `calibration: jeol`, corresponding to the file `calibration/jeol.yaml`. For starters, you can simply set the calibration values to 1.0.
4547

4648
### __6. Test if it works__
47-
Run `instamatic.temcontroller` to start a IPython shell that initializes the connection. It should run with no crashes or warnings.
49+
Run `instamatic.controller` to start a IPython shell that initializes the connection. It should run with no crashes or warnings.
4850

4951
### __7. Update `settings.yaml`__
5052
There are a few more choices to make in `instamatic/settings.yaml`. If you use a TVIPS camera, make sure you put `use_cam_server: true`.

readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ Cameras supported:
2222
- ASI CheeTah through `serval-toolkit` library
2323
- TVIPS cameras through EMMENU4 API
2424
- Quantum Detectors MerlinEM
25+
- Gatan cameras through FEI scripting interface
2526
- (Gatan cameras through DM plugin [1])
2627

2728
Instamatic has been developed on a JEOL-2100 with a Timepix camera, and a JEOL-1400 and JEOL-3200 with TVIPS cameras (XF416/F416).
2829

29-
See [instamatic-dev/instamatic-tecnai-server](https://github.com/instamatic-dev/instamatic-tecnai-server) for a TEM interface to control a FEI Tecnai-TEM on Windows XP/Python 3.4 via instamatic.
30+
See [instamatic-dev/instamatic-tecnai-server](https://github.com/instamatic-dev/instamatic-tecnai-server) for a TEM interface to control a FEI Tecnai or FEI Titan TEM and associated cameras on Windows XP/Python 3.4 via instamatic.
3031

3132
[1]: Support for Gatan cameras is somewhat underdeveloped. As an alternative, a DigitalMicrograph script for collecting cRED data on a OneView camera (or any other Gatan camera) can be found [here](https://github.com/instamatic-dev/InsteaDMatic).
3233

@@ -47,7 +48,7 @@ pip install instamatic
4748

4849
## OS requirement
4950

50-
The package requires Windows 7 or higher. It has been mainly developed and tested under windows 7 and higher.
51+
The package requires Windows 7 or higher. It has been mainly developed and tested under Windows 7 and higher.
5152

5253
## Package dependencies
5354

src/instamatic/calibrate/calibrate_beamshift.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ def live(
101101
) -> Self:
102102
while True:
103103
c = calibrate_beamshift(ctrl=ctrl, save_images=True, outdir=outdir)
104-
with c.annotate_videostream(vsp) if vsp else nullcontext():
104+
binsize = ctrl.cam.default_binsize
105+
with c.annotate_videostream(vsp, binsize) if vsp else nullcontext():
105106
if input(' >> Accept? [y/n] ') == 'y':
106107
return c
107108

@@ -127,15 +128,15 @@ def plot(self, to_file: Optional[AnyPath] = None):
127128
plt.show()
128129

129130
@contextmanager
130-
def annotate_videostream(self, vsp: Optional[VideoStreamProcessor] = None) -> None:
131+
def annotate_videostream(self, vsp: VideoStreamProcessor, binsize: int = 1) -> None:
131132
shifts = np.dot(self.shifts, np.linalg.inv(self.transform))
132133
ins: list[DeferredImageDraw.Instruction] = []
133134

134135
vsp.temporary_frame = np.max(self.images, axis=0)
135136
print('Determined (blue) vs calibrated (orange) beam positions:')
136137
for p, s in zip(self.pixels, shifts):
137-
p = (p + self.reference_pixel)[::-1] # xy coords inverted for plot
138-
s = (s + self.reference_pixel)[::-1] # xy coords inverted for plot
138+
p = (p + self.reference_pixel)[::-1] / binsize # xy coords inverted for plot
139+
s = (s + self.reference_pixel)[::-1] / binsize # xy coords inverted for plot
139140
ins.append(vsp.draw.circle(p, radius=3, fill='blue'))
140141
ins.append(vsp.draw.circle(s, radius=3, fill='orange'))
141142
ins.append(vsp.draw.circle(self.reference_pixel[::-1], radius=3, fill='black'))

src/instamatic/camera/camera_client.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import threading
77
import time
88
from functools import wraps
9+
from typing import Any, Generator
910

1011
import numpy as np
1112

@@ -126,11 +127,7 @@ def _eval_dct(self, dct):
126127
with self._eval_lock:
127128
self.s.send(dumper(dct))
128129

129-
acquiring_image = dct['attr_name'] == 'get_image'
130-
acquiring_movie = dct['attr_name'] == 'get_movie'
131-
132-
if acquiring_movie:
133-
raise NotImplementedError('Acquiring movies over a socket is not supported.')
130+
acquiring_image = dct['attr_name'] in {'get_image', 'get_movie', '__gen_next__'}
134131

135132
if acquiring_image and not self.use_shared_memory:
136133
response = self.s.recv(self._imagebufsize)
@@ -146,6 +143,8 @@ def _eval_dct(self, dct):
146143
data = self.get_data_from_shared_memory(**data)
147144

148145
if status == 200:
146+
if isinstance(data, dict) and '__generator__' in data:
147+
return self._wrap_remote_generator(data['__generator__'])
149148
return data
150149

151150
elif status == 500:
@@ -206,3 +205,20 @@ def block(self):
206205

207206
def unblock(self):
208207
raise NotImplementedError('This camera cannot be streamed.')
208+
209+
def _wrap_remote_generator(self, gen_id: str) -> Generator[Any]:
210+
"""Pass a reference to yield from a remote __generator__ with id."""
211+
212+
def generator():
213+
kwargs = {'id': gen_id}
214+
try:
215+
while True:
216+
dct = {'attr_name': '__gen_next__', 'kwargs': kwargs}
217+
value = self._eval_dct(dct)
218+
if value is None:
219+
return
220+
yield value
221+
finally:
222+
self._eval_dct({'attr_name': '__gen_close__', 'kwargs': kwargs})
223+
224+
return generator()

src/instamatic/camera/videostream.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ def run(self):
7272
if self.acquireInitiateEvent.is_set():
7373
r = self.request
7474
self.acquireInitiateEvent.clear()
75-
e = r.exposure if r.exposure else self.default_exposure
76-
b = r.binsize if r.binsize else self.default_binsize
75+
e = float(r.exposure if r.exposure else self.default_exposure)
76+
b = int(r.binsize if r.binsize else self.default_binsize)
7777
if isinstance(r, ImageRequest):
7878
media = self.cam.get_image(exposure=e, binsize=b)
7979
self.callback(media, request=r)

src/instamatic/experiments/fast_adt/experiment.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ def __init__(
213213
self.flatfield = flatfield
214214
self.fast_adt_frame = experiment_frame
215215
self.beamshift: Optional[CalibBeamShift] = None
216+
self.binsize: int = 1
216217
self.camera_length: int = 0
217218

218219
if videostream_frame is not None:
@@ -337,6 +338,7 @@ def start_collection(self, **params) -> None:
337338

338339
with self.ctrl.beam.blanked(), self.ctrl.cam.blocked():
339340
if params['tracking_algo'] == 'manual':
341+
self.binsize = self.ctrl.cam.default_binsize
340342
self.runs.tracking = TrackingRun.from_params(params)
341343
self.determine_pathing_manually()
342344
for pathing_run in self.runs.pathing:
@@ -366,8 +368,8 @@ def displayed_pathing(self, step: Step) -> None:
366368
draw = self.videostream_processor.draw
367369
instructions: list[draw.Instruction] = []
368370
for run_i, p in enumerate(self.runs.pathing):
369-
x = p.table.at[step.Index, 'beampixel_x']
370-
y = p.table.at[step.Index, 'beampixel_y']
371+
x = p.table.at[step.Index, 'beampixel_x'] / self.binsize
372+
y = p.table.at[step.Index, 'beampixel_y'] / self.binsize
371373
instructions.append(draw.circle((x, y), fill='white', radius=5))
372374
instructions.append(draw.circle((x, y), fill=get_color(run_i), radius=3))
373375
try:
@@ -388,7 +390,7 @@ def determine_pathing_manually(self) -> None:
388390
self.beamshift = self.get_beamshift()
389391
self.msg1('Locate the beam (move it if needed) and click on its center.')
390392
with self.click_listener as cl:
391-
obs_beampixel_xy = np.array(cl.get_click().xy)
393+
obs_beampixel_xy = np.array(cl.get_click().xy) * self.binsize
392394
cal_beampixel_yx = self.beamshift.beamshift_to_pixelcoord(self.ctrl.beamshift.get())
393395

394396
self.ctrl.restore('FastADT_track')
@@ -401,11 +403,12 @@ def determine_pathing_manually(self) -> None:
401403
self.msg1(f'Click on tracked point: {step.summary}.')
402404
with self.displayed_pathing(step=step), self.click_listener:
403405
click = self.click_listener.get_click()
404-
delta_yx = (np.array(click.xy) - obs_beampixel_xy)[::-1]
406+
click_xy = np.array(click.xy) * self.binsize
407+
delta_yx = (click_xy - obs_beampixel_xy)[::-1]
405408
click_beampixel_yx = cast(Sequence[float], cal_beampixel_yx + delta_yx)
406409
click_beamshift_xy = self.beamshift.pixelcoord_to_beamshift(click_beampixel_yx)
407410
cols = ['beampixel_x', 'beampixel_y', 'beamshift_x', 'beamshift_y']
408-
run.table.loc[step.Index, cols] = *click.xy, *click_beamshift_xy
411+
run.table.loc[step.Index, cols] = *click_xy, *click_beamshift_xy
409412
tracking_frames.append(step.image)
410413
if 'image' not in run.table:
411414
run.table['image'] = tracking_frames

src/instamatic/gui/ctrl_frame.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,13 @@ def toggle_rmb_beam(self, _name, _index, _mode) -> None:
364364
self.var_rmb_beam.set(False)
365365
return
366366

367+
binning = self.ctrl.cam.default_binsize
368+
367369
def _callback(click: ClickEvent) -> None:
368370
if click.button == MouseButton.RIGHT:
369-
bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x))
371+
pixel_x = click.x * binning
372+
pixel_y = click.y * binning
373+
bs = calib_beamshift.pixelcoord_to_beamshift((pixel_y, pixel_x))
370374
self.ctrl.beamshift.set(*[float(b) for b in bs])
371375

372376
d.add_listener('rmb_beam', _callback, active=True)

0 commit comments

Comments
 (0)