diff --git a/README.md b/README.md index 576d384..2d54bba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PiCamera +# JamesCam Camera for Raspberry Pi ## How to run @@ -11,3 +11,31 @@ Or: ``` python3 src/main.py ``` + +## Configuration +Check `config.json` for an example configuration. + +### `camera_type` +Specifies the camera type (`picam` interface implementation) to use. +``` +1: legacy picamera implementation +``` + +### `capture_resolution` +Specifies the resolution of the photos to take + +### `preview_resolution` +Specifies the resolution of the viewfinder. I've set this to the resolution of the hyperpixel4 display + +### `output_format` +Specifies the output format of the image. Check supported formats using `python3 -m PIL` + +### `output_extension` +Specifies the file extension of the image. Should probably match the `output_format` option. + +### `output_directory` +Specifies the directory to put taken images in to. + +### `output_filename_format` +Specifies the format of the output filename. +> See [Python docs](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) for reference. diff --git a/config.json b/config.json new file mode 100644 index 0000000..66d2226 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "camera_type": 1, + "capture_resolution": [3464, 2309], + "preview_resolution": [800, 480], + "output_format": "JPEG", + "output_extension": ".jpg", + "output_directory": "./images", + "output_filename_format": "%d-%m-%Y %H-%M-%S" +} diff --git a/requirements.txt b/requirements.txt index 2ecdcb5..c5fe309 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ -pygame==1.9.6 -pygame-gui==0.5.7 \ No newline at end of file +pygame==2.1.2 +pygame-gui==0.6.4 +picamera==1.13 +pillow==9.2.0 diff --git a/src/main.py b/src/main.py index 78d5cc6..5d37198 100644 --- a/src/main.py +++ b/src/main.py @@ -1,36 +1,71 @@ -import picamui -import picam +from picam_manager.picam_manager import PiCamManager +from picamui.picamui import PiCamUi -def main(): - captureResolution = (1280, 1024) - captureDirectory = "./images" - captureExtension = "jpg" +import json +from PIL import Image +from datetime import datetime + +CONFIG_PATH = "./config.json" + +def load_config() -> dict: + """Load JSON config from disk + + :returns: Loaded JSON configuration as a dict + """ + with open(CONFIG_PATH, 'rt') as config_file: + return json.load(config_file) + +def main() -> None: + """Main function for JamesCam""" + config = load_config() # Setup UI - ui = picamui.PiCamUi() + ui = PiCamUi() ui.createUi() # Setup camera - cam = picam.PiCam() - cam.setPreviewResolution(ui.getScreenResolution()) - captureResolution = cam.getMaxResolution() + cam_manager = PiCamManager(config) + camera = cam_manager.camera loop = True while loop: - rgb = cam.getPreviewFrame() - ui.updatePreview(rgb, cam.getPreviewResolution()) + preview_bytes = camera.capture_preview() + + ui.updatePreview(preview_bytes, camera.get_preview_resolution()) ui.update() - uiEvents = ui.getEvents() - for event in uiEvents: - if event == "keyDownEscape" or event == "pygameQuit" or event == "btnExitPressed": + ui_events = ui.getEvents() + for event in ui_events: + if event == 'keyDownEscape' or event == 'pygameQuit' or event == 'btnExitPressed': loop = False - elif event == "btnTakePressed": - cam.capture(captureResolution, captureDirectory, captureExtension) + elif event == 'btnTakePressed': + now = datetime.now() + capture_bytes = camera.capture() + + pil_image = Image.frombuffer(mode='RGB', size=camera.get_capture_resolution(), data=capture_bytes) + + formatted_filename = now.strftime(config['output_filename_format']) + filename = f'{formatted_filename}{config["output_extension"]}' + + with open(f'{config["output_directory"]}/{filename}', "wb") as output_file: + pil_image.save(output_file, format=config["output_format"]) else: - print("Unknown event {}".format(event)) + print('Unknown event {}'.format(event)) + ui.cleanup() +def print_ascii_art() -> None: + """Extremely important function""" + print(""" + ██  █████  ███  ███ ███████ ███████  ██████  █████  ███  ███  + ██ ██   ██ ████  ████ ██      ██      ██      ██   ██ ████  ████  + ██ ███████ ██ ████ ██ █████  ███████ ██  ███████ ██ ████ ██  +██ ██ ██   ██ ██  ██  ██ ██          ██ ██  ██   ██ ██  ██  ██  + █████  ██  ██ ██      ██ ███████ ███████  ██████ ██  ██ ██      ██  +                                                             +""") + if __name__ == "__main__": + print_ascii_art() main() diff --git a/src/picam.py b/src/picam.py deleted file mode 100644 index 2581baf..0000000 --- a/src/picam.py +++ /dev/null @@ -1,44 +0,0 @@ -import picamera -import io - -from datetime import datetime - -class PiCam: - camera = None - resPreview = (640, 480) - - def __init__(self): - self.camera = picamera.PiCamera() - - def setCamResolution(self, res): - self.camera.resolution = res - - def setPreviewResolution(self, res): - self.resPreview = res - self.setCamResolution(self.resPreview) - - def getPreviewResolution(self): - return self.resPreview - - def getMaxResolution(self): - return (self.camera.MAX_RESOLUTION.width, self.camera.MAX_RESOLUTION.height) - - def capture(self, res, directory, extension): - now = datetime.now() - strNow = now.strftime("%d-%m-%Y %H-%M-%S") - self.setCamResolution(res) - self.camera.capture("./{0}/{1}.{2}".format(directory, strNow, extension)) - self.setCamResolution(self.resPreview) - - def getPreviewFrame(self): - rgb = bytearray(self.getPreviewResolution()[0] * self.getPreviewResolution()[1] * 3) - stream = io.BytesIO() - self.camera.capture(stream, use_video_port=True, format="rgb") - stream.seek(0) - stream.readinto(rgb) - stream.close() - - return rgb - - def cleanup(self): - self.camera.close() diff --git a/src/picam/picam.py b/src/picam/picam.py new file mode 100644 index 0000000..a904e9e --- /dev/null +++ b/src/picam/picam.py @@ -0,0 +1,46 @@ +class PiCam: + def __init__(self, config: dict): + self.capture_resolution = config["capture_resolution"] + self.preview_resolution = config["preview_resolution"] + + def set_capture_resolution(self, resolution: "tuple[int, int]") -> None: + """Set capture resolution for camera + + :param resolution: New resolution for camera captures + """ + self.capture_resolution = resolution + + def set_preview_resolution(self, resolution: "tuple[int, int]") -> None: + """Set resolution for camera preview (viewfinder) + + :param resolution: New resolution for camera preview + """ + self.preview_resolution = resolution + + def get_preview_resolution(self) -> "tuple[int, int]": + """Get the current preview resolution + + :returns: Tuple containing current preview resolution (x, y) + """ + raise NotImplementedError + + def get_capture_resolution(self) -> "tuple[int, int]": + """Get the current capture resolution + + :returns: Tuple containing current capture resolution (x, y) + """ + raise NotImplementedError + + def capture(self) -> bytes: + """Capture an image + + :returns: Image data as a byte buffer + """ + raise NotImplementedError + + def capture_preview(self) -> bytes: + """Capture a preview image for the viewfinder + + :returns: Image data as a byte buffer + """ + raise NotImplementedError diff --git a/src/picam/picam1.py b/src/picam/picam1.py new file mode 100644 index 0000000..263778f --- /dev/null +++ b/src/picam/picam1.py @@ -0,0 +1,53 @@ +from picam import PiCam + +from picamera import PiCamera + +class PiCam1(PiCam): + """Implementation of PiCam class using legacy picamera module""" + def __init__(self, config: dict): + super().__init__(config) + + self.init_camera() + + def __del__(self): + """Destructor - cleans up picamera""" + self.close_camera() + + def set_camera_resolution(self, resolution: "tuple[int, int]") -> None: + """Set resolution of active camera + + :param resolution: Resolution to set camera to + """ + self.camera.resolution = resolution + + def set_preview_resolution(self, resolution: "tuple[int, int]") -> None: + self.set_camera_resolution(resolution) + return super().set_preview_resolution(resolution) + + def init_camera(self) -> None: + """Initialize picamera camera object""" + self.camera = PiCamera(resolution=self.preview_resolution) + + def close_camera(self) -> None: + """Close picamera camera object""" + self.camera.close() + self.camera = None + + def capture(self, preview: bool = False) -> bytes: + # Bytes to hold output buffer + out = bytes() + + if not preview: + # If preview we'll already have the right resolution + self.set_camera_resolution(self.capture_resolution) + + self.camera.capture(out, format='rgb', use_video_port=preview) + + if not preview: + # If preview we'll already have the right resolution + self.set_camera_resolution(self.preview_resolution) + + return out + + def capture_preview(self) -> bytes: + return self.capture(preview=True) diff --git a/src/picam_manager/picam_manager.py b/src/picam_manager/picam_manager.py new file mode 100644 index 0000000..4575df8 --- /dev/null +++ b/src/picam_manager/picam_manager.py @@ -0,0 +1,10 @@ +from picam.picam1 import PiCam1 + +class PiCamManager: + def __init__(self, config: dict): + self.config = config + + if config['camera_type'] == 1: + self.camera = PiCam1(config) + else: + raise Exception(f'Unsupported camera type {config["camera_type"]}') diff --git a/src/picamui.py b/src/picamui/picamui.py similarity index 100% rename from src/picamui.py rename to src/picamui/picamui.py