Skip to content

Commit 272af96

Browse files
committed
Raw capture and deferred processing
Images can now be captured with a binary raw format, and processed later to PNG. TIFF export should be a simple addition. Image processing is done based on defined parameters, retrieved via a property. Expensive steps (e.g. interpolating the white reference image) may be cached.
1 parent b311683 commit 272af96

File tree

2 files changed

+106
-32
lines changed

2 files changed

+106
-32
lines changed

src/labthings_picamera2/thing.py

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from dataclasses import dataclass
23
from datetime import datetime
34
import io
45
import json
@@ -51,6 +52,7 @@ class RawImageModel(BaseModel):
5152
image_data: RawBlob
5253
thing_states: Optional[Mapping[str, Mapping]]
5354
metadata: Optional[Mapping[str, Mapping]]
55+
processing_inputs: Optional[ImageProcessingInputs] = None
5456
size: tuple[int, int]
5557
stride: int
5658
format: str
@@ -120,12 +122,20 @@ class LensShading(BaseModel):
120122
Cb: list[list[float]]
121123

122124

123-
class ImageProcessingParameters(BaseModel):
125+
class ImageProcessingInputs(BaseModel):
124126
lens_shading: LensShading
125127
colour_gains: tuple[float, float]
128+
white_norm_lores: NDArray
129+
raw_size: tuple[int, int]
126130
colour_correction_matrix: tuple[float, float, float, float, float, float, float, float, float]
127-
gamma: list[list[int]]
128-
white_norm: NDArray
131+
gamma: NDArray
132+
133+
134+
@dataclass
135+
class ImageProcessingCache:
136+
white_norm: np.ndarray
137+
gamma: interp1d
138+
ccm: np.ndarray
129139

130140

