99
1010import numpy as np
1111import piexif
12+ import simplejpeg
1213from pidng .camdefs import Picamera2Camera
1314from pidng .core import PICAM2DNG
1415from PIL import Image
@@ -59,8 +60,8 @@ def __enter__(self) -> "MappedArray":
5960 else :
6061 config = self .__stream .configuration
6162
62- # helpers.make_array never makes a copy.
63- array = self .__request .picam2 .helpers .make_array (array , config )
63+ # helpers._make_array_shared never makes a copy.
64+ array = self .__request .picam2 .helpers ._make_array_shared (array , config )
6465
6566 self .__array = array
6667 return self
@@ -76,6 +77,8 @@ def array(self) -> Optional[np.ndarray]:
7677
7778
7879class CompletedRequest :
80+ FASTER_JPEG = True # set to False to use the older JPEG encode method
81+
7982 def __init__ (self , request : Any , picam2 : "Picamera2" ) -> None :
8083 self .request = request
8184 self .ref_count : int = 1
@@ -142,7 +145,7 @@ def make_array(self, name: str) -> np.ndarray:
142145 config = self .config .get (name , None )
143146 if config is None :
144147 raise RuntimeError (f'Stream { name !r} is not defined' )
145- elif config ['format' ] == 'MJPEG' :
148+ elif config ['format' ] == 'MJPEG' :
146149 return np .array (Image .open (io .BytesIO (self .make_buffer (name ))))
147150
148151 # We don't want to send out an exported handle to the camera buffer, so we're going to have
@@ -196,8 +199,47 @@ def save(self, name: str, file_output: Any, format: Optional[str] = None,
196199 exif_data - dictionary containing user defined exif data (based on `piexif`). This will
197200 overwrite existing exif information generated by picamera2.
198201 """
199- return self .picam2 .helpers .save (self .make_image (name ), self .get_metadata (), file_output ,
200- format , exif_data )
202+ # We have a more optimised path for writing JPEGs using simplejpeg.
203+ config = self .config .get (name , None )
204+ if config is None :
205+ raise RuntimeError (f'Stream { name !r} is not defined' )
206+ if self .FASTER_JPEG and config ['format' ] != "MJPEG" and \
207+ self .picam2 .helpers ._get_format_str (file_output , format ) in ('jpg' , 'jpeg' ):
208+ quality = self .picam2 .options .get ("quality" , 90 )
209+ with MappedArray (self , 'main' ) as m :
210+ format = self .config [name ]["format" ]
211+ if format == 'YUV420' :
212+ width , height = self .config [name ]['size' ]
213+ Y = m .array [:height , :width ]
214+ reshaped = m .array .reshape ((m .array .shape [0 ] * 2 , m .array .strides [0 ] // 2 ))
215+ U = reshaped [2 * height : 2 * height + height // 2 , :width // 2 ]
216+ V = reshaped [2 * height + height // 2 :, :width // 2 ]
217+ output_bytes = simplejpeg .encode_jpeg_yuv_planes (Y , U , V , quality )
218+ Y = reshaped = U = V = None
219+ else :
220+ FORMAT_TABLE = {"XBGR8888" : "RGBX" , "XRGB8888" : "BGRX" , "BGR888" : "RGB" , "RGB888" : "BGR" }
221+ output_bytes = simplejpeg .encode_jpeg (m .array , quality , FORMAT_TABLE [format ], '420' )
222+
223+ exif = self .picam2 .helpers ._prepare_exif (self .get_metadata (), exif_data )
224+
225+ if isinstance (file_output , io .BytesIO ):
226+ f = file_output
227+ else :
228+ f = open (file_output , 'wb' )
229+ try :
230+ if exif :
231+ # Splice in the exif data as we write it out.
232+ f .write (output_bytes [:2 ] + bytes .fromhex ('ffe1' ) + (len (exif ) + 2 ).to_bytes (2 , 'big' ))
233+ f .write (exif )
234+ f .write (output_bytes [2 :])
235+ else :
236+ f .write (output_bytes )
237+ except Exception :
238+ if f is not file_output :
239+ f .close ()
240+ else :
241+ return self .picam2 .helpers .save (self .make_image (name ), self .get_metadata (), file_output ,
242+ format , exif_data )
201243
202244 def save_dng (self , file_output : Any , name : str = "raw" ) -> None :
203245 """Save a DNG RAW image of the raw stream's buffer."""
@@ -218,17 +260,19 @@ class Helpers:
218260 def __init__ (self , picam2 : "Picamera2" ):
219261 self .picam2 = picam2
220262
221- def make_array (self , buffer : np .ndarray , config : Dict [str , Any ]) -> np .ndarray :
222- """Make a 2D numpy array from the named stream's buffer."""
263+ def _make_array_shared (self , buffer : np .ndarray , config : Dict [str , Any ]) -> np .ndarray :
264+ """Makes a 2d numpy array from the named stream's buffer without copying memory.
265+
266+ This method makes an array that is guaranteed to be shared with the underlying
267+ buffer, that is, no copy of the pixel data is made.
268+ """
223269 array = buffer
224270 fmt = config ["format" ]
225271 w , h = config ["size" ]
226272 stride = config ["stride" ]
227273
228- # Turning the 1d array into a 2d image-like array only works if the
229- # image stride (which is in bytes) is a whole number of pixels. Even
230- # then, if they don't match exactly you will get "padding" down the RHS.
231- # Working around this requires another expensive copy of all the data.
274+ # Reshape the 1d array into an image, and "slice" off any padding bytes on the
275+ # right hand edge (which doesn't copy the pixel data).
232276 if fmt in ("BGR888" , "RGB888" ):
233277 if stride != w * 3 :
234278 array = array .reshape ((h , stride ))
@@ -263,6 +307,18 @@ def make_array(self, buffer: np.ndarray, config: Dict[str, Any]) -> np.ndarray:
263307 raise RuntimeError ("Format " + fmt + " not supported" )
264308 return image
265309
310+ def make_array (self , buffer , config ):
311+ """Makes a 2d numpy array for the named stream's buffer.
312+
313+ This method makes a copy of the underlying camera buffer, so that it can be
314+ safely returned to the camera system.
315+ """
316+ array = self ._make_array_shared (buffer , config )
317+ if array .data .c_contiguous :
318+ return np .copy (array )
319+ else :
320+ return np .ascontiguousarray (array )
321+
266322 def _get_pil_mode (self , fmt ):
267323 mode_lookup = {"RGB888" : "BGR" , "BGR888" : "RGB" , "XBGR8888" : "RGBX" , "XRGB8888" : "BGRX" }
268324 mode = mode_lookup .get (fmt , None )
@@ -277,7 +333,7 @@ def make_image(self, buffer: np.ndarray, config: Dict[str, Any], width: Optional
277333 if fmt == "MJPEG" :
278334 return Image .open (io .BytesIO (buffer )) # type: ignore
279335 else :
280- rgb = self .make_array (buffer , config )
336+ rgb = self ._make_array_shared (buffer , config )
281337
282338 # buffer was already a copy, so don't need to worry about an extra copy for the "RGBX" mode.
283339 buf = rgb
@@ -294,6 +350,34 @@ def make_image(self, buffer: np.ndarray, config: Dict[str, Any], width: Optional
294350 pil_img = pil_img .resize ((width , height )) # type: ignore
295351 return pil_img
296352
353+ def _prepare_exif (self , metadata , exif_data ):
354+ exif = b''
355+ if "AnalogueGain" in metadata and "DigitalGain" in metadata :
356+ datetime_now = datetime .now ().strftime ("%Y:%m:%d %H:%M:%S" )
357+ zero_ifd = {piexif .ImageIFD .Make : "Raspberry Pi" ,
358+ piexif .ImageIFD .Model : self .picam2 .camera .id ,
359+ piexif .ImageIFD .Software : "Picamera2" ,
360+ piexif .ImageIFD .DateTime : datetime_now }
361+ total_gain = metadata ["AnalogueGain" ] * metadata ["DigitalGain" ]
362+ exif_ifd = {piexif .ExifIFD .DateTimeOriginal : datetime_now ,
363+ piexif .ExifIFD .ExposureTime : (metadata ["ExposureTime" ], 1000000 ),
364+ piexif .ExifIFD .ISOSpeedRatings : int (total_gain * 100 )}
365+ exif_dict = {"0th" : zero_ifd , "Exif" : exif_ifd }
366+ # merge user provided exif data, overwriting the defaults
367+ exif_dict = exif_dict | (exif_data or {})
368+ exif = piexif .dump (exif_dict )
369+ return exif
370+
371+ def _get_format_str (self , file_output , format ):
372+ if isinstance (format , str ):
373+ return format .lower ()
374+ elif isinstance (file_output , str ):
375+ return file_output .split ('.' )[- 1 ].lower ()
376+ elif isinstance (file_output , Path ):
377+ return file_output .suffix .lower ()
378+ else :
379+ raise RuntimeError ("Cannot determine format to save" )
380+
297381 def save (self , img : Image .Image , metadata : Dict [str , Any ], file_output : Union [str , Path ], format : Optional [str ] = None ,
298382 exif_data : Optional [Dict ] = None ) -> None :
299383 """Save a JPEG or PNG image of the named stream's buffer.
@@ -305,36 +389,15 @@ def save(self, img: Image.Image, metadata: Dict[str, Any], file_output: Union[st
305389 exif_data = {}
306390 # This is probably a hideously expensive way to do a capture.
307391 start_time = time .monotonic ()
308- exif = b''
309- if isinstance (format , str ):
310- format_str = format .lower ()
311- elif isinstance (file_output , str ):
312- format_str = file_output .split ('.' )[- 1 ].lower ()
313- elif isinstance (file_output , Path ):
314- format_str = file_output .suffix .lower ()
315- else :
316- raise RuntimeError ("Cannot determine format to save" )
392+ format_str = self ._get_format_str (file_output , format )
317393 if format_str in ('png' ) and img .mode == 'RGBX' :
318394 # It seems we can't save an RGBX png file, so make it RGBA instead. We can't use RGBA
319395 # everywhere, because we can only save an RGBX jpeg, not an RGBA one.
320396 img = img .convert (mode = 'RGBA' )
397+ exif = b''
321398 if format_str in ('jpg' , 'jpeg' ):
322399 # Make up some extra EXIF data.
323- if "AnalogueGain" in metadata and "DigitalGain" in metadata :
324- datetime_now = datetime .now ().strftime ("%Y:%m:%d %H:%M:%S" )
325- assert self .picam2 .camera is not None
326- zero_ifd = {piexif .ImageIFD .Make : "Raspberry Pi" ,
327- piexif .ImageIFD .Model : self .picam2 .camera .id ,
328- piexif .ImageIFD .Software : "Picamera2" ,
329- piexif .ImageIFD .DateTime : datetime_now }
330- total_gain = metadata ["AnalogueGain" ] * metadata ["DigitalGain" ]
331- exif_ifd = {piexif .ExifIFD .DateTimeOriginal : datetime_now ,
332- piexif .ExifIFD .ExposureTime : (metadata ["ExposureTime" ], 1000000 ),
333- piexif .ExifIFD .ISOSpeedRatings : int (total_gain * 100 )}
334- exif_dict = {"0th" : zero_ifd , "Exif" : exif_ifd }
335- # merge user provided exif data, overwriting the defaults
336- exif_dict = exif_dict | exif_data
337- exif = piexif .dump (exif_dict )
400+ exif = self ._prepare_exif (metadata , exif_data )
338401 # compress_level=1 saves pngs much faster, and still gets most of the compression.
339402 png_compress_level = self .picam2 .options .get ("compress_level" , 1 )
340403 jpeg_quality = self .picam2 .options .get ("quality" , 90 )
0 commit comments