@@ -56,40 +56,11 @@ def __enter__(self) -> "MappedArray":
5656 if self .__reshape :
5757 if isinstance (self .__stream , str ):
5858 config = cast (Dict [str , Any ], self .__request .config )[self .__stream ]
59- fmt = config ["format" ]
60- w , h = config ["size" ]
61- stride = config ["stride" ]
6259 else :
6360 config = self .__stream .configuration
64- fmt = str (config .pixel_format )
65- w = config .size .width
66- h = config .size .height
67- stride = config .stride
68-
69- # Turning the 1d array into a 2d image-like array only works if the
70- # image stride (which is in bytes) is a whole number of pixels. Even
71- # then, if they don't match exactly you will get "padding" down the RHS.
72- # Working around this requires another expensive copy of all the data.
73- if fmt in ("BGR888" , "RGB888" ):
74- if stride != w * 3 :
75- array = array .reshape ((h , stride ))
76- array = array [:, :w * 3 ]
77- array = array .reshape ((h , w , 3 ))
78- elif fmt in ("XBGR8888" , "XRGB8888" ):
79- if stride != w * 4 :
80- array = array .reshape ((h , stride ))
81- array = array [:, :w * 4 ]
82- array = array .reshape ((h , w , 4 ))
83- elif fmt in ("YUV420" , "YVU420" ):
84- # Returning YUV420 as an image of 50% greater height (the extra bit continaing
85- # the U/V data) is useful because OpenCV can convert it to RGB for us quite
86- # efficiently. We leave any packing in there, however, as it would be easier
87- # to remove that after conversion to RGB (if that's what the caller does).
88- array = array .reshape ((h * 3 // 2 , stride ))
89- elif formats .is_raw (fmt ):
90- array = array .reshape ((h , stride ))
91- else :
92- raise RuntimeError ("Format " + fmt + " not supported" )
61+
62+ # helpers.make_array never makes a copy.
63+ array = self .__request .picam2 .helpers .make_array (array , config )
9364
9465 self .__array = array
9566 return self
@@ -167,12 +138,55 @@ def get_metadata(self) -> Dict[str, Any]:
167138 return metadata
168139
169140 def make_array (self , name : str ) -> np .ndarray :
170- """Make a 2D numpy array from the named stream's buffer."""
171- return self .picam2 .helpers .make_array (self .make_buffer (name ), cast (Dict [str , Any ], self .config )[name ])
141+ """Make a 2d numpy array from the named stream's buffer."""
142+ config = self .config .get (name , None )
143+ if config is None :
144+ raise RuntimeError (f'Stream { name !r} is not defined' )
145+ elif config ['format' ] == 'MJPEG' :
146+ return np .array (Image .open (io .BytesIO (self .make_buffer (name ))))
147+
148+ # We don't want to send out an exported handle to the camera buffer, so we're going to have
149+ # to do a copy. If the buffer is not contiguous, we can use the copy to make it so.
150+ with MappedArray (self , name ) as m :
151+ if m .array .data .c_contiguous :
152+ return np .copy (m .array )
153+ else :
154+ return np .ascontiguousarray (m .array )
172155
173156 def make_image (self , name : str , width : Optional [int ] = None , height : Optional [int ] = None ) -> Image .Image :
174157 """Make a PIL image from the named stream's buffer."""
175- return self .picam2 .helpers .make_image (self .make_buffer (name ), cast (Dict [str , Any ], self .config )[name ], width , height )
158+ config = self .config .get (name , None )
159+ if config is None :
160+ raise RuntimeError (f'Stream { name !r} is not defined' )
161+
162+ fmt = config ['format' ]
163+ if fmt == 'MJPEG' :
164+ return Image .open (io .BytesIO (self .make_buffer (name )))
165+ mode = self .picam2 .helpers ._get_pil_mode (fmt )
166+
167+ with MappedArray (self , name , write = False ) as m :
168+ shape = m .array .shape
169+ # At this point, array is the same memory as the camera buffer - no copy has happened.
170+ buffer = m .array
171+ stride = m .array .strides [0 ]
172+ if mode == "RGBX" :
173+ # FOr RGBX mode only, PIL shares the underlying buffer. So if we don't want to pass
174+ # out a handle to the camera buffer, then we must copy it.
175+ buffer = np .copy (m .array )
176+ stride = buffer .strides [0 ]
177+ elif not m .array .data .c_contiguous :
178+ # PIL will accept images with padding, but we must give it a contiguous buffer and
179+ # pass the stride as the penultimate "magic" parameter to frombuffer.
180+ buffer = m .array .base
181+
182+ pil_img = Image .frombuffer ("RGB" , (shape [1 ], shape [0 ]), buffer , "raw" , mode , stride , 1 )
183+
184+ width = width or shape [1 ]
185+ height = height or shape [0 ]
186+ if width != shape [1 ] or height != shape [0 ]:
187+ # This will be slow. Consider requesting camera images of this size in the first place!
188+ pil_img = pil_img .resize ((width , height ))
189+ return pil_img
176190
177191 def save (self , name : str , file_output : Any , format : Optional [str ] = None ,
178192 exif_data : Optional [Dict [str , Any ]] = None ) -> None :
@@ -187,8 +201,12 @@ def save(self, name: str, file_output: Any, format: Optional[str] = None,
187201
188202 def save_dng (self , file_output : Any , name : str = "raw" ) -> None :
189203 """Save a DNG RAW image of the raw stream's buffer."""
190- return self .picam2 .helpers .save_dng (self .make_buffer (name ), self .get_metadata (),
191- cast (Dict [str , Any ], self .config )[name ], file_output )
204+ # Don't use make_buffer(), this saves a copy.
205+ if self .stream_map .get (name , None ) is None :
206+ raise RuntimeError (f'Stream { name !r} is not defined' )
207+ with _MappedBuffer (self , name , write = False ) as b :
208+ buffer = np .array (b , copy = False , dtype = np .uint8 )
209+ return self .picam2 .helpers .save_dng (buffer , self .get_metadata (), self .config [name ], file_output )
192210
193211
194212class Helpers :
@@ -214,17 +232,17 @@ def make_array(self, buffer: np.ndarray, config: Dict[str, Any]) -> np.ndarray:
214232 if fmt in ("BGR888" , "RGB888" ):
215233 if stride != w * 3 :
216234 array = array .reshape ((h , stride ))
217- array = np . asarray ( array [:, :w * 3 ], order = 'C' )
235+ array = array [:, :w * 3 ]
218236 image = array .reshape ((h , w , 3 ))
219237 elif fmt in ("XBGR8888" , "XRGB8888" ):
220238 if stride != w * 4 :
221239 array = array .reshape ((h , stride ))
222- array = np . asarray ( array [:, :w * 4 ], order = 'C' )
240+ array = array [:, :w * 4 ]
223241 image = array .reshape ((h , w , 4 ))
224242 elif fmt in ("BGR161616" , "RGB161616" ):
225243 if stride != w * 6 :
226244 array = array .reshape ((h , stride ))
227- array = np . asarray ( array [:, :w * 6 ], order = 'C' )
245+ array = array [:, :w * 6 ]
228246 array = array .view (np .uint16 )
229247 image = array .reshape ((h , w , 3 ))
230248 elif fmt in ("YUV420" , "YVU420" ):
@@ -245,6 +263,13 @@ def make_array(self, buffer: np.ndarray, config: Dict[str, Any]) -> np.ndarray:
245263 raise RuntimeError ("Format " + fmt + " not supported" )
246264 return image
247265
266+ def _get_pil_mode (self , fmt ):
267+ mode_lookup = {"RGB888" : "BGR" , "BGR888" : "RGB" , "XBGR8888" : "RGBX" , "XRGB8888" : "BGRX" }
268+ mode = mode_lookup .get (fmt , None )
269+ if mode is None :
270+ raise RuntimeError (f"Stream format { fmt } not supported for PIL images" )
271+ return mode
272+
248273 def make_image (self , buffer : np .ndarray , config : Dict [str , Any ], width : Optional [int ] = None ,
249274 height : Optional [int ] = None ) -> Image .Image :
250275 """Make a PIL image from the named stream's buffer."""
@@ -253,15 +278,17 @@ def make_image(self, buffer: np.ndarray, config: Dict[str, Any], width: Optional
253278 return Image .open (io .BytesIO (buffer )) # type: ignore
254279 else :
255280 rgb = self .make_array (buffer , config )
256- mode_lookup = {"RGB888" : "BGR" , "BGR888" : "RGB" , "XBGR8888" : "RGBX" , "XRGB8888" : "BGRX" }
257- if fmt not in mode_lookup :
258- raise RuntimeError (f"Stream format { fmt } not supported for PIL images" )
259- mode = mode_lookup [fmt ]
260- pil_img = Image .frombuffer ("RGB" , (rgb .shape [1 ], rgb .shape [0 ]), rgb , "raw" , mode , 0 , 1 )
261- if width is None :
262- width = rgb .shape [1 ]
263- if height is None :
264- height = rgb .shape [0 ]
281+
282+ # buffer was already a copy, so don't need to worry about an extra copy for the "RGBX" mode.
283+ buf = rgb
284+ if not rgb .data .c_contiguous :
285+ buf = rgb .base
286+
287+ mode = self ._get_pil_mode (fmt )
288+ pil_img = Image .frombuffer ("RGB" , (rgb .shape [1 ], rgb .shape [0 ]), buf , "raw" , mode , rgb .strides [0 ], 1 )
289+
290+ width = width or rgb .shape [1 ]
291+ height = height or rgb .shape [0 ]
265292 if width != rgb .shape [1 ] or height != rgb .shape [0 ]:
266293 # This will be slow. Consider requesting camera images of this size in the first place!
267294 pil_img = pil_img .resize ((width , height )) # type: ignore
0 commit comments