Skip to content

Commit f560ee7

Browse files
Create a device that will give the max pixel from an AD camera (#1723)
* first try * add trigger function which writes to soft signals * small changes * get rid of max pixel location * add tests - need modifying as made with copilot * make triggerable for blesky plan * start to add max-pixel device in i04 (WIP because on beamline) * Finish adding device to i04.py * Add changes from beamline testing - from github * add comment for kernal size * attemt own tests * add my tests * add assert called onece tests * make changes from Dom's comments * add greyscale test * remove print statements in test * Fix merge issue --------- Co-authored-by: Dominic Oram <[email protected]>
1 parent 7668e7a commit f560ee7

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

src/dodal/beamlines/i04.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from dodal.devices.i03.dcm import DCM
2424
from dodal.devices.i04.beamsize import Beamsize
2525
from dodal.devices.i04.constants import RedisConstants
26+
from dodal.devices.i04.max_pixel import MaxPixel
2627
from dodal.devices.i04.murko_results import MurkoResultsDevice
2728
from dodal.devices.i04.transfocator import Transfocator
2829
from dodal.devices.ipin import IPin
@@ -390,6 +391,16 @@ def scintillator() -> Scintillator:
390391
)
391392

392393

394+
@device_factory()
395+
def max_pixel() -> MaxPixel:
396+
"""Get the i04 max pixel device, instantiate it if it hasn't already been.
397+
If this is called when already instantiated in i04, it will return the existing object.
398+
"""
399+
return MaxPixel(
400+
f"{PREFIX.beamline_prefix}-DI-OAV-01:",
401+
)
402+
403+
393404
@device_factory()
394405
def beamsize() -> Beamsize:
395406
"""Get the i04 beamsize device, instantiate it if it hasn't already been.

src/dodal/devices/i04/max_pixel.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import cv2
2+
import numpy as np
3+
from bluesky.protocols import Triggerable
4+
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
5+
from ophyd_async.epics.core import (
6+
epics_signal_r,
7+
)
8+
9+
# kernal size describes how many of the neigbouring pixels are used for the blur,
10+
# higher kernal size means more of a blur effect
11+
KERNAL_SIZE = (7, 7)
12+
13+
14+
class MaxPixel(StandardReadable, Triggerable):
15+
"""Gets the max pixel (brightest pixel) from the image after some image processing."""
16+
17+
def __init__(self, prefix: str, name: str = "") -> None:
18+
self.array_data = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
19+
self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
20+
super().__init__(name)
21+
22+
async def _convert_to_gray_and_blur(self):
23+
"""
24+
Preprocess the image array data (convert to grayscale and apply a gaussian blur)
25+
Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
26+
as we aren't interested in data relating to colour. A blur is then applied to mitigate
27+
errors due to rogue hot pixels.
28+
"""
29+
data = await self.array_data.get_value()
30+
gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
31+
return cv2.GaussianBlur(gray_arr, KERNAL_SIZE, 0)
32+
33+
@AsyncStatus.wrap
34+
async def trigger(self):
35+
arr = await self._convert_to_gray_and_blur()
36+
max_val = float(np.max(arr)) # np.int64
37+
assert isinstance(max_val, float)
38+
self._max_val_setter(max_val)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from collections.abc import AsyncGenerator
2+
from unittest.mock import ANY, AsyncMock, MagicMock, patch
3+
4+
import cv2
5+
import numpy as np
6+
import pytest
7+
from ophyd_async.core import init_devices, set_mock_value
8+
9+
from dodal.devices.i04.max_pixel import KERNAL_SIZE, MaxPixel
10+
11+
12+
@pytest.fixture
13+
async def max_pixel() -> AsyncGenerator[MaxPixel]:
14+
async with init_devices(mock=True):
15+
max_pixel = MaxPixel("TEST: MAX_PIXEL")
16+
yield max_pixel
17+
18+
19+
@pytest.mark.parametrize(
20+
"preprocessed_data, expected",
21+
[
22+
([1, 2, 3], 3), # test can handle standard input
23+
([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 0), # test can handle all 0's
24+
([-9, -8, 0], 0), # test can handle negatives
25+
([6.9, 8.9, 7.5, 6.45], 8.9), # check can handle floats
26+
],
27+
)
28+
@patch("dodal.devices.i04.max_pixel.MaxPixel._convert_to_gray_and_blur")
29+
async def test_returns_max(
30+
mocked_preprocessed_data: AsyncMock,
31+
preprocessed_data,
32+
expected,
33+
max_pixel: MaxPixel,
34+
):
35+
mocked_preprocessed_data.return_value = preprocessed_data
36+
await max_pixel.trigger()
37+
assert await max_pixel.max_pixel_val.get_value() == expected
38+
39+
40+
@patch("dodal.devices.i04.max_pixel.cv2.cvtColor")
41+
@patch("dodal.devices.i04.max_pixel.cv2.GaussianBlur")
42+
async def test_preprocessed_data_grayscale_is_called(
43+
mocked_cv2_blur: MagicMock, mocked_cv2_grey: MagicMock, max_pixel: MaxPixel
44+
):
45+
data = np.array([1])
46+
set_mock_value(max_pixel.array_data, data)
47+
await max_pixel._convert_to_gray_and_blur()
48+
mocked_cv2_grey.assert_called_once_with(data, cv2.COLOR_BGR2GRAY)
49+
mocked_cv2_blur.assert_called_once_with(ANY, KERNAL_SIZE, 0)
50+
51+
52+
test_arr = np.array(
53+
[
54+
[[123, 65, 0], [0, 0, 0], [0, 0, 0]],
55+
[[123, 65, 0], [0, 0, 0], [0, 0, 0]],
56+
[[123, 65, 0], [0, 0, 0], [0, 0, 0]],
57+
[[123, 65, 0], [0, 0, 0], [0, 0, 0]],
58+
],
59+
dtype=np.uint8,
60+
)
61+
62+
63+
async def test_greyscale_works(max_pixel: MaxPixel):
64+
test_arr_shape = test_arr.shape # (4, 3, 3)
65+
set_mock_value(max_pixel.array_data, test_arr)
66+
await max_pixel.array_data.get_value()
67+
processed_data = await max_pixel._convert_to_gray_and_blur()
68+
processed_data_shape = processed_data.shape # (4,3)
69+
70+
assert processed_data_shape[0] == test_arr_shape[0]
71+
assert processed_data_shape[1] == test_arr_shape[1]

0 commit comments

Comments
 (0)