-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Finish setting up an ophyd_async OAV #857
Merged
Merged
Changes from all commits
Commits
Show all changes
81 commits
Select commit
Hold shift + click to select a range
b2d7b1b
Set up skeleton of async mjpg
noemifrisina bf59cce
Start filling things in
noemifrisina 579bac1
Try using completed_status
noemifrisina 67e774c
Add SnapshotWithBeamCentre
noemifrisina dd0e707
Add SnapshotWithBeamCentre
noemifrisina 2a1ed7f
A first try at adding snapshot to oav
noemifrisina 75e83b5
Rename
noemifrisina e47699b
Start adding async grid overlay
noemifrisina 6d28b69
Actually add file
noemifrisina 866addb
Start adding a test - might be deleted
noemifrisina 07ffa05
Move mjpg to plugins and get trigger working with async
noemifrisina 4d9c485
Remove microns per pixels from mjpg
noemifrisina 76eecae
Pass an instance of soft signal beam_centre and fix imports
noemifrisina 3553bfc
Fix some typing and signals
noemifrisina a15765c
A bunch of fixmes
noemifrisina a0092a3
A bunch of fixmes with test
noemifrisina b5f6e7c
Improving file saving to make async and looking for the problem
noemifrisina 233abc9
Merge branch 'main' into 824_mjpg-async-device
noemifrisina 51d45f7
A passing test
noemifrisina 46c4a33
get test to pass
noemifrisina f6af967
Tidy up and fix tests
noemifrisina d321c16
Add a test for snapshot with grid
noemifrisina d316812
Merge branch 'main' into 824_mjpg-async-device
noemifrisina 13edeca
Fix oav
noemifrisina 126f687
Start breaking things: remove old OAV
noemifrisina 25166a4
Rename oav async file
noemifrisina a5b800b
Remove old MJPG code and move around some files
noemifrisina 83429ac
Rename
noemifrisina c6a518b
Instantiate async oav on beamlines
noemifrisina 3294404
Tidy up some tests
noemifrisina 1899d5d
Add commented out test
noemifrisina 35d4af7
Add a sort of similar test for utils
noemifrisina e0546c4
Remove some prints
noemifrisina 84a7098
Use async oav in system test
noemifrisina 0f44f68
Remove some old tests and add to new ones
noemifrisina a27afb9
Remove tests for old oav
noemifrisina dab5697
Remove old OAVConfigParams
noemifrisina dc3650b
Fix import
noemifrisina fe599ed
Hopefully fix system test
noemifrisina caef818
Fix more imports
noemifrisina acc1bd0
Add small missing test
noemifrisina c7b78cd
Fix snapshot prefixes
noemifrisina 1b03b35
Fix snapshot prefixes - part 2
noemifrisina 5358486
Merge branch 'main' into 824_mjpg-async-device
noemifrisina dcaa8fd
Add small test
noemifrisina b4ec7a2
Try to make codecov happy
noemifrisina fe259d7
Fix test
noemifrisina eb32c5a
Add a small cam plugin for oav since we use it
noemifrisina 5a03213
Try to fix test
noemifrisina 6541cdd
Add test for parameters
noemifrisina 9f13205
Add array sizes
noemifrisina df612d8
Merge branch 'main' into 824_mjpg-async-device
noemifrisina b2d0dbe
Add check for zoom level and test
noemifrisina 9c8a3cc
Fix linting
noemifrisina c0914a6
Really fix linting
noemifrisina bffbd29
Merge branch 'main' into 824_mjpg-async-device
noemifrisina 6f19281
Fix docstring
noemifrisina f30a519
Change i04 oav instatiation
noemifrisina 11826ae
Add connect
noemifrisina 8d0567c
Merge branch 'main' into 824_mjpg-async-device
noemifrisina 91ae0a6
Remove i10_4 again
noemifrisina 9431b34
Fix CAM plugin
noemifrisina fa965e5
Remove unused signals
noemifrisina 9705c1e
Do no catch exception when failing to create snapshot
noemifrisina 51bf8e8
Put format insto constant
noemifrisina 1bd7ebb
Remove unused oav error file and redefine zoom level error
noemifrisina 83503a4
Fix super
noemifrisina 1da64f3
Fix typo
noemifrisina 05065e4
Remove extra connect
noemifrisina 367c02d
Remove subscription id for snapshot
noemifrisina ea51f05
Fix string enum
noemifrisina bcc5d12
Pull draw_crosshair out of class
noemifrisina 1a1c1d9
Mock describe instead of method
noemifrisina 76ccda1
Have a util function for saving image with asyncio
noemifrisina 27c4b64
Use raise_for_status in clientsession
noemifrisina 88ab363
Tidy up mocks in snapshots
noemifrisina a12ca18
Separate test for mkdir
noemifrisina eeb8daf
Add readables
noemifrisina 7f4482e
More add_children_as_readables
noemifrisina 8825f18
Use IMG_FORMAT
noemifrisina 598d256
Merge branch 'main' into 824_mjpg-async-device
noemifrisina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from enum import Enum | ||
|
||
from ophyd_async.core import StandardReadable | ||
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw | ||
|
||
|
||
class ColorMode(str, Enum): | ||
""" | ||
Enum to store the various color modes of the camera. We use RGB1. | ||
""" | ||
|
||
MONO = "Mono" | ||
BAYER = "Bayer" | ||
RGB1 = "RGB1" | ||
RGB2 = "RGB2" | ||
RGB3 = "RGB3" | ||
YUV444 = "YUV444" | ||
YUV422 = "YUV422" | ||
YUV421 = "YUV421" | ||
|
||
|
||
class Cam(StandardReadable): | ||
def __init__(self, prefix: str, name: str = "") -> None: | ||
self.color_mode = epics_signal_rw(ColorMode, prefix + "ColorMode") | ||
self.acquire_period = epics_signal_rw(float, prefix + "AcquirePeriod") | ||
self.acquire_time = epics_signal_rw(float, prefix + "AcquireTime") | ||
self.gain = epics_signal_rw(float, prefix + "Gain") | ||
|
||
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV") | ||
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV") | ||
super().__init__(name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,138 +1,83 @@ | ||
import os | ||
import threading | ||
from abc import ABC, abstractmethod | ||
from io import BytesIO | ||
from pathlib import Path | ||
|
||
import requests | ||
from ophyd import Component, Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal | ||
from PIL import Image, ImageDraw | ||
import aiofiles | ||
from aiohttp import ClientSession | ||
from bluesky.protocols import Triggerable | ||
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw | ||
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw | ||
from PIL import Image | ||
|
||
from dodal.devices.oav.oav_parameters import OAVConfigParams | ||
from dodal.log import LOGGER | ||
|
||
IMG_FORMAT = "png" | ||
|
||
class MJPG(Device, ABC): | ||
|
||
async def asyncio_save_image(image: Image.Image, path: str): | ||
buffer = BytesIO() | ||
image.save(buffer, format=IMG_FORMAT) | ||
async with aiofiles.open(path, "wb") as fh: | ||
await fh.write(buffer.getbuffer()) | ||
|
||
|
||
class MJPG(StandardReadable, Triggerable, ABC): | ||
"""The MJPG areadetector plugin creates an MJPG video stream of the camera's output. | ||
This devices uses that stream to grab images. When it is triggered it will send the | ||
latest image from the stream to the `post_processing` method for child classes to handle. | ||
""" | ||
|
||
filename = Component(Signal) | ||
directory = Component(Signal) | ||
last_saved_path = Component(Signal) | ||
url = Component(EpicsSignal, "JPG_URL_RBV", string=True) | ||
x_size = Component(EpicsSignalRO, "ArraySize1_RBV") | ||
y_size = Component(EpicsSignalRO, "ArraySize2_RBV") | ||
input_rbpv = Component(EpicsSignalRO, "NDArrayPort_RBV") | ||
input_plugin = Component(EpicsSignal, "NDArrayPort") | ||
def __init__(self, prefix: str, name: str = "") -> None: | ||
self.url = epics_signal_rw(str, prefix + "JPG_URL_RBV") | ||
|
||
# scaling factors for the snapshot at the time it was triggered | ||
microns_per_pixel_x = Component(Signal) | ||
microns_per_pixel_y = Component(Signal) | ||
self.x_size = epics_signal_r(int, prefix + "ArraySize1_RBV") | ||
self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV") | ||
|
||
oav_params: OAVConfigParams | None = None | ||
with self.add_children_as_readables(): | ||
self.filename = soft_signal_rw(str) | ||
self.directory = soft_signal_rw(str) | ||
self.last_saved_path = soft_signal_rw(str) | ||
|
||
KICKOFF_TIMEOUT: float = 30.0 | ||
self.KICKOFF_TIMEOUT = 30.0 | ||
|
||
def _save_image(self, image: Image.Image): | ||
"""A helper function to save a given image to the path supplied by the directory | ||
and filename signals. The full resultant path is put on the last_saved_path signal | ||
super().__init__(name) | ||
|
||
async def _save_image(self, image: Image.Image): | ||
"""A helper function to save a given image to the path supplied by the \ | ||
directory and filename signals. The full resultant path is put on the \ | ||
last_saved_path signal | ||
""" | ||
filename_str = self.filename.get() | ||
directory_str: str = self.directory.get() # type: ignore | ||
filename_str = await self.filename.get_value() | ||
directory_str = await self.directory.get_value() | ||
|
||
path = Path(f"{directory_str}/{filename_str}.png").as_posix() | ||
if not os.path.isdir(Path(directory_str)): | ||
path = Path(f"{directory_str}/{filename_str}.{IMG_FORMAT}").as_posix() | ||
if not Path(directory_str).is_dir(): | ||
LOGGER.info(f"Snapshot folder {directory_str} does not exist, creating...") | ||
os.mkdir(directory_str) | ||
Path(directory_str).mkdir(parents=True) | ||
|
||
LOGGER.info(f"Saving image to {path}") | ||
image.save(path) | ||
self.last_saved_path.put(path) | ||
|
||
def trigger(self): | ||
await asyncio_save_image(image, path) | ||
|
||
await self.last_saved_path.set(path, wait=True) | ||
|
||
@AsyncStatus.wrap | ||
async def trigger(self): | ||
"""This takes a snapshot image from the MJPG stream and send it to the | ||
post_processing method, expected to be implemented by a child of this class. | ||
It is the responsibility of the child class to save any resulting images. | ||
It is the responsibility of the child class to save any resulting images by \ | ||
calling _save_image. | ||
""" | ||
st = DeviceStatus(device=self, timeout=self.KICKOFF_TIMEOUT) | ||
url_str = self.url.get() | ||
url_str = await self.url.get_value() | ||
|
||
assert isinstance( | ||
self.oav_params, OAVConfigParams | ||
), "MJPG does not have valid OAV parameters" | ||
self.microns_per_pixel_x.set(self.oav_params.micronsPerXPixel) | ||
self.microns_per_pixel_y.set(self.oav_params.micronsPerYPixel) | ||
|
||
def get_snapshot(): | ||
try: | ||
response = requests.get(url_str, stream=True) | ||
response.raise_for_status() | ||
with Image.open(BytesIO(response.content)) as image: | ||
self.post_processing(image) | ||
st.set_finished() | ||
except requests.HTTPError as e: | ||
st.set_exception(e) | ||
|
||
threading.Thread(target=get_snapshot, daemon=True).start() | ||
|
||
return st | ||
async with ClientSession(raise_for_status=True) as session: | ||
async with session.get(url_str) as response: | ||
data = await response.read() | ||
with Image.open(BytesIO(data)) as image: | ||
await self.post_processing(image) | ||
|
||
@abstractmethod | ||
def post_processing(self, image: Image.Image): | ||
async def post_processing(self, image: Image.Image): | ||
pass | ||
|
||
|
||
class SnapshotWithBeamCentre(MJPG): | ||
"""A child of MJPG which, when triggered, draws an outlined crosshair at the beam | ||
centre in the image and saves the image to disk.""" | ||
|
||
CROSSHAIR_LENGTH_PX = 20 | ||
CROSSHAIR_OUTLINE_COLOUR = "Black" | ||
CROSSHAIR_FILL_COLOUR = "White" | ||
|
||
def post_processing(self, image: Image.Image): | ||
assert ( | ||
self.oav_params is not None | ||
), "Snapshot device does not have valid OAV parameters" | ||
beam_x = self.oav_params.beam_centre_i | ||
beam_y = self.oav_params.beam_centre_j | ||
|
||
SnapshotWithBeamCentre.draw_crosshair(image, beam_x, beam_y) | ||
|
||
self._save_image(image) | ||
|
||
@classmethod | ||
def draw_crosshair(cls, image: Image.Image, beam_x: int, beam_y: int): | ||
draw = ImageDraw.Draw(image) | ||
OUTLINE_WIDTH = 1 | ||
HALF_LEN = cls.CROSSHAIR_LENGTH_PX / 2 | ||
draw.rectangle( | ||
[ | ||
beam_x - OUTLINE_WIDTH, | ||
beam_y - HALF_LEN - OUTLINE_WIDTH, | ||
beam_x + OUTLINE_WIDTH, | ||
beam_y + HALF_LEN + OUTLINE_WIDTH, | ||
], | ||
fill=cls.CROSSHAIR_OUTLINE_COLOUR, | ||
) | ||
draw.rectangle( | ||
[ | ||
beam_x - HALF_LEN - OUTLINE_WIDTH, | ||
beam_y - OUTLINE_WIDTH, | ||
beam_x + HALF_LEN + OUTLINE_WIDTH, | ||
beam_y + OUTLINE_WIDTH, | ||
], | ||
fill=cls.CROSSHAIR_OUTLINE_COLOUR, | ||
) | ||
draw.line( | ||
((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)), | ||
fill=cls.CROSSHAIR_FILL_COLOUR, | ||
) | ||
draw.line( | ||
((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)), | ||
fill=cls.CROSSHAIR_FILL_COLOUR, | ||
) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: these aren't connecting in
dodal connect i03
as you're missing a colon