diff --git a/README.md b/README.md index c8245178..566d8302 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ -# picam -RaspberryPi Camera software to be used with a screen module. +# PiCam +RaspberryPi Camera project to create a software and hardware stack to build your own professional style camera. +My test model of the platform is called JankyCam. + +I hope that this project will inspire others to help contribute and eventually make something everyone can use to gain access to the world of photography and lenses beyond just mobile phones. Also for this to be used as a platform for computational photography and experimentation/learning. + +This is still early days and requires a lot of work before its easy to use. # Requirements - opencv-python @@ -33,11 +38,12 @@ pip3 install src/. git clone https://github.com/trex22/Colour_Profiles.git ``` -# dcamprof +# dcamprof dcp to json conversion https://torger.se/anders/dcamprof.html#download ``` ./dcamprof.exe dcp2json "C:/development/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.dcp" "C:/development/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.json" + ./dcamprof.exe dcp2json "C:/development/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.dcp" "C:/development/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json" ``` @@ -90,3 +96,27 @@ https://torger.se/anders/dcamprof.html#download - continuous shot - stop motion mode - astro-photography mode + - Lens exif data tool + - Improve tools to be cli + - python module + - pyton requirements.txt + - EXIF copyright info + - Image watermark tool + - Photo Viewer + - 60fps bpp 10 Full + - DOL-HDR + - FOM: https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=273804 + - long exposures: https://www.raspberrypi.org/forums/viewtopic.php?t=277689 + - https://www.raspberrypi.org/forums/viewtopic.php?t=275962 + - https://www.raspberrypi.org/forums/viewtopic.php?t=275962 + - https://www.raspberrypi.org/forums/viewtopic.php?t=279752 + - Star Eater: https://photo.stackexchange.com/questions/116765/new-pi-camera-any-good-for-astrophotography + - https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=277768 + - https://github.com/raspberrypi/userland/blob/3fd8527eefd8790b4e8393458efc5f94eb21a615/interface/mmal/mmal_parameters_camera.h + - https://picamera.readthedocs.io/en/release-1.13/api_mmalobj.html + - http://www.jvcref.com/files/PI/documentation/html/struct_m_m_a_l___p_a_r_a_m_e_t_e_r___h_e_a_d_e_r___t.html + - https://picamera.readthedocs.io/en/release-1.10/api_camera.html#picamera.camera.PiCamera.annotate_text_size + - https://stamm-wilbrandt.de/en/Raspberry_camera.html + - https://github.com/rellimmot/Sony-IMX219-Raspberry-Pi-V2-CMOS/blob/master/RASPBERRY%20PI%20CAMERA%20V2%20DATASHEET%20IMX219PQH5_7.0.0_Datasheet_XXX.PDF + - Global external shutter: https://github.com/Hermann-SW/Raspberry_v1_camera_global_external_shutter + - Manual MMAL: https://gist.github.com/rwb27/a23808e9f4008b48de95692a38ddaa08 diff --git a/test/overlay_test.py b/experiments/overlay_test.py similarity index 100% rename from test/overlay_test.py rename to experiments/overlay_test.py diff --git a/test/photogrammetry_single_light.py b/experiments/photogrammetry_single_light.py similarity index 100% rename from test/photogrammetry_single_light.py rename to experiments/photogrammetry_single_light.py diff --git a/test/take_hdr_image.py b/experiments/take_hdr_image.py similarity index 100% rename from test/take_hdr_image.py rename to experiments/take_hdr_image.py diff --git a/test/take_image.py b/experiments/take_image.py similarity index 100% rename from test/take_image.py rename to experiments/take_image.py diff --git a/test/take_image_experimental.py b/experiments/take_image_experimental.py similarity index 100% rename from test/take_image_experimental.py rename to experiments/take_image_experimental.py diff --git a/test/take_manual_image.py b/experiments/take_manual_image.py similarity index 100% rename from test/take_manual_image.py rename to experiments/take_manual_image.py diff --git a/test/take_raw_image.py b/experiments/take_raw_image.py similarity index 100% rename from test/take_raw_image.py rename to experiments/take_raw_image.py diff --git a/test/take_video.py b/experiments/take_video.py similarity index 100% rename from test/take_video.py rename to experiments/take_video.py diff --git a/install.sh b/install.sh index c6a7e818..905b7467 100644 --- a/install.sh +++ b/install.sh @@ -20,10 +20,11 @@ sudo raspi-config mkdir -p ~/DCIM sudo apt update -sudo apt install -y samba samba-common git build-essential cmake python3 python3-pip python-dev python-rpi.gpio python3-dev python3-rpi.gpio libopencv-dev python-opencv python-picamera python3-picamera libatlas-base-dev libhdf5-dev libhdf5-serial-dev libatlas-base-dev libjasper-dev libqtgui4 libqt4-test libatlas-base-dev libxml2-dev libxslt-dev +sudo apt install -y samba samba-common git build-essential cmake python3 python3-pip python-dev python-rpi.gpio python3-dev python3-rpi.gpio libopencv-dev python-opencv python-picamera python3-picamera libatlas-base-dev libhdf5-dev libhdf5-serial-dev libatlas-base-dev libjasper-dev libqtgui4 libqt4-test libatlas-base-dev libxml2-dev libxslt-dev libgstreamer1.0-0 pip3 install opencv-contrib-python numpy ExifRead pip3 install Pillow picamerax +pip3 install pi-ina219 sudo pip3 install Click==7.0 sudo pip3 install adafruit-python-shell diff --git a/src/camera_handler.py b/src/camera_handler.py index a45af9af..0dd3eb5d 100644 --- a/src/camera_handler.py +++ b/src/camera_handler.py @@ -1,20 +1,186 @@ +import os import time import glob from io import BytesIO -from pydng.core import RPICAM2DNG +from picamerax import PiCamera +from picamerax import mmal, mmalobj, exc +from picamerax.mmalobj import to_rational, to_fraction, to_resolution + +import RPi.GPIO as GPIO # Modules -import document_handler import overlay_handler +import menu_handler +from thread_writer import ThreadWriter +from thread_raw_converter import ThreadRawConverter + +################################################################################ +## Camera Instance ## +################################################################################ +def start_preview(camera, config): + config["preview"] = True + + format = config["format"] + bayer = config["bayer"] + + # options are: "built-in" "continuous_shot" + if config["preview_mode"] == "continuous_shot": + # for frame in camera.capture_continuous(rawCapture, format=format, bayer=bayer, use_video_port=True): + # TODO: + time.sleep(0.1) + else: # default + camera.start_preview() + message = input("Press enter to quit\n\n") # Run until someone presses enter + +def stop_preview(camera, config): + # Just set the variable. The loop in the other thread will halt on next iteration + config["preview"] = False + + if config["preview_mode"] == config["default_preview_mode"]: + camera.stop_preview() + +def start_camera(original_config, skip_auto=False): + global camera + global overlay + global config + + # Force variables to be blanked + camera = None + overlay = None + config = original_config + + # Config Variables + fps = config["fps"] + screen_fps = config["screen_fps"] + + screen_w = config["screen_w"] + screen_h = config["screen_h"] + width = config["width"] + height = config["height"] + + # Init + camera = PiCamera(framerate=config["fps"]) + + if skip_auto == False: + auto_mode(camera, overlay, config) + + overlay = None + + camera.resolution = (screen_w, screen_h) + camera.framerate = screen_fps # fps + + overlay = overlay_handler.add_overlay(camera, overlay, config) + overlay_handler.display_text(camera, '', config) + print(f'screen: ({screen_w}, {screen_h}), res: ({width}, {height})') + + start_button_listen(config) + + return [camera, overlay] + +def stop_camera(camera, overlay, config): + stop_preview(camera, config) + + if overlay != None: + overlay = overlay_handler.remove_overlay(camera, overlay, config) + + if camera != None: + camera.close() + + camera = None + overlay = None + + stop_button_listen() + +################################################################################ +## GPIO Stuff ## +################################################################################ +def button_callback_1(): + global camera + global overlay + global config + + print("Button 1: Menu") + menu_handler.select_menu_item(camera, config) + +def button_callback_2(): + global camera + global overlay + global config + + print("Button 2: Option") + menu_handler.select_option(camera, overlay, config) + +def button_callback_3(): + global camera + global overlay + global config + + print("Button 3: Zoom") + zoom(camera, config) + overlay = overlay_handler.add_overlay(camera, overlay, config) + +def button_callback_4(): + global camera + global overlay + global config -def auto_mode(camera, config): + print("Button 4: Take shot") + + overlay = overlay_handler.remove_overlay(camera, overlay, config) + + if config["video"]: + trigger_video(camera, overlay, config) + else: + if config["hdr"]: + take_hdr_shot(camera, overlay, config) + else: + take_single_shot(camera, overlay, config) + + overlay = overlay_handler.add_overlay(camera, overlay, config) + +def start_button_listen(config): + # GPIO Config + button_1 = config["gpio"]["button_1"] + button_2 = config["gpio"]["button_2"] + button_3 = config["gpio"]["button_3"] + button_4 = config["gpio"]["button_4"] + bouncetime = config["gpio"]["bouncetime"] + + # Set button callbacks + # GPIO.setwarnings(False) # Ignore warning for now + GPIO.setwarnings(True) + # GPIO.setmode(GPIO.BOARD) # Use physical pin numbering + GPIO.setmode(GPIO.BCM) + + GPIO.setup(button_1, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(button_2, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(button_3, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(button_4, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + GPIO.add_event_detect(button_1, GPIO.RISING, callback=lambda x: button_callback_1(), bouncetime=bouncetime) + GPIO.add_event_detect(button_2, GPIO.RISING, callback=lambda x: button_callback_2(), bouncetime=bouncetime) + GPIO.add_event_detect(button_3, GPIO.RISING, callback=lambda x: button_callback_3(), bouncetime=bouncetime) + GPIO.add_event_detect(button_4, GPIO.RISING, callback=lambda x: button_callback_4(), bouncetime=bouncetime) + +def stop_button_listen(): + GPIO.cleanup() # Clean up + +################################################################################ +## Camera Actions ## +################################################################################ +def auto_mode(camera, overlay, config, skip_dpc=False): + config['dpc'] = config['default_dpc'] camera.iso = config["default_iso"] camera.exposure_mode = config["default_exposure_mode"] camera.shutter_speed = config["default_shutter_speed"] camera.awb_mode = config["default_awb_mode"] + if skip_dpc == False: + set_dpc(camera, overlay, config) + + set_fom(camera, config) overlay_handler.display_text(camera, '', config) print(f'auto mode!') @@ -56,6 +222,18 @@ def adjust_shutter_speed(camera, config): overlay_handler.display_text(camera, '', config) print(f'shutter_speed: {config["shutter_speed"]}') +def long_shutter_speed(camera, config): + idex = config["available_long_shutter_speeds"].index(config["shutter_speed"]) + 1 + + if idex < len(config["available_long_shutter_speeds"]): + config["shutter_speed"] = config["available_long_shutter_speeds"][idex] + else: + config["shutter_speed"] = config["default_shutter_speed"] + + camera.shutter_speed = config["shutter_speed"] + overlay_handler.display_text(camera, '', config) + print(f'shutter_speed: {config["shutter_speed"]}') + def adjust_awb_mode(camera, config): idex = config["available_awb_mode"].index(config["awb_mode"]) + 1 @@ -83,36 +261,102 @@ def adjust_encoding(camera, config): overlay_handler.display_text(camera, '', config) print(f'encoding: {config["encoding"]}') +def adjust_dpc(config): + current_dpc = config["dpc"] + + if (current_dpc == config["default_dpc"]): + print("Set DPC to disabled") + config["dpc"] = 0 + elif (current_dpc == 0): + print("Set DPC to 1") + config["dpc"] = 1 + elif (current_dpc == 1): + print("Set DPC to 2") + config["dpc"] = 2 + elif (current_dpc == 2): + print("Set DPC to 3") + config["dpc"] = 3 + else: + print("Reset DPC") + config["dpc"] = config["default_dpc"] + +def set_dpc(camera, overlay, config): + current_dpc = config["dpc"] + print(f'current_dpc: {current_dpc}') + + # Turn off Camera + stop_camera(camera, overlay, config) + + # Set DPC Mode + # TODO: Add to MMAL interface here: https://github.com/labthings/picamerax/blob/master/picamerax/mmal.py + os.system(f'sudo vcdbg set imx477_dpc {current_dpc}') # TODO: Security risk here! + + # Start Camera + camera, overlay = start_camera(config, skip_auto=True) + start_preview(camera, config) # Runs main camera loop + +def set_raw_convert(camera, config): + config["raw_convert"] = not config["raw_convert"] + overlay_handler.display_text(camera, '', config) + print(f'raw_convert: {config["raw_convert"]}') + +def adjust_fom(camera, config): + config["fom"] = not config["fom"] + overlay_handler.display_text(camera, '', config) + +def set_fom(camera, config): + value = config["fom"] + parameter = mmal.MMAL_PARAMETER_DRAW_BOX_FACES_AND_FOCUS + set_mmal_parameter(camera, parameter, value) + print(f'fom: {config["fom"]}') + +def adjust_hdr2(camera, config): + config["hdr2"] = not config["hdr2"] + overlay_handler.display_text(camera, '', config) + +def set_hdr2(camera, config): + value = config["hdr2"] + parameter = mmal.MMAL_PARAMETER_HIGH_DYNAMIC_RANGE + set_mmal_parameter(camera, parameter, value) + print(f'hdr2: {config["hdr2"]}') + def zoom(camera, config): current_zoom = camera.zoom + print(f'current_zoom: {current_zoom}') - if (current_zoom == config["max_zoom"]): - camera.zoom = config["default_zoom"] - else: + if (current_zoom == config["default_zoom"]): + print("Set Zoom to max_zoom") camera.zoom = config["max_zoom"] + elif (current_zoom == config["max_zoom"]): + print("Set Zoom to max_zoom_2") + camera.zoom = config["max_zoom_2"] + elif (current_zoom == config["max_zoom_2"]): + print("Set Zoom to max_zoom_3") + camera.zoom = config["max_zoom_3"] + else: + print("Reset Zoom") + camera.zoom = config["default_zoom"] -def take_hdr_shot(camera, config): +def take_hdr_shot(camera, overlay, config): screen_w = config["screen_w"] screen_h = config["screen_h"] width = config["width"] height = config["height"] - # dcim_path = config["dcim_path"] - # dcim_images_path_raw = config["dcim_images_path_raw"] - # dcim_original_images_path = config["dcim_original_images_path"] + format = config["format"] + bayer = config["bayer"] + dcim_hdr_images_path = config["dcim_hdr_images_path"] - # dcim_videos_path = config["dcim_videos_path"] - # dcim_tmp_path = config["dcim_tmp_path"] camera.resolution = (width, height) start_time = time.time() # SEE: https://github.com/KEClaytor/pi-hdr-timelapse - nimages = 5 #10 #2160 - exposure_min = 10 - exposure_max = 80 #90 + nimages = 5 + exposure_min = 25 + exposure_max = 75 exp_step = 5 exp_step = (exposure_max - exposure_min) / (nimages - 1.0) @@ -132,7 +376,9 @@ def take_hdr_shot(camera, config): camera.brightness = step # camera.exposure_compensation = step - camera.capture(filename, format, bayer=True) + stream = BytesIO() + camera.capture(stream, format, bayer=bayer) + write_via_thread(filename, 'wb', stream.getbuffer()) camera.brightness = original_brightness # camera.exposure_compensation = original_exposure_compensation @@ -144,19 +390,15 @@ def take_hdr_shot(camera, config): print("--- %s seconds ---" % (time.time() - start_time)) -def take_single_shot(camera, config): +def take_single_shot(camera, overlay, config): screen_w = config["screen_w"] screen_h = config["screen_h"] width = config["width"] height = config["height"] - # dcim_path = config["dcim_path"] dcim_images_path_raw = config["dcim_images_path_raw"] dcim_original_images_path = config["dcim_original_images_path"] - # dcim_hdr_images_path = config["dcim_hdr_images_path"] - # dcim_videos_path = config["dcim_videos_path"] - # dcim_tmp_path = config["dcim_tmp_path"] format = config["format"] bayer = config["bayer"] @@ -165,7 +407,7 @@ def take_single_shot(camera, config): filecount = len(existing_files) frame_count = filecount - filename = f'{dcim_images_path_raw}/{frame_count}.{format}' + raw_filename = f'{dcim_images_path_raw}/{frame_count}.dng' original_filename = f'{dcim_original_images_path}/{frame_count}.{format}' print(original_filename) @@ -177,17 +419,12 @@ def take_single_shot(camera, config): print(f'screen: ({screen_w}, {screen_h}), res: ({width}, {height}), shutter_speed: {camera.shutter_speed}') camera.capture(stream, format, bayer=bayer) + write_via_thread(original_filename, 'wb', stream.getbuffer()) - with open(original_filename, 'wb') as f: - f.write(stream.getbuffer()) - - if (config["convert_raw"] == True): + if (config["raw_convert"] == True): print("Begin conversion and save DNG raw ...") - json_colour_profile = document_handler.load_colour_profile(config) - output = RPICAM2DNG().convert(stream, json_camera_profile=json_colour_profile) + ThreadRawConverter(config, stream, raw_filename) - with open(filename, 'wb') as f: - f.write(output) else: print("--- skip raw conversion ---") @@ -195,7 +432,7 @@ def take_single_shot(camera, config): camera.resolution = (screen_w, screen_h) -def trigger_video(camera, config): +def trigger_video(camera, overlay, config): if config["recording"]: camera.stop_recording() config["recording"] = False @@ -206,12 +443,7 @@ def trigger_video(camera, config): width = config["width"] height = config["height"] - # dcim_path = config["dcim_path"] - # dcim_images_path_raw = config["dcim_images_path_raw"] - # dcim_original_images_path = config["dcim_original_images_path"] - # dcim_hdr_images_path = config["dcim_hdr_images_path"] dcim_videos_path = config["dcim_videos_path"] - # dcim_tmp_path = config["dcim_tmp_path"] format = config["video_format"] @@ -221,10 +453,48 @@ def trigger_video(camera, config): original_filename = f'{dcim_videos_path}/{filecount}.{format}' print(original_filename) - # start_time = time.time() camera.resolution = (width, height) - print(f'screen: ({screen_w}, {screen_h}), res: ({width}, {height}), shutter_speed: {camera.shutter_speed}') config["recording"] = True camera.start_recording(original_filename, format) + +def write_via_thread(original_filename, write_type, stream): + w = ThreadWriter(original_filename, write_type) + w.write(stream) + w.close() + +# Available conversions +# to_resolution +# to_fraction +# to_rational +# https://gist.github.com/rwb27/a23808e9f4008b48de95692a38ddaa08 +def set_mmal_parameter(camera, parameter, value): + if isinstance(value, bool): + ret = mmal.mmal_port_parameter_set_boolean(camera._camera.control._port, parameter, value) + print(f'MMAL Response: {ret}') + return ret + else: + converted_value = to_rational(value) + ret = mmal.mmal_port_parameter_set_rational(camera._camera.control._port, parameter, converted_value) + print(f'MMAL Response: {ret}') + return ret + +# TODO: +# https://github.com/labthings/picamerax/blob/master/picamerax/mmal.py +# MMAL_PARAMETER_HIGH_DYNAMIC_RANGE, +# MMAL_PARAMETER_DYNAMIC_RANGE_COMPRESSION, +# MMAL_PARAMETER_ALGORITHM_CONTROL, +# MMAL_PARAMETER_SHARPNESS, +# MMAL_PARAMETER_ANTISHAKE, +# MMAL_PARAMETER_CAMERA_BURST_CAPTURE, +# MMAL_PARAMETER_DPC # https://github.com/raspberrypi/userland/blob/3fd8527eefd8790b4e8393458efc5f94eb21a615/interface/mmal/mmal_parameters_camera.h +# MMAL_PARAMETER_SHUTTER_SPEED +# MMAL_PARAMETER_BLACK_LEVEL +# MMAL_PARAMETER_ANALOG_GAIN +# MMAL_PARAMETER_DIGITAL_GAIN +# MMAL_PARAMETER_STILLS_DENOISE +# MMAL_PARAMETER_ZERO_SHUTTER_LAG +# MMAL_PARAMETER_FIELD_OF_VIEW +# MMAL_PARAMETER_EXPOSURE_COMP +# MMAL_PARAMETER_FLICKER_AVOID diff --git a/src/main.py b/src/main.py index 77293258..3c62e6be 100644 --- a/src/main.py +++ b/src/main.py @@ -15,6 +15,17 @@ # available_exposure_compensations = [-25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25] +# HQ Camera on-sensor defective pixel correction (DPC) +# https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=277768 +# 0 - All DPC disabled. +# 1 - Enable mapped on-sensor DPC. +# 2 - Enable dynamic on-sensor DPC. +# 3 - Enable mapped and dynamic on-sensor DPC. + +# The default is (3). It would be useful to get feedback from users who do astrophotography if disabling DPC actually makes a difference or not. + +# Note that this does not disable the ISP defective pixel correction that will still be active, so you will likely only see changes in the RAW image. + # 8MP pi camera v2.1 # width = 3280 # height = 2464 @@ -23,30 +34,19 @@ # width = 4056 # height = 3040 -VERSION = "0.0.13" - -import time -import glob - -from picamerax import PiCamera - -import RPi.GPIO as GPIO +VERSION = "0.0.24" # Modules import document_handler -import overlay_handler import camera_handler -import menu_handler ################################################################################ ## Config ## ################################################################################ -global config config = { "colour_profile_path": "/home/pi/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json", - "convert_raw": False, "dcim_path": 'home/pi/DCIM', - "dcim_images_path_raw": '/home/pi/DCIM/images', + "dcim_images_path_raw": '/home/pi/DCIM/images/raw', "dcim_original_images_path": '/home/pi/DCIM/images/original', "dcim_hdr_images_path": '/home/pi/DCIM/images/hdr', "dcim_videos_path": '/home/pi/DCIM/videos', @@ -58,18 +58,21 @@ "bayer": True, "fps": 40, # 60 # 10 fps max at full resolution "screen_fps": 40, # 120 fps at 1012x760 - "screen_w": 1024, # 1012 # 320 screen res # Needs to be 4:3 - "screen_h": 768, #760 # 240 screen res # Needs to be 4:3 + "screen_w": 1280, # 1024 # 1012 # 320 screen res # Needs to be 4:3 + "screen_h": 960, # 768 #760 # 240 screen res # Needs to be 4:3 "overlay_w": 320, "overlay_h": 240, "width": 4056, # Image width "height": 3040, # Image height "video_width": 4056, "video_height": 3040, + "annotate_text_size": 48, # 6 to 160, inclusive. The default is 32 "exposure_mode": 'auto', "default_exposure_mode": 'auto', "default_zoom": (0.0, 0.0, 1.0, 1.0), "max_zoom": (0.4, 0.4, 0.2, 0.2), + "max_zoom_2": (0.4499885557335775, 0.4499885557335775, 0.09999237048905166, 0.09999237048905166), + "max_zoom_3": (0.5, 0.5, 0.05, 0.05), "available_exposure_modes": [ "auto", # default has to be first in the list "off", @@ -88,16 +91,26 @@ "available_isos": [0, 100, 200, 320, 400, 500, 640, 800, 1600], # 0 is auto / 3200, 6400 "iso": 0, # 800 / should shift to 0 - auto "default_iso": 0, - "available_shutter_speeds": [0, 100, 500, 1000, 2000, 4000, 8000, 16667, 33333, 66667, 125000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 15000000, 20000000, 25000000, 30000000, 35000000, 40000000], + "available_shutter_speeds": [0, 100, 500, 1000, 2000, 4000, 8000, 16667, 33333, 66667, 125000, 250000, 500000, 1000000], + "available_long_shutter_speeds": [2000000, 5000000, 10000000, 15000000, 20000000, 25000000, 30000000, 35000000, 40000000, 200000000], "shutter_speed": 0, "default_shutter_speed": 0, "available_awb_mode": ['auto', 'off', 'sunlight', 'cloudy', 'shade', 'tungsten', 'fluorescent', 'incandescent', 'flash', 'horizon'], "awb_mode": 'auto', "default_awb_mode": 'auto', # "awb_gains": 0.0 - 8.0 (), - "available_menu_items": ["auto", "exposure_mode", "iso", "shutter_speed", "awb_mode", "hdr", "video", "resolution", "encoding"], + "dpc": 0, # 0 - 3, default is 3 and 0 is disabled + "default_dpc": 0, + "raw_convert": True, + "available_dpc_options": [0, 1, 2, 3], #https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=277768 + "available_menu_items": ["auto", "exposure_mode", "iso", "shutter_speed", "long_shutter_speed", "awb_mode", "hdr", "video", "resolution", "encoding", "dpc - star eater", "raw_convert", "fom", "hdr2"], "menu_item": "auto", "default_menu_item": "auto", "hdr": False, + "preview": True, + "fom": True, + "hdr2": False, + "preview_mode": "built-in", # "built-in" "continuous_shot" + "default_preview_mode": 'built-in', "video": False, "recording": False, "encoding": False, # TODO @@ -106,130 +119,16 @@ "button_2": 23, "button_3": 22, "button_4": 17, - "bouncetime": 300 + "bouncetime": 500 } } -# filetype = config["filetype"] -# bpp = config["bpp"] -# format = config["format"] - -fps = config["fps"] -screen_fps = config["screen_fps"] - -# dcim_images_path_raw = config["dcim_images_path_raw"] -# dcim_original_images_path = config["dcim_original_images_path"] -# dcim_hdr_images_path = config["dcim_hdr_images_path"] -# dcim_videos_path = config["dcim_videos_path"] -# dcim_tmp_path = config["dcim_tmp_path"] - -# colour_profile_path = config["colour_profile_path"] - -screen_w = config["screen_w"] -screen_h = config["screen_h"] - -width = config["width"] -height = config["height"] - -# GPIO Config -button_1 = config["gpio"]["button_1"] -button_2 = config["gpio"]["button_2"] -button_3 = config["gpio"]["button_3"] -button_4 = config["gpio"]["button_4"] - -bouncetime = config["gpio"]["bouncetime"] - document_handler.check_for_folders(config) -################################################################################ -# Callbacks # -################################################################################ - -def button_callback_1(channel): - print("Button 1: Menu") - global camera - global overlay - global config - - menu_handler.select_menu_item(camera, config) - -def button_callback_2(channel): - print("Button 2: Option") - global camera - global overlay - global config - - menu_handler.select_option(camera, config) - -def button_callback_3(channel): - print("Button 3: Zoom") - global camera - global overlay - global config - - camera_handler.zoom(camera, config) - overlay = overlay_handler.add_overlay(camera, overlay, config) - -def button_callback_4(channel): - print("Button 4: Take shot") - global camera - global overlay - global config - - overlay_handler.remove_overlay(camera, overlay, config) - overlay = None - - if config["video"]: - camera_handler.trigger_video(camera, config) - else: - if config["hdr"]: - camera_handler.take_hdr_shot(camera, config) - else: - camera_handler.take_single_shot(camera, config) - - overlay = overlay_handler.add_overlay(camera, overlay, config) - ################################################################################ # Main Loop # ################################################################################ -# Start PiCam -global camera - -# Init Camera -# camera = PiCamera(framerate=config["fps"]) -camera = PiCamera(framerate=config["fps"]) -camera_handler.auto_mode(camera, config) - -global overlay -overlay = None - # Begin Camera start-up -camera.resolution = (screen_w, screen_h) -camera.framerate = screen_fps # fps - -camera.start_preview() -overlay = overlay_handler.add_overlay(camera, overlay, config) -overlay_handler.display_text(camera, '', config) - -# Set button callbacks -# GPIO.setwarnings(False) # Ignore warning for now -GPIO.setwarnings(True) -# GPIO.setmode(GPIO.BOARD) # Use physical pin numbering -GPIO.setmode(GPIO.BCM) - -GPIO.setup(button_1, GPIO.IN, pull_up_down=GPIO.PUD_UP) -GPIO.setup(button_2, GPIO.IN, pull_up_down=GPIO.PUD_UP) -GPIO.setup(button_3, GPIO.IN, pull_up_down=GPIO.PUD_UP) -GPIO.setup(button_4, GPIO.IN, pull_up_down=GPIO.PUD_UP) - -GPIO.add_event_detect(button_1, GPIO.RISING, callback=button_callback_1, bouncetime=bouncetime) -GPIO.add_event_detect(button_2, GPIO.RISING, callback=button_callback_2, bouncetime=bouncetime) -GPIO.add_event_detect(button_3, GPIO.RISING, callback=button_callback_3, bouncetime=bouncetime) -GPIO.add_event_detect(button_4, GPIO.RISING, callback=button_callback_4, bouncetime=bouncetime) - -print(f'screen: ({screen_w}, {screen_h}), res: ({width}, {height})') -message = input("Press enter to quit\n\n") # Run until someone presses enter -camera.stop_preview() -GPIO.cleanup() # Clean up -overlay_handler.remove_overlay(camera, overlay, config) +camera, overlay = camera_handler.start_camera(config) # Runs main camera loop +camera_handler.stop_camera(camera, overlay, config) diff --git a/src/menu_handler.py b/src/menu_handler.py index 29ebdea6..ae6f76ac 100644 --- a/src/menu_handler.py +++ b/src/menu_handler.py @@ -12,15 +12,17 @@ def select_menu_item(camera, config): overlay_handler.display_text(camera, '', config) print(f'menu_item: {config["menu_item"]}') -def select_option(camera, config): +def select_option(camera, overlay, config): if config["menu_item"] == "auto": - camera_handler.auto_mode(camera, config) + camera_handler.auto_mode(camera, overlay, config) if config["menu_item"] == "exposure_mode": camera_handler.adjust_exposure_mode(camera, config) if config["menu_item"] == "iso": camera_handler.adjust_iso(camera, config) if config["menu_item"] == "shutter_speed": camera_handler.adjust_shutter_speed(camera, config) + if config["menu_item"] == "long_shutter_speed": + camera_handler.long_shutter_speed(camera, config) if config["menu_item"] == "awb_mode": camera_handler.adjust_awb_mode(camera, config) if config["menu_item"] == "hdr": @@ -29,3 +31,14 @@ def select_option(camera, config): camera_handler.set_video(camera, config) if config["menu_item"] == "encoding": camera_handler.adjust_encoding(camera, config) + if config["menu_item"] == "dpc - star eater": + camera_handler.adjust_dpc(config) + camera_handler.set_dpc(camera, overlay, config) + if config["menu_item"] == "raw_convert": + camera_handler.set_raw_convert(camera, config) + if config["menu_item"] == "fom": + camera_handler.adjust_fom(camera, config) + camera_handler.set_fom(camera, config) + if config["menu_item"] == "hdr2": + camera_handler.adjust_hdr2(camera, config) + camera_handler.set_hdr2(camera, config) diff --git a/src/overlay_handler.py b/src/overlay_handler.py index 1d10cbcf..8a4f1699 100644 --- a/src/overlay_handler.py +++ b/src/overlay_handler.py @@ -20,9 +20,13 @@ def display_text(camera, text, config): camera_settings = f"exposure mode: {camera.exposure_mode}, iso: {camera.iso}, awb mode: {config['awb_mode']}" shutter_speed = compute_shutter_speed_from_us(config["shutter_speed"]) + shutter_text = f'Shutter Speed: {shutter_speed}, set: {camera.shutter_speed}' + boolean_text =f'hdr: {config["hdr"]}, raw_convert: {config["raw_convert"]}, dpc: {config["dpc"]}' + output_text = f'{mode}\n{camera_settings}\n{boolean_text}\n{selected_item}\n{shutter_text}\n{text}' - camera.annotate_text = f'{mode} - {camera_settings}\nhdr: {config["hdr"]}\n{selected_item}\n{shutter_text}\n{text}' + camera.annotate_text_size = config["annotate_text_size"] + camera.annotate_text = output_text # https://picamera.readthedocs.io/en/release-1.10/recipes1.html#overlaying-images-on-the-preview def add_overlay(camera, overlay, config): @@ -48,12 +52,15 @@ def add_overlay(camera, overlay, config): return overlay def remove_overlay(camera, overlay, config): - camera.remove_overlay(overlay) - camera.annotate_text = None - camera.framerate = config["fps"] + if camera != None and overlay != None: + camera.remove_overlay(overlay) + camera.annotate_text = None + camera.framerate = config["fps"] + + # del overlay + overlay = None - # del overlay # Doesnt work - # overlay = None # Global variable + return None def generate_overlay_image(overlay_h, overlay_w): # Create an array representing a wxh image of diff --git a/src/thread_raw_converter.py b/src/thread_raw_converter.py new file mode 100644 index 00000000..9923a781 --- /dev/null +++ b/src/thread_raw_converter.py @@ -0,0 +1,28 @@ +# https://stackoverflow.com/questions/30135091/write-thread-safe-to-file-in-python + +from threading import Thread +from thread_writer import ThreadWriter +from pydng.core import RPICAM2DNG +import document_handler + +class ThreadRawConverter: + def __init__(self, config, stream, filename): + self.config = config + self.json_colour_profile = document_handler.load_colour_profile(config) + self.filename = filename + self.stream = stream + + self.finished = False + self.thread_writer = ThreadWriter(self.filename, 'wb') + + Thread(name = "ThreadRawConverter", target=self.internal_converter).start() + + def internal_converter(self): + while not self.finished: + if self.stream != None: + # TODO: Copy over the EXIF data + output = RPICAM2DNG().convert(self.stream, json_camera_profile=self.json_colour_profile) + + self.thread_writer.write(output) + self.finished = True + self.thread_writer.close() diff --git a/src/thread_writer.py b/src/thread_writer.py new file mode 100644 index 00000000..a52bab08 --- /dev/null +++ b/src/thread_writer.py @@ -0,0 +1,35 @@ +# https://stackoverflow.com/questions/30135091/write-thread-safe-to-file-in-python + +from queue import Queue, Empty +from threading import Thread + +class ThreadWriter: + def __init__(self, *args): + self.filewriter = open(*args) + self.queue = Queue() + self.finished = False + self.auto_close = False + Thread(name = "ThreadWriter", target=self.internal_writer).start() + + def write(self, data, auto_close=False): + self.queue.put(data) + self.auto_close = auto_close + + def internal_writer(self): + while not self.finished: + try: + data = self.queue.get(True, 1) + except Empty: + continue + self.filewriter.write(data) + self.queue.task_done() + + if self.auto_close == True: + self.queue.join() + self.finished = True + self.filewriter.close() + + def close(self): + self.queue.join() + self.finished = True + self.filewriter.close() diff --git a/tools/process_hdr.py b/tools/process_hdr.py index 45579fe6..10d844db 100644 --- a/tools/process_hdr.py +++ b/tools/process_hdr.py @@ -15,6 +15,7 @@ # For now look at a static folder dcim_hdr_images_path = '/home/pi/DCIM/images/hdr' # dcim_hdr_images_path = '/mnt/i/tmp/hdr' +dcim_hdr_images_path = '/mnt/g/tmp/hdr' filetype = '.png' @@ -31,11 +32,11 @@ def compute_exposure_times(nimages): return exposure_times -files = glob.glob(f'{dcim_hdr_images_path}/*') +files = glob.glob(f'{dcim_hdr_images_path}/*.*') print(files) filenames = files # TODO: automate -nimages = 5 #10 #2160 # TODO: Automate +nimages = 9 #3 #10 #2160 # TODO: Automate frame_count = 0 # TODO: automate exposure_times = compute_exposure_times(nimages) diff --git a/tools/process_raw_output.py b/tools/process_raw_output.py index 1e8dfe81..e863445d 100644 --- a/tools/process_raw_output.py +++ b/tools/process_raw_output.py @@ -18,24 +18,24 @@ import document_handler # Constants -original_files_path = "/home/pi/DCIM/images/original" -raw_file_save_path = "/home/pi/DCIM/images/raw" +original_files_path = "/mnt/g/tmp/original" +raw_file_save_path = "/mnt/g/tmp/raw" filetype = '.dng' # TODO: List them all # Colour profiles: -# colour_profile_path = "/home/pi/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json" -# colour_profile_path = "/home/pi/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.json" -# colour_profile_path = "/home/pi/Colour_Profiles/imx477/PyDNG_profile" +# colour_profile_path = "../Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json" +# colour_profile_path = "../Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.json" +# colour_profile_path = "../Colour_Profiles/imx477/PyDNG_profile" config = { - "neutral_colour_profile": "/home/pi/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json", + "neutral_colour_profile": "../Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Neutral Look.json", "neutral_colour_profile_name": "neutral_colour", - "skin_tone_colour_profile": "/home/pi/Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.json", + "skin_tone_colour_profile": "../Colour_Profiles/imx477/Raspberry Pi High Quality Camera Lumariver 2860k-5960k Skin+Sky Look.json", "skin_tone_colour_profile_name": "skin_tone", - "pydng_colour_profile": "/home/pi/Colour_Profiles/imx477/PyDNG_profile.json", + "pydng_colour_profile": "../Colour_Profiles/imx477/PyDNG_profile.json", "pydng_colour_profile_name": "pydng", - "selected_colour_profile": "all" # can be all or neutral_colour_profile, skin_tone_colour_profile, pydng_colour_profile ... others to be added later + "selected_colour_profile": "neutral_colour_profile" #"all" # can be all or neutral_colour_profile, skin_tone_colour_profile, pydng_colour_profile ... others to be added later } def generate_filename(original_files_path, raw_file_save_path, f, config, colour_profile_name):