From 62efbf8a10fcee3ccc31085a59d8e4a03776a694 Mon Sep 17 00:00:00 2001 From: Simon Reichel Date: Thu, 24 Aug 2023 11:46:46 +0100 Subject: [PATCH] Add user defined exif_data when capturing files When capturing to EXIF-capable files, the user can provide the dictionary `exif_data` which is merged with the auto-generated EXIF-data before writing into the image file. Changes affect the `capture_file_` method and all derivatives. Signed-off-by: Simon Lenz --- picamera2/picamera2.py | 43 +++++++++++++++++++++++++++++------------- picamera2/request.py | 26 +++++++++++++++++++------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/picamera2/picamera2.py b/picamera2/picamera2.py index 6fab895e..a0fb76a6 100644 --- a/picamera2/picamera2.py +++ b/picamera2/picamera2.py @@ -1202,14 +1202,14 @@ def dispatch_functions(self, functions, wait, signal_function=None, immediate=Fa self._run_process_requests() return job.get_result() if wait else job - def capture_file_(self, file_output, name: str, format=None) -> dict: + def capture_file_(self, file_output, name: str, format=None, exif_data=None) -> dict: if not self.completed_requests: return (False, None) request = self.completed_requests.pop(0) if name == "raw" and formats.is_raw(self.camera_config["raw"]["format"]): request.save_dng(file_output) else: - request.save(name, file_output, format=format) + request.save(name, file_output, format=format, exif_data=exif_data) result = request.get_metadata() request.release() @@ -1221,12 +1221,17 @@ def capture_file( name: str = "main", format=None, wait=None, - signal_function=None) -> dict: + signal_function=None, + exif_data=None) -> dict: """Capture an image to a file in the current camera mode. Return the metadata for the frame captured. + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. """ - functions = [partial(self.capture_file_, file_output, name, format=format)] + functions = [partial(self.capture_file_, file_output, name, format=format, + exif_data=exif_data)] return self.dispatch_functions(functions, wait, signal_function) def switch_mode_(self, camera_config): @@ -1241,22 +1246,26 @@ def switch_mode(self, camera_config, wait=None, signal_function=None): return self.dispatch_functions(functions, wait, signal_function, immediate=True) def switch_mode_and_capture_file(self, camera_config, file_output, name="main", format=None, - wait=None, signal_function=None): + wait=None, signal_function=None, exif_data=None): """Switch the camera into a new (capture) mode, capture an image to file. Then return back to the initial camera mode. + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. """ preview_config = self.camera_config - def capture_and_switch_back_(self, file_output, preview_config, format): - done, result = self.capture_file_(file_output, name, format=format) + def capture_and_switch_back_(self, file_output, preview_config, format, exif_data=exif_data): + done, result = self.capture_file_(file_output, name, format=format, exif_data=exif_data) if not done: return (False, None) self.switch_mode_(preview_config) return (True, result) functions = [partial(self.switch_mode_, camera_config), - partial(capture_and_switch_back_, self, file_output, preview_config, format)] + partial(capture_and_switch_back_, self, file_output, preview_config, format, + exif_data=exif_data)] return self.dispatch_functions(functions, wait, signal_function, immediate=True) def switch_mode_and_capture_request(self, camera_config, wait=None, signal_function=None): @@ -1620,7 +1629,7 @@ def set_overlay(self, overlay) -> None: def start_and_capture_files(self, name: str = "image{:03d}.jpg", initial_delay=1, preview_mode="preview", capture_mode="still", num_files=1, delay=1, - show_preview=True): + show_preview=True, exif_data=None): """This function makes capturing multiple images more convenient. Should only be used in command line line applications (not from a Qt application, for example). @@ -1648,6 +1657,9 @@ def start_and_capture_files(self, name: str = "image{:03d}.jpg", with delay zero, then there may be no images shown. This parameter only has any effect if a preview is not already running. If it is, it would have to be stopped first (with the stop_preview method). + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. """ if self.started: self.stop() @@ -1657,7 +1669,7 @@ def start_and_capture_files(self, name: str = "image{:03d}.jpg", self.start(show_preview=show_preview) for i in range(num_files): time.sleep(initial_delay if i == 0 else delay) - self.switch_mode_and_capture_file(capture_mode, name.format(i)) + self.switch_mode_and_capture_file(capture_mode, name.format(i), exif_data=exif_data) else: # No preview between captures, it's more efficient just to stay in capture mode. if initial_delay: @@ -1669,14 +1681,14 @@ def start_and_capture_files(self, name: str = "image{:03d}.jpg", self.configure(capture_mode) self.start(show_preview=show_preview) for i in range(num_files): - self.capture_file(name.format(i)) + self.capture_file(name.format(i), exif_data=exif_data) if i == num_files - 1: break time.sleep(delay) self.stop() def start_and_capture_file(self, name="image.jpg", delay=1, preview_mode="preview", - capture_mode="still", show_preview=True): + capture_mode="still", show_preview=True, exif_data=exif_data): """This function makes capturing a single image more convenient. Should only be used in command line line applications (not from a Qt application, for example). @@ -1698,9 +1710,14 @@ def start_and_capture_file(self, name="image.jpg", delay=1, preview_mode="previe displays an image by default during the preview phase. This parameter only has any effect if a preview is not already running. If it is, it would have to be stopped first (with the stop_preview method). + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. """ self.start_and_capture_files(name=name, initial_delay=delay, preview_mode=preview_mode, - capture_mode=capture_mode, num_files=1, show_preview=show_preview) + capture_mode=capture_mode, num_files=1, + show_preview=show_preview, + exif_data=exif_data) def start_and_record_video(self, output, encoder=None, config=None, quality=Quality.MEDIUM, show_preview=False, duration=0, audio=False): diff --git a/picamera2/request.py b/picamera2/request.py index 90f92f63..83a5be92 100644 --- a/picamera2/request.py +++ b/picamera2/request.py @@ -162,9 +162,14 @@ def make_image(self, name, width=None, height=None): """Make a PIL image from the named stream's buffer.""" return self.picam2.helpers.make_image(self.make_buffer(name), self.config[name], width, height) - def save(self, name, file_output, format=None): - """Save a JPEG or PNG image of the named stream's buffer.""" - return self.picam2.helpers.save(self.make_image(name), self.get_metadata(), file_output, format) + def save(self, name, file_output, format=None, exif_data=None): + """Save a JPEG or PNG image of the named stream's buffer. + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. + """ + return self.picam2.helpers.save(self.make_image(name), self.get_metadata(), file_output, + format, exif_data) def save_dng(self, filename, name="raw"): """Save a DNG RAW image of the raw stream's buffer.""" @@ -240,8 +245,14 @@ def make_image(self, buffer, config, width=None, height=None): pil_img = pil_img.resize((width, height)) return pil_img - def save(self, img, metadata, file_output, format=None): - """Save a JPEG or PNG image of the named stream's buffer.""" + def save(self, img, metadata, file_output, format=None, exif_data=None): + """Save a JPEG or PNG image of the named stream's buffer. + + exif_data - dictionary containing user defined exif data (based on `piexif`). This will + overwrite existing exif information generated by picamera2. + """ + if exif_data is None: + exif_data = {} # This is probably a hideously expensive way to do a capture. start_time = time.monotonic() exif = b'' @@ -269,7 +280,10 @@ def save(self, img, metadata, file_output, format=None): exif_ifd = {piexif.ExifIFD.DateTimeOriginal: datetime_now, piexif.ExifIFD.ExposureTime: (metadata["ExposureTime"], 1000000), piexif.ExifIFD.ISOSpeedRatings: int(total_gain * 100)} - exif = piexif.dump({"0th": zero_ifd, "Exif": exif_ifd}) + exif_dict = {"0th": zero_ifd, "Exif": exif_ifd} + # merge user provided exif data, overwriting the defaults + exif_dict = exif_dict | exif_data + exif = piexif.dump(exif_dict) # compress_level=1 saves pngs much faster, and still gets most of the compression. png_compress_level = self.picam2.options.get("compress_level", 1) jpeg_quality = self.picam2.options.get("quality", 90)