131141
class BlobNumpyDict(BlobBytes):
@@ -533,6 +543,7 @@ def capture_raw(
533543
self,
534544
states_getter: GetThingStates,
535545
get_states: bool=True,
546+
get_processing_inputs: bool=True,
536547
) -> RawImageModel:
537548
"""Capture a raw image
538549
@@ -551,14 +562,17 @@ def capture_raw(
551562
image_data = RawBlob.from_bytes(buffer.tobytes()),
552563
thing_states = states_getter() if get_states else None,
553564
metadata = { "parameters": parameters, "sensor": configuration["sensor"] },
565+
processing_inputs = (
566+
self.image_processing_inputs if get_processing_inputs else None
567+
),
554568
size = configuration["raw"]["size"],
555569
format = configuration["raw"]["format"],
556570
stride = configuration["raw"]["stride"],
557571
)
558572

559-
@thing_action
560-
def prepare_image_normalisation(self) -> ImageProcessingParameters:
561-
"""The parameters used to convert raw image data into processed images"""
573+
@thing_property
574+
def image_processing_inputs(self) -> ImageProcessingInputs:
575+
"""The information needed to turn raw images into processed ones"""
562576
lst = self.lens_shading_tables
563577
lum = np.array(lst.luminance)
564578
Cr = np.array(lst.Cr)
@@ -574,48 +588,91 @@ def prepare_image_normalisation(self) -> ImageProcessingParameters:
574588
with self.picamera() as cam:
575589
size: tuple[int, int] = cam.camera_configuration()["raw"]["size"]
576590

577-
zoom_factors = [
578-
i / 2 / n for i, n in zip(size[::-1], white_norm_lores.shape[:2])
579-
] + [1]
580-
white_norm = zoom(white_norm_lores, zoom_factors, order=1)[
581-
: (size[1]//2), : (size[0]//2), :
582-
] # Could use some work
583-
584591
contrast_algorithm = Picamera2.find_tuning_algo(self.tuning, "rpi.contrast")
585592
gamma = np.array(contrast_algorithm["gamma_curve"]).reshape((-1, 2))
586-
gamma_list: list[list[int]] = gamma.tolist()
587-
return ImageProcessingParameters(
588-
lens_shading = lst,
589-
colour_gains = self.colour_gains,
590-
colour_correction_matrix = self.colour_correction_matrix,
591-
gamma = gamma_list,
592-
white_norm = white_norm,
593+
594+
return ImageProcessingInputs(
595+
lens_shading=lst,
596+
colour_gains=(gr, gb),
597+
colour_correction_matrix=self.colour_correction_matrix,
598+
white_norm_lores=white_norm_lores,
599+
raw_size=size,
600+
gamma=gamma,
593601
)
594602

603+
@staticmethod
604+
def generate_image_processing_cache(
605+
p: ImageProcessingInputs,
606+
) -> ImageProcessingCache:
607+
"""Prepare to process raw images
608+
609+
This is a static method to ensure its outputs depend only on its
610+
inputs."""
611+
zoom_factors = [
612+
i / 2 / n for i, n in zip(p.raw_size[::-1], p.white_norm_lores.shape[:2])
613+
] + [1]
614+
white_norm = zoom(p.white_norm_lores, zoom_factors, order=1)[
615+
: (p.raw_size[1]//2), : (p.raw_size[0]//2), :
616+
]
617+
ccm = np.array(p.colour_correction_matrix).reshape((3,3))
618+
gamma = interp1d(p.gamma[:, 0] / 255, p.gamma[:, 1] / 255)
619+
return ImageProcessingCache(
620+
white_norm=white_norm,
621+
ccm = ccm,
622+
gamma = gamma,
623+
)
624+
625+
_image_processing_cache: ImageProcessingCache | None = None
626+
@thing_action
627+
def prepare_image_normalisation(
628+
self,
629+
inputs: ImageProcessingInputs | None = None
630+
) -> ImageProcessingInputs:
631+
"""The parameters used to convert raw image data into processed images
632+
633+
NB this method uses only information from `inputs` or
634+
`self.image_processing_inputs`, to ensure repeatability
635+
"""
636+
p = inputs or self.image_processing_inputs
637+
self._image_processing_cache = self.generate_image_processing_cache(p)
638+
return p
639+
595640
@thing_action
596-
def process_raw_array(self, raw: RawImageModel, parameters: Optional[ImageProcessingParameters]=None)->NDArray:
641+
def process_raw_array(
642+
self,
643+
raw: RawImageModel,
644+
use_cache: bool = False,
645+
)->NDArray:
597646
"""Convert a raw image to a processed array"""
598-
p = parameters or self.prepare_image_normalisation()
647+
if not use_cache:
648+
if raw.processing_inputs is None:
649+
raise ValueError(
650+
"The raw image does not contain processing inputs, "
651+
"and we are not using the cache. This may be solved by "
652+
"capturing with `get_processing_inputs=True`."
653+
)
654+
self.prepare_image_normalisation(
655+
raw.processing_inputs
656+
)
657+
p = self._image_processing_cache
658+
assert p is not None
599659
assert raw.format == "SBGGR10_CSI2P"
600-
ccm = np.array(p.colour_correction_matrix).reshape((3,3))
601-
gamma = np.array(p.gamma)
602-
gamma_8bit = interp1d(gamma[:, 0] / 255, gamma[:, 1] / 255)
603660
buffer = np.frombuffer(raw.image_data.content, dtype=np.uint8)
604661
packed = buffer.reshape((-1, raw.stride))
605662
rgb = rggb2rgb(raw2rggb(packed, raw.size))
606663
normed = rgb / p.white_norm
607664
corrected = np.dot(
608-
ccm, normed.reshape((-1, 3)).T
665+
p.ccm, normed.reshape((-1, 3)).T
609666
).T.reshape(normed.shape)
610667
corrected[corrected < 0] = 0
611668
corrected[corrected > 255] = 255
612-
processed_image = gamma_8bit(corrected)
613-
return processed_image
669+
processed_image = p.gamma(corrected)
670+
return processed_image.astype(np.uint8)
614671

615672
@thing_action
616-
def raw_to_png(self, raw: RawImageModel, parameters: Optional[ImageProcessingParameters]=None)->PNGBlob:
673+
def raw_to_png(self, raw: RawImageModel, use_cache: bool = False)->PNGBlob:
617674
"""Process a raw image to a PNG"""
618-
arr = self.process_raw_array(raw=raw, parameters=parameters)
675+
arr = self.process_raw_array(raw=raw, use_cache=use_cache)
619676
image = Image.fromarray(arr.astype(np.uint8), mode="RGB")
620677
out = io.BytesIO()
621678
image.save(out, format="png")

tests/test_acquisition.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,25 @@ def test_jpeg_and_array(client):
3131
assert array_main.shape[1::-1] == jpeg_capture.size
3232

3333
def test_raw_and_processed(client):
34-
#params = client.prepare_image_normalisation()
3534
raw = client.capture_raw()
36-
blob = client.raw_to_png(raw=raw) #, parameters=params)
35+
blob = client.raw_to_png(raw=raw)
3736
img = Image.open(blob.open())
3837
print(img.size)
38+
39+
def test_raw_with_cache(client):
40+
#params = client.prepare_image_normalisation()
41+
inputs = client.image_processing_inputs
42+
client.prepare_image_normalisation(inputs=inputs)
43+
raws = [
44+
client.capture_raw(get_processing_inputs=False, get_states=False)
45+
for _ in range(3)
46+
]
47+
blobs = [
48+
client.raw_to_png(raw=raw, use_cache=True)
49+
for raw in raws
50+
]
51+
for blob in blobs:
52+
img = Image.open(blob.open())
53+
expected_size = (
54+
d//2 for d in inputs["raw_size"]
55+
)

0 commit comments

Comments
 (0)