Skip to content

Commit 16d52ae

Browse files
committed
Optimise some of the request make_buffer/array/image functions
There are some code paths where we can do more to avoid copying the entire image buffer. The request make_buffer, make_array and make_image functions are now guaranteed always to do just one copy (which is required if you want to avoid open handles to the dmabufs). Signed-off-by: David Plowman <[email protected]>
1 parent 209bf9a commit 16d52ae

File tree

1 file changed

+76
-49
lines changed

1 file changed

+76
-49
lines changed

picamera2/request.py

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

194212
class 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

Comments
 (0)