11from __future__ import annotations
2+ from dataclasses import dataclass
23from datetime import datetime
34import io
45import 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
131141class 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" )
0 commit comments