Apply suggestions from code review of branch jpeg-capture-in-stacking
Co-authored-by: Beth Probert <beth_probert@outlook.com>
This commit is contained in:
parent
2d49bf2061
commit
0933ece63a
8 changed files with 43 additions and 15 deletions
|
|
@ -11,6 +11,11 @@ from openflexure_microscope_server.things.camera.picamera import StreamingPiCame
|
||||||
|
|
||||||
@fixture(scope="module")
|
@fixture(scope="module")
|
||||||
def client():
|
def client():
|
||||||
|
"""
|
||||||
|
A pytest fixture that initialises a test client for the StreamingPiCamera2 Thing.
|
||||||
|
This fixture sets up a ThingServer, registers a StreamingPiCamera2 instance at the
|
||||||
|
"/camera/" endpoint, and provides a ThingClient for interacting with it during tests.
|
||||||
|
"""
|
||||||
server = ThingServer()
|
server = ThingServer()
|
||||||
server.add_thing(StreamingPiCamera2(), "/camera/")
|
server.add_thing(StreamingPiCamera2(), "/camera/")
|
||||||
with TestClient(server.app) as test_client:
|
with TestClient(server.app) as test_client:
|
||||||
|
|
@ -30,13 +35,20 @@ def test_jpeg_and_array(client):
|
||||||
Check that grabbing a jpeg from the stream results in the same size
|
Check that grabbing a jpeg from the stream results in the same size
|
||||||
image as a array capture or a jpeg capture.
|
image as a array capture or a jpeg capture.
|
||||||
"""
|
"""
|
||||||
|
# Grab a jpeg from the stream
|
||||||
blob = client.grab_jpeg()
|
blob = client.grab_jpeg()
|
||||||
mjpeg_frame = Image.open(blob.open())
|
mjpeg_frame = Image.open(blob.open())
|
||||||
assert mjpeg_frame
|
assert mjpeg_frame
|
||||||
|
|
||||||
|
# Capture a jpeg
|
||||||
blob = client.capture_jpeg(resolution="main")
|
blob = client.capture_jpeg(resolution="main")
|
||||||
jpeg_capture = Image.open(blob.open())
|
jpeg_capture = Image.open(blob.open())
|
||||||
assert jpeg_capture
|
assert jpeg_capture
|
||||||
|
|
||||||
|
# Capture an array
|
||||||
arrlist = client.capture_array(stream_name="main")
|
arrlist = client.capture_array(stream_name="main")
|
||||||
array_main = np.array(arrlist)
|
array_main = np.array(arrlist)
|
||||||
|
|
||||||
|
# Verify image sizes are the same
|
||||||
assert mjpeg_frame.size == jpeg_capture.size
|
assert mjpeg_frame.size == jpeg_capture.size
|
||||||
assert array_main.shape[1::-1] == jpeg_capture.size
|
assert array_main.shape[1::-1] == jpeg_capture.size
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Check exposure times do not drift.
|
Check exposure times do not drift.
|
||||||
|
|
||||||
This can get very tedious. Recommende running pytest with -s option
|
This can get very tedious. Recommend running pytest with -s option
|
||||||
to monitor progress.
|
to monitor progress.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -38,16 +38,18 @@ def _test_exposure_time_drift(desired_time):
|
||||||
print(f"Pre-capture the time is set to {pre_capture_et}")
|
print(f"Pre-capture the time is set to {pre_capture_et}")
|
||||||
# Check exp is set correctly within known tolerance
|
# Check exp is set correctly within known tolerance
|
||||||
assert abs(pre_capture_et - desired_time) < exposure_tol
|
assert abs(pre_capture_et - desired_time) < exposure_tol
|
||||||
|
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
client.capture_jpeg(resolution="full")
|
client.capture_jpeg(resolution="full")
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# Exposure can update on first capture, due to frame rate restrictions
|
# Exposure can update on first capture, due to frame rate restrictions
|
||||||
first_et = client.exposure_time
|
first_et = client.exposure_time
|
||||||
assert abs(first_et - pre_capture_et) < exposure_tol
|
assert abs(first_et - pre_capture_et) < exposure_tol
|
||||||
frame_et = client.exposure_time
|
else:
|
||||||
print(f"Frame {i} captured with exposure time {frame_et}")
|
frame_et = client.exposure_time
|
||||||
# Check no further drift in value
|
print(f"Frame {i} captured with exposure time {frame_et}")
|
||||||
assert first_et == frame_et
|
# Check no further drift in value
|
||||||
|
assert first_et == frame_et
|
||||||
|
|
||||||
# Set the exposure time to the value it already is. To check it doesn't shift
|
# Set the exposure time to the value it already is. To check it doesn't shift
|
||||||
print(f"Setting exposure time to {frame_et} to check it doesn't change")
|
print(f"Setting exposure time to {frame_et} to check it doesn't change")
|
||||||
|
|
@ -62,6 +64,9 @@ def _test_exposure_time_drift(desired_time):
|
||||||
|
|
||||||
|
|
||||||
def test_exposure_time_drift():
|
def test_exposure_time_drift():
|
||||||
|
"""
|
||||||
|
Performs the exposure time test for a range of exposure time values.
|
||||||
|
"""
|
||||||
for desired_time in [100, 1000, 10000]:
|
for desired_time in [100, 1000, 10000]:
|
||||||
_test_exposure_time_drift(desired_time)
|
_test_exposure_time_drift(desired_time)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ def test_sensor_mode():
|
||||||
for size in [(3280, 2464), (1640, 1232)]:
|
for size in [(3280, 2464), (1640, 1232)]:
|
||||||
client.sensor_mode = {"output_size": size, "bit_depth": 10}
|
client.sensor_mode = {"output_size": size, "bit_depth": 10}
|
||||||
arr = np.array(client.capture_array(stream_name="raw"))
|
arr = np.array(client.capture_array(stream_name="raw"))
|
||||||
|
# Check that the array dimensions match the requested image size.
|
||||||
|
# Note: Numpy array shape is (y,x), but the sensor is set with (x,y)
|
||||||
|
# hence the need to compare index 0 with index 1.
|
||||||
assert arr.shape[0] == size[1]
|
assert arr.shape[0] == size[1]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,14 @@ def generate_bad_tuning():
|
||||||
return bad_tuning
|
return bad_tuning
|
||||||
|
|
||||||
|
|
||||||
def print_tuning(read_file=False):
|
def print_tuning(read_file: bool = False):
|
||||||
"""
|
"""
|
||||||
Print the path of the default tuning file from the the environment variable.
|
Print the path of the default tuning file from the the environment variable.
|
||||||
|
|
||||||
:param read_file: Boolean, set true to also print the file contents.
|
:param read_file: Boolean, set true to also print the file contents.
|
||||||
|
|
||||||
This is useful for debuging. As PyTest suppresses the printing by default the
|
This is useful for debugging. As pytest suppresses the printing by default the
|
||||||
-s option is needed when running pylint to see this.
|
-s option is needed when running pytest to see this.
|
||||||
"""
|
"""
|
||||||
key = "LIBCAMERA_RPI_TUNING_FILE"
|
key = "LIBCAMERA_RPI_TUNING_FILE"
|
||||||
if key in os.environ:
|
if key in os.environ:
|
||||||
|
|
@ -52,7 +52,7 @@ def print_tuning(read_file=False):
|
||||||
print("Tuning file environment variable not set")
|
print("Tuning file environment variable not set")
|
||||||
|
|
||||||
|
|
||||||
def _test_bad_tuning_after_good_tuning(configure):
|
def _test_bad_tuning_after_good_tuning(configure: bool = False):
|
||||||
"""
|
"""
|
||||||
Load the default tuning file into the camera, re-load with a broken tuning file,
|
Load the default tuning file into the camera, re-load with a broken tuning file,
|
||||||
check it errors. Finally check the default tuning file will load again afterwards.
|
check it errors. Finally check the default tuning file will load again afterwards.
|
||||||
|
|
@ -66,14 +66,14 @@ def _test_bad_tuning_after_good_tuning(configure):
|
||||||
bad_tuning = generate_bad_tuning()
|
bad_tuning = generate_bad_tuning()
|
||||||
default_tuning = load_default_tuning()
|
default_tuning = load_default_tuning()
|
||||||
print_tuning()
|
print_tuning()
|
||||||
print("opening camera with explicitly specified tuning")
|
print("opening camera with default tuning")
|
||||||
with Picamera2(tuning=default_tuning) as cam:
|
with Picamera2(tuning=default_tuning) as cam:
|
||||||
print_tuning()
|
print_tuning()
|
||||||
if configure:
|
if configure:
|
||||||
cam.configure(cam.create_preview_configuration())
|
cam.configure(cam.create_preview_configuration())
|
||||||
del cam
|
del cam
|
||||||
recalibrate_utils.recreate_camera_manager()
|
recalibrate_utils.recreate_camera_manager()
|
||||||
print(f"Opening camera with tuning['version'] = {bad_tuning['version']}")
|
print(f"Opening camera with bad tuning - ['version'] = {bad_tuning['version']}")
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
# The bad version should cause a problem
|
# The bad version should cause a problem
|
||||||
cam = Picamera2(tuning=bad_tuning)
|
cam = Picamera2(tuning=bad_tuning)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ dev = [
|
||||||
"matplotlib~=3.10"
|
"matplotlib~=3.10"
|
||||||
]
|
]
|
||||||
pi = [
|
pi = [
|
||||||
"picamera2~=0.3.12",
|
"picamera2~=0.3.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ class AutofocusThing(Thing):
|
||||||
|
|
||||||
stack_z_range = stack_dz * (images_to_capture - 1)
|
stack_z_range = stack_dz * (images_to_capture - 1)
|
||||||
if stack_z_range > 0:
|
if stack_z_range > 0:
|
||||||
|
# Perform backlash corrected move. See issue #420
|
||||||
stage.move_relative(z=-(STACK_OVERSHOOT + stack_z_range / 2))
|
stage.move_relative(z=-(STACK_OVERSHOOT + stack_z_range / 2))
|
||||||
stage.move_relative(z=STACK_OVERSHOOT)
|
stage.move_relative(z=STACK_OVERSHOOT)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
@ -348,7 +349,7 @@ class AutofocusThing(Thing):
|
||||||
images_dir: str,
|
images_dir: str,
|
||||||
stack_dir: str,
|
stack_dir: str,
|
||||||
logger: InvocationLogger,
|
logger: InvocationLogger,
|
||||||
):
|
) -> None:
|
||||||
"""Gets a list of images in a folder (stack_dir), sorts them by filesize, and copies the sharpest
|
"""Gets a list of images in a folder (stack_dir), sorts them by filesize, and copies the sharpest
|
||||||
image to images dir."""
|
image to images dir."""
|
||||||
image_list = glob.glob(os.path.join(stack_dir, "*"))
|
image_list = glob.glob(os.path.join(stack_dir, "*"))
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ class JPEGBlob(Blob):
|
||||||
|
|
||||||
|
|
||||||
class PNGBlob(Blob):
|
class PNGBlob(Blob):
|
||||||
|
"""A class representing a PNG image as a LabThings FastAPI Blob"""
|
||||||
|
|
||||||
media_type: str = "image/png"
|
media_type: str = "image/png"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -377,10 +377,13 @@ class SmartScanThing(Thing):
|
||||||
return (next_point[0], next_point[1], z_estimate)
|
return (next_point[0], next_point[1], z_estimate)
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _calc_displacement_from_test_image(self, overlap):
|
def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Take a test image and use camera stage mapping to calculate x and y displacement
|
Take a test image and use camera stage mapping to calculate x and y displacement
|
||||||
|
|
||||||
|
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
||||||
|
that each image should overlap its nearest neighbour by 50%.
|
||||||
|
|
||||||
Return (dx, dy) - the x and y displacments in steps
|
Return (dx, dy) - the x and y displacments in steps
|
||||||
"""
|
"""
|
||||||
test_jpg = self._cam.grab_jpeg()
|
test_jpg = self._cam.grab_jpeg()
|
||||||
|
|
@ -422,7 +425,9 @@ class SmartScanThing(Thing):
|
||||||
dx, dy = self._calc_displacement_from_test_image(overlap)
|
dx, dy = self._calc_displacement_from_test_image(overlap)
|
||||||
stitch_resize = STITCHING_RESOLUTION[0] / self.capture_resolution[0]
|
stitch_resize = STITCHING_RESOLUTION[0] / self.capture_resolution[0]
|
||||||
|
|
||||||
self._scan_logger.debug(f"Resizing images when stitching by {stitch_resize}")
|
self._scan_logger.debug(
|
||||||
|
f"Resizing images when stitching by a factor of {stitch_resize}"
|
||||||
|
)
|
||||||
|
|
||||||
self._scan_logger.info(
|
self._scan_logger.info(
|
||||||
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"
|
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue