From 3e8b35d7251eb4f99bf0947d430b20ba18d3ca20 Mon Sep 17 00:00:00 2001 From: Monkeymademe Date: Sat, 22 Jun 2024 01:52:52 +0200 Subject: [PATCH 01/29] Removed lite --- README.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9b9f092..f7c2ff7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Picamera2 WebUI Lite +# Picamera2 WebUI ## Overview -Picamera2 WebUI Lite is a lightweight web interface for the Raspberry Pi camera module, built on the Picamera2 library. This project provides a simple user interface to configure camera settings, capture photos, and manage images in a basic gallery. +Picamera2 WebUI is a lightweight web interface for the Raspberry Pi camera module, built on the Picamera2 library. This project provides a simple user interface to configure camera settings, capture photos, and manage images in a basic gallery. [![Watch the Demo here](https://img.youtube.com/vi/K_pSdu5fv1M/0.jpg)](https://www.youtube.com/watch?v=K_pSdu5fv1M) @@ -13,10 +13,6 @@ Picamera2 WebUI Lite is a lightweight web interface for the Raspberry Pi camera - **Capture Photos:** Take photos with a single click and save them to the image gallery. - **Image Gallery:** Veiw, delete, and download your images in a simple gallery interface. -## What Does 'Lite' Mean - -This is part of a bigger project I am working on that would have some very advanced features like databases for settings and different gallery folders for example. But a lite version started to form during development so before I go down the rabbit hole of advanced features I branched this off so it nicely stands alone. - ## Is this a finished project I don't think there will be ever a point I could call this finished but at the moment there are features still in testing and missing so, no this is not a finished product. @@ -49,11 +45,11 @@ sudo apt update && sudo apt upgrade 2. Clone the repository to your Raspberry Pi: ```bash -git clone https://github.com/monkeymademe/picamera2-WebUI-Lite.git +git clone https://github.com/monkeymademe/picamera2-WebUI.git ``` 3. Enter the directory: ```bash -cd picamera2-WebUI-Lite +cd picamera2-WebUI ``` 4. Run the application and access the web interface through your browser. ```bash @@ -65,24 +61,24 @@ python app.py - Run the following command and note down the location for python which python should look like "/usr/bin/python" `which python` - Goto the following directory `cd /etc/systemd/system/` -- Create and edit the following file `sudo nano picamera2-webui-lite.service` +- Create and edit the following file `sudo nano picamera2-webui.service` - Paste this into the file, in the line "ExecStart" the 1st part should be the result of your "which python" command we did at the start (if its the same then its all good) the 2nd path is the location of the cloned repo with the app.py ```bash [Unit] -Description=Picamera2 WebUI Lite Server +Description=Picamera2 WebUI Server After=network.target [Service] Type=simple -ExecStart=/usr/bin/python /home/pi/picamera2-WebUI-Lite/app.py +ExecStart=/usr/bin/python /home/pi/picamera2-WebUI/app.py Restart=always [Install] WantedBy=multi-user.target ``` - Save the file -- Run `sudo systemctl start picamera2-webui-lite.service` to start the service -- Run the following to check the service is running `sudo systemctl status picamera2-webui-lite.service` -- Run the following to enable the service to its running on reboot `sudo systemctl enable picamera2-webui-lite.service` +- Run `sudo systemctl start picamera2-webui.service` to start the service +- Run the following to check the service is running `sudo systemctl status picamera2-webui.service` +- Run the following to enable the service to its running on reboot `sudo systemctl enable picamera2-webui.service` ## Compatibilty @@ -113,4 +109,4 @@ Raspberry Pi Compatibilty: - If the camera is not connected the system will not load ## Copyright and license -Code and documentation copyright 2024 the Picamera2 WebUI Lite Authors. Code released under the MIT License. Docs released under Creative Commons. +Code and documentation copyright 2024 the Picamera2 WebUI Authors. Code released under the MIT License. Docs released under Creative Commons. From 70d6df8949a1bde3c0904611765ae43046da115e Mon Sep 17 00:00:00 2001 From: Monkeymademe Date: Sat, 22 Jun 2024 02:00:27 +0200 Subject: [PATCH 02/29] Putting Beta into beta branch --- .gitignore | 2 + app.py | 758 +++++++++++------------ camera-config.json | 8 +- camera-last-config.json | 18 + camera-module-info.json | 18 +- static/camera_config/camera-1.json | 60 ++ static/camera_config/camera-2.json | 60 ++ static/camera_config/default_imx708.json | 60 ++ templates/about.html | 4 +- templates/base.html | 45 +- templates/camera_info.html | 13 +- templates/camerasettings.html | 485 +++++++++++---- templates/home.html | 37 ++ 13 files changed, 1057 insertions(+), 511 deletions(-) create mode 100644 .gitignore create mode 100644 camera-last-config.json create mode 100644 static/camera_config/camera-1.json create mode 100644 static/camera_config/camera-2.json create mode 100644 static/camera_config/default_imx708.json create mode 100644 templates/home.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd40cb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +gallery/ +gallery/snapshot/ diff --git a/app.py b/app.py index fb260ec..9d2a994 100644 --- a/app.py +++ b/app.py @@ -11,73 +11,44 @@ from picamera2 import Picamera2 from picamera2.encoders import JpegEncoder from picamera2.encoders import MJPEGEncoder +from picamera2.encoders import H264Encoder from picamera2.outputs import FileOutput from libcamera import Transform, controls -# Int Flask +# Init Flask app = Flask(__name__) -# Int Picamera2 and default settings -picam2 = Picamera2() +Picamera2.set_logging(Picamera2.DEBUG) + +# Get global camera information +global_cameras = Picamera2.global_camera_info() +# global_cameras = [global_cameras[0]] -# Int Picamera2 and default settings -timelapse_running = False -timelapse_thread = None # Get the directory of the current script current_dir = os.path.dirname(os.path.abspath(__file__)) # Define the path to the camera-config.json file camera_config_path = os.path.join(current_dir, 'camera-config.json') -# Pull settings from from config file -with open(camera_config_path, "r") as file: - camera_config = json.load(file) -# Print config for validation -print(f'\nCamera Config:\n{camera_config}\n') - -# Split config for different uses -live_settings = camera_config.get('controls', {}) -rotation_settings = camera_config.get('rotation', {}) -sensor_mode = camera_config.get('sensor-mode', 1) -capture_settings = camera_config.get('capture-settings', {}) - -# Parse the selected capture resolution for later -selected_resolution = capture_settings["Resolution"] -resolution = capture_settings["available-resolutions"][selected_resolution] -print(f'\nCamera Settings:\n{capture_settings}\n') -print(f'\nCamera Set Resolution:\n{resolution}\n') - -# Get the sensor modes and pick from the the camera_config -camera_modes = picam2.sensor_modes -mode = picam2.sensor_modes[sensor_mode] - -# Create the video_config -video_config = picam2.create_video_configuration(main={'size':resolution}, sensor={'output_size': mode['size'], 'bit_depth': mode['bit_depth']}) -print(f'\nVideo Config:\n{video_config}\n') - -# Pull default settings and filter live_settings for anything picamera2 wont use (because the not all cameras use all settings) -default_settings = picam2.camera_controls -live_settings = {key: value for key, value in live_settings.items() if key in default_settings} - -# Define the path to the camera-module-info.json file -camera_module_info_path = os.path.join(current_dir, 'camera-module-info.json') -# Load camera modules data from the JSON file -with open(camera_module_info_path, "r") as file: + +# Load the camera-module-info.json file +with open(os.path.join(current_dir, 'camera-module-info.json'), 'r') as file: camera_module_info = json.load(file) -camera_properties = picam2.camera_properties -print(f'\nPicamera2 Camera Properties:\n{camera_properties}\n') -# Set the path where the images will be stored -UPLOAD_FOLDER = os.path.join(current_dir, 'static/gallery') -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +# Load the JSON configuration file +with open(os.path.join(current_dir, 'camera-last-config.json'), 'r') as file: + camera_last_config = json.load(file) +# Set the path where the images will be stored +CAMERA_CONFIG_FOLDER = os.path.join(current_dir, 'static/camera_config') +app.config['CAMERA_CONFIG_FOLDER'] = CAMERA_CONFIG_FOLDER +print(CAMERA_CONFIG_FOLDER) # Create the upload folder if it doesn't exist -os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +os.makedirs(app.config['CAMERA_CONFIG_FOLDER'], exist_ok=True) -#################### -# Streaming Class -#################### +# Set the path where the images will be stored +UPLOAD_FOLDER = os.path.join(current_dir, 'static/gallery') +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER -output = None class StreamingOutput(io.BufferedIOBase): def __init__(self): self.frame = None @@ -88,348 +59,402 @@ def write(self, buf): self.frame = buf self.condition.notify_all() -def generate(): - global output +# Define a function to generate the stream for a specific camera +def generate_stream(camera): while True: - with output.condition: - output.condition.wait() - frame = output.frame + with camera.output.condition: + camera.output.condition.wait() + frame = camera.output.frame yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + +# CameraObject that will store the itteration of 1 or more cameras +class CameraObject: + def __init__(self, camera_num, camera_info): + # Init camera to picamera2 using the camera number + self.camera = Picamera2(camera_num) + # Basic Camera Info (Sensor type etc) + self.camera_info = camera_info + # Default controls for the Camera + self.settings = self.camera.camera_controls + # Lists all sensor modes + self.sensor_modes = self.camera.sensor_modes + # Using the output from sensor_modes generate a list of available resolutions + self.output_resolutions = self.available_resolutions() + -#################### -# Load Config from file Function -#################### -# Load camera settings from config file -def load_settings(settings_file): - try: - with open(settings_file, 'r') as file: - settings = json.load(file) - print(settings) - return settings - except FileNotFoundError: - # Return default settings if the file is not found - logging.error(f"Settings file {settings_file} not found") - return None - except Exception as e: - logging.error(f"Error loading camera settings: {e}") - return None + self.output = None + # need an if statment for checking if there is config or load a default template for now this is ok cause config is assumed + #self.saved_config = self.load_settings_from_file(camera_info['Config_Location']) + self.init_camera() + print(f'\nSaved Config:\n{self.saved_config}\n') + self.live_config = {} + self.live_config = self.saved_config + print(f'\nLive Config:\n{self.live_config}\n') + print(f"\nSensor Mode:\n{self.live_config['sensor-mode']}\n") + + def build_default_config(self): + default_config = {} + for control, values in self.settings.items(): + if control in ['ScalerCrop', 'AfPause', 'FrameDurationLimits', 'NoiseReductionMode', 'AfMetering', 'ColourGains', 'StatsOutputEnable', 'AnalogueGain', 'AfWindows', 'AeFlickerPeriod', 'HdrMode', 'AfTrigger']: + continue # Skip ScalerCrop for debugging purposes + + if isinstance(values, tuple) and len(values) == 3: + min_value, max_value, default_value = values + + # Handle default_value being None + if default_value is None: + default_value = min_value # Assign minimum value if default is None + + # Handle array or span types (example with ScalerCrop) + if isinstance(min_value, (list, tuple)): + default_value = list(min_value) # Convert to list if needed + + default_config[control] = default_value + + return default_config + + + + + def available_resolutions(self): + # Use a set to collect unique resolutions + resolutions_set = set() + for mode in self.sensor_modes: + size = mode.get('size') + if size: + resolutions_set.add(size) + # Convert the set back to a list + unique_resolutions = list(resolutions_set) + # Sort the resolutions from smallest to largest + sorted_resolutions = sorted(unique_resolutions, key=lambda x: (x[0] * x[1], x)) + return sorted_resolutions + + def start_streaming(self): + self.output = StreamingOutput() + self.camera.start_recording(MJPEGEncoder(), output=FileOutput(self.output)) + time.sleep(1) + + def stop_streaming(self): + self.camera.stop_recording() + + def load_settings_from_file(self, config_location): + with open(os.path.join(CAMERA_CONFIG_FOLDER ,config_location), 'r') as file: + return json.load(file) + + def update_settings(self, new_settings): + self.settings.update(new_settings) + + def save_settings_to_file(self): + with open(self.camera_info['Config_Location'], 'w') as file: + json.dump(self.settings, file) + + def configure_camera(self): + try: + # Attempt to set the controls + self.camera.set_controls(self.live_config['controls']) + print('Controls set successfully.') + + # Adding a small sleep to ensure operations are completed + time.sleep(0.5) + except Exception as e: + # Log the exception + logging.error("An error occurred while configuring the camera: %s", str(e)) + print(f"An error occurred: {str(e)}") + + def init_camera(self): + self.capture_settings = { + "Resize": False, + "makeRaw": False, + "Resolution": 0 + } + self.rotation = { + "hflip": 0, + "vflip": 0 + } + self.sensor_mode = 1 + # If no config file use default generated from controls + self.live_settings = self.build_default_config() + # Parse the selected capture resolution for later + selected_resolution = self.capture_settings["Resolution"] + resolution = self.output_resolutions[selected_resolution] + print(f'\nCamera Settings:\n{self.capture_settings}\n') + print(f'\nCamera Set Resolution:\n{resolution}\n') + + # Get the sensor modes and pick from the the camera_config + mode = self.camera.sensor_modes[self.sensor_mode] + print(f'MODE Config:\n{mode}\n') + self.video_config = self.camera.create_video_configuration(main={'size':resolution}) + + # self.video_config = self.camera.create_video_configuration(main={'size':resolution}, sensor={'output_size': mode['size'], 'bit_depth': mode['bit_depth']}) + print(f'\nVideo Config:\n{self.video_config}\n') + self.camera.configure(self.video_config) + # Pull default settings and filter live_settings for anything picamera2 wont use (because the not all cameras use all settings) + self.live_settings = {key: value for key, value in self.live_settings.items() if key in self.settings} + self.camera.set_controls(self.live_settings) + self.rotation_settings = self.rotation + self.saved_config = {'controls':self.live_settings, 'rotation':self.rotation, 'sensor-mode':int(self.sensor_mode), 'capture-settings':self.capture_settings} + + def update_live_config(self, data): + # Update only the keys that are present in the data + print(data) + for key in data: + if key in self.live_config['controls']: + try: + if key in ('AfMode', 'AeConstraintMode', 'AeExposureMode', 'AeFlickerMode', 'AeFlickerPeriod', 'AeMeteringMode', 'AfRange', 'AfSpeed', 'AwbMode', 'ExposureTime') : + self.live_config['controls'][key] = int(data[key]) + elif key in ('Brightness', 'Contrast', 'Saturation', 'Sharpness', 'ExposureValue', 'LensPosition'): + self.live_config['controls'][key] = float(data[key]) + elif key in ('AeEnable', 'AwbEnable', 'ScalerCrop'): + self.live_config['controls'][key] = data[key] + # Update the configuration of the video feed + self.configure_camera() + success = True + settings = self.live_config['controls'] + print(settings) + return success, settings + except Exception as e: + logging.error(f"Error capturing image: {e}") + elif key in self.live_config['capture-settings']: + if key == 'Resolution': + self.live_config['capture-settings']['Resolution'] = int(data[key]) + selected_resolution = int(data[key]) + resolution = self.output_resolutions[selected_resolution] + mode = self.camera.sensor_modes[self.sensor_mode] + self.stop_streaming() + self.video_config = self.camera.create_video_configuration(main={'size':resolution}, sensor={'output_size': mode['size'], 'bit_depth': mode['bit_depth']}) + self.camera.configure(self.video_config) + self.start_streaming() + success = True + settings = self.live_config['capture-settings'] + return success, settings + elif key == 'makeRaw': + self.live_config['capture-settings'][key] = data[key] + success = True + settings = self.live_config['capture-settings'] + return success, settings + elif key == 'sensor-mode': + self.sensor_mode = sensor_mode = int(data[key]) + mode = self.camera.sensor_modes[self.sensor_mode] + print("MODE") + print(mode) + self.live_config['sensor-mode'] = int(data[key]) + resolution = mode['size'] + self.stop_streaming() + try: + self.video_config = self.camera.create_video_configuration(main={'size': resolution}) + except Exception as e: + # Log the exception + logging.error("An error occurred while configuring the camera: %s", str(e)) + print(f"An error occurred: {str(e)}") + print(resolution) + self.camera.configure(self.video_config) + print(f'\nVideo Config:\n{self.video_config}\n') + print(self.camera_info) + print(self.settings) + self.start_streaming() + success = True + settings = self.live_config['sensor-mode'] + return success, settings + + def apply_rotation(self,data): + self.stop_streaming() + transform = Transform() + # Update settings that require a restart + for key, value in data.items(): + if key in self.live_config['rotation']: + if key in ('hflip', 'vflip'): + self.live_config['rotation'][key] = data[key] + setattr(transform, key, value) + self.video_config['transform'] = transform + self.camera.configure(self.video_config) + time.sleep(0.5) + self.start_streaming() + success = True + settings = self.live_config['rotation'] + return success, settings + + def take_snapshot(self,camera_num): + try: + image_name = f'snapshot/pimage_snapshot_{camera_num}' + filepath = os.path.join(app.config['UPLOAD_FOLDER'], image_name) + request = self.camera.capture_request() + request.save("main", f'{filepath}.jpg') + logging.info(f"Image captured successfully. Path: {filepath}") + return f'{filepath}.jpg' + except Exception as e: + logging.error(f"Error capturing image: {e}") + + +# Init dictionary to store camera instances +cameras = {} +camera_new_config = {'cameras': []} +print(f'\nDetected Cameras:\n{global_cameras}\n') +# Iterate over each camera in the global_cameras list +for camera_info in global_cameras: + # Flag to check if a matching camera is found in the last config + matching_camera_found = False + print(f'\nCamera Info:\n{camera_info}\n') + # Get the number of the camera in the global_cameras list + camera_num = camera_info['Num'] + # Iterate over each camera in the last config + for camera_info_last in camera_last_config['cameras']: + # Check if the camera number and model match + print(camera_info_last) + print(f"{camera_info['Num']} {camera_info_last['Num']} and {camera_info['Model']} {camera_info_last['Model']}") + if (camera_info['Num'] == camera_info_last['Num'] and camera_info['Model'] == camera_info_last['Model']): + print(f"Detected camera {camera_info['Num']}: {camera_info['Model']} matched last used in config.") + # Add the matching camera to the new config + camera_new_config['cameras'].append(camera_info_last) + # Set the flag to True + matching_camera_found = True + # Merge some data before creating object + camera_info['Config_Location'] = camera_new_config['cameras'][camera_num]['Config_Location'] + camera_info['Has_Config'] = camera_new_config['cameras'][camera_num]['Has_Config'] + # Create an instance of the custom CameraObject class + camera_obj = CameraObject(camera_num, camera_info) + # Start the camera + camera_obj.start_streaming() + # Add the camera instance to the dictionary + cameras[camera_num] = camera_obj + + # If camera is not matching the last config, check its a pi camera if not a supported pi camera module its skipped + if not matching_camera_found: + is_pi_cam = False + # Iterate over the supported Camera Modules and look for a match + for camera_modules in camera_module_info['camera_modules']: + if (camera_info['Model'] == camera_modules['sensor_model']): + is_pi_cam = True + print("Camera config has changed since last boot - Adding new Camera") + add_camera_config = {'Num':camera_info['Num'], 'Model':camera_info['Model'], 'Is_Pi_Cam': is_pi_cam, 'Has_Config': False, 'Config_Location': f"default_{camera_info['Model']}.json"} + camera_new_config['cameras'].append(add_camera_config) + # Merge some data before creating object + camera_info['Config_Location'] = camera_new_config['cameras'][camera_num]['Config_Location'] + camera_info['Has_Config'] = camera_new_config['cameras'][camera_num]['Has_Config'] + # Create an instance of the custom CameraObject class + camera_obj = CameraObject(camera_num, camera_info) + # Start the camera + camera_obj.start_streaming() + # Add the camera instance to the dictionary + cameras[camera_num] = camera_obj + +# Print the new config for debug +print(f'\nCurrent detected compatible Cameras:\n{camera_new_config}\n') +# Write config to last config file for next reboot +with open(os.path.join(current_dir, 'camera-last-config.json'), 'w') as file: + json.dump(camera_new_config, file, indent=4) + #################### # Site Routes (routes to actual pages) #################### -@app.route("/") + +# Define your 'home' route +@app.route('/') def home(): - return render_template("camerasettings.html", title="Picamera2 WebUI Lite", live_settings=live_settings, rotation_settings=rotation_settings, settings_from_camera=default_settings, capture_settings=capture_settings) + # Assuming cameras is a dictionary containing your CameraObjects + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + print(f'Camera Data {cameras_data}') + camera_list = [(camera_num, camera, camera.camera_info['Model']) for camera_num, camera in cameras.items()] + # Pass cameras_data as a context variable to your template + return render_template('home.html', title="Picamera2 WebUI", cameras_data=cameras_data, camera_list=camera_list) + +@app.route('/control_camera_') +def control_camera(camera_num): + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + camera = cameras.get(camera_num) + resolutions = camera.available_resolutions() + print(camera.live_config.get('capture-settings')) + if camera: + return render_template("camerasettings.html", title="Picamera2 WebUI - Camera ", cameras_data=cameras_data, camera_num=camera_num, live_settings=camera.live_config.get('controls'), rotation_settings=camera.live_config.get('rotation'), settings_from_camera=camera.settings, capture_settings=camera.live_config.get('capture-settings'), resolutions=resolutions, enumerate=enumerate) + else: + abort(404) @app.route("/beta") def beta(): return render_template("beta.html", title="beta") -@app.route("/camera_info") -def camera_info(): - connected_camera = picam2.camera_properties['Model'] +@app.route("/camera_info_") +def camera_info(camera_num): + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + camera = cameras.get(camera_num) + connected_camera = camera.camera_info['Model'] connected_camera_data = next((module for module in camera_module_info["camera_modules"] if module["sensor_model"] == connected_camera), None) if connected_camera_data: - return render_template("camera_info.html", title="Camera Info", connected_camera_data=connected_camera_data, camera_modes=camera_modes, sensor_mode=sensor_mode) + return render_template("camera_info.html", title="Camera Info", cameras_data=cameras_data, camera_num=camera_num, connected_camera_data=connected_camera_data, camera_modes=camera.sensor_modes, sensor_mode=camera.live_config.get('sensor-mode')) else: return jsonify(error="Camera module data not found") @app.route("/about") def about(): - return render_template("about.html", title="About Picamera2 WebUI Lite") - -@app.route('/video_feed') -def video_feed(): - return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame') - -@app.route('/snapshot') -def snapshot(): - # Capture an image - take_snapshot() - # Wait for a few seconds to ensure the image is saved - time.sleep(2) - # Return the image file - image_name = 'snapshot/pimage_snapshot.jpg' - filepath = os.path.join(app.config['UPLOAD_FOLDER'], image_name) - return send_file(filepath, as_attachment=False, download_name="snapshot.jpg", mimetype='image/jpeg') + return render_template("about.html", title="About Picamera2 WebUI") + +@app.route('/video_feed_') +def video_feed(camera_num): + camera = cameras.get(camera_num) + if camera: + return Response(generate_stream(camera), mimetype='multipart/x-mixed-replace; boundary=frame') + else: + abort(404) + + +@app.route('/snapshot_') +def snapshot(camera_num): + camera = cameras.get(camera_num) + if camera: + # Capture an image + filepath = camera.take_snapshot(camera_num) + # Wait for a few seconds to ensure the image is saved + time.sleep(1) + return send_file(filepath, as_attachment=False, download_name="snapshot.jpg", mimetype='image/jpeg') + else: + abort(404) #################### -# Setting Routes (routes that manipulate settings) +# POST routes for saving data #################### # Route to update settings to the buffer -@app.route('/update_live_settings', methods=['POST']) -def update_settings(): - global live_settings, capture_settings, picam2, video_config, resolution, sensor_mode, mode +@app.route('/update_live_settings_', methods=['POST']) +def update_settings(camera_num): + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + camera = cameras.get(camera_num) try: # Parse JSON data from the request data = request.get_json() print(data) - # Update only the keys that are present in the data - for key in data: - if key in live_settings: - print(key) - if key in ('AfMode', 'AeConstraintMode', 'AeExposureMode', 'AeFlickerMode', 'AeFlickerPeriod', 'AeMeteringMode', 'AfRange', 'AfSpeed', 'AwbMode', 'ExposureTime') : - live_settings[key] = int(data[key]) - elif key in ('Brightness', 'Contrast', 'Saturation', 'Sharpness', 'ExposureValue', 'LensPosition'): - live_settings[key] = float(data[key]) - elif key in ('AeEnable', 'AwbEnable', 'ScalerCrop'): - live_settings[key] = data[key] - # Update the configuration of the video feed - configure_camera(live_settings) - return jsonify(success=True, message="Settings updated successfully", settings=live_settings) - elif key in capture_settings: - if key in ('Resolution'): - capture_settings['Resolution'] = int(data[key]) - selected_resolution = int(data[key]) - resolution = capture_settings["available-resolutions"][selected_resolution] - stop_camera_stream() - video_config = picam2.create_video_configuration(main={'size':resolution}, sensor={'output_size': mode['size'], 'bit_depth': mode['bit_depth']}) - start_camera_stream() - return jsonify(success=True, message="Settings updated successfully", settings=capture_settings) - elif key in ('makeRaw'): - capture_settings[key] = data[key] - return jsonify(success=True, message="Settings updated successfully", settings=capture_settings) - elif key == ('sensor_mode'): - sensor_mode = int(data[key]) - mode = picam2.sensor_modes[sensor_mode] - stop_camera_stream() - video_config = picam2.create_video_configuration(main={'size':resolution}, sensor={'output_size': mode['size'], 'bit_depth': mode['bit_depth']}) - start_camera_stream() - save_sensor_mode(sensor_mode) - return jsonify(success=True, message="Settings updated successfully", settings=sensor_mode) - except Exception as e: - return jsonify(success=False, message=str(e)) -# Route to update settings that requires a restart of the stream -@app.route('/update_restart_settings', methods=['POST']) -def update_restart_settings(): - global rotation_settings, video_config - try: - data = request.get_json() - stop_camera_stream() - transform = Transform() - # Update settings that require a restart - for key, value in data.items(): - if key in rotation_settings: - if key in ('hflip', 'vflip'): - rotation_settings[key] = data[key] - setattr(transform, key, value) - video_config["transform"] = transform - start_camera_stream() - return jsonify(success=True, message="Restart settings updated successfully", settings=live_settings) + success, settings = camera.update_live_config(data) + print(settings) + return jsonify(success=success, message="Settings updated successfully", settings=settings) except Exception as e: return jsonify(success=False, message=str(e)) -@app.route('/reset_default_live_settings', methods=['GET']) -def reset_default_live_settings(): - global live_settings, rotation_settings - try: - # Get the default settings from picam2.camera_controls - default_settings = picam2.camera_controls - - # Apply only the default values to live_settings - for key in default_settings: - if key in live_settings: - min_value, max_value, default_value = default_settings[key] - live_settings[key] = default_value if default_value is not None else max_value - configure_camera(live_settings) - - # Reset rotation settings and restart stream - for key, value in rotation_settings.items(): - rotation_settings[key] = 0 - restart_configure_camera(rotation_settings) - - return jsonify(data1=live_settings, data2=rotation_settings) - except Exception as e: - return jsonify(error=str(e)) - -# Add a new route to save settings -@app.route('/save_settings', methods=['GET']) -def save_settings(): - global live_settings, rotation_settings, capture_settings, camera_config +@app.route('/update_restart_settings_', methods=['POST']) +def update_restart_settings(camera_num): + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + camera = cameras.get(camera_num) try: - with open('camera-config.json', 'r') as file: - camera_config = json.load(file) - - # Update controls in the configuration with live_settings - for key, value in live_settings.items(): - if key in camera_config['controls']: - camera_config['controls'][key] = value - - # Update controls in the configuration with rotation settings - for key, value in rotation_settings.items(): - if key in camera_config['rotation']: - camera_config['rotation'][key] = value - - # Update controls in the configuration with rotation settings - for key, value in capture_settings.items(): - if key in camera_config['capture-settings']: - camera_config['capture-settings'][key] = value - - # Save current camera settings to the JSON file - with open('camera-config.json', 'w') as file: - json.dump(camera_config, file, indent=4) - - return jsonify(success=True, message="Settings saved successfully") + data = request.get_json() + print(data) + success, settings = camera.apply_rotation(data) + return jsonify(success=True, message="Restart settings updated successfully", settings=settings) except Exception as e: - logging.error(f"Error in saving data: {e}") return jsonify(success=False, message=str(e)) - -def save_sensor_mode(sensor_mode): - try: - with open('camera-config.json', 'r') as file: - camera_config = json.load(file) - # Update sensor mode - camera_config['sensor-mode'] = sensor_mode - - # Save current camera settings to the JSON file - with open('camera-config.json', 'w') as file: - json.dump(camera_config, file, indent=4) - return jsonify(success=True, message="Settings saved successfully") - except Exception as e: - logging.error(f"Error in saving data: {e}") - return jsonify(success=False, message=str(e)) - -#################### -# Start/Stop Steam and Take photo -#################### - -def start_camera_stream(): - global picam2, output, video_config - picam2.configure(video_config) - output = StreamingOutput() - picam2.start_recording(JpegEncoder(), FileOutput(output)) - metadata = picam2.capture_metadata() - time.sleep(1) - -def stop_camera_stream(): - global picam2 - picam2.stop_recording() - time.sleep(1) - -# Define the route for capturing a photo -@app.route('/capture_photo', methods=['POST']) -def capture_photo(): - try: - take_photo() # Call your take_photo function - time.sleep(1) - return jsonify(success=True, message="Photo captured successfully") - except Exception as e: - return jsonify(success=False, message=str(e)) - -# Route to stop the timelapse -@app.route('/stop_timelapse', methods=['POST']) -def stop_timelapse(): - global timelapse_running, timelapse_thread - print("Stop timelapse button pressed") - # Check if the timelapse is running - if timelapse_running: - # Set the timelapse flag to False - timelapse_running = False - - # Wait for the timelapse thread to finish - if timelapse_thread: - timelapse_thread.join() - - return jsonify(success=True, message="Timelapse stopped successfully") - else: - print("Timelapse is not running") - return jsonify(success=True, message="Timelapse is not running") - - # Route to start the timelapse -@app.route('/start_timelapse', methods=['POST']) -def start_timelapse(): - global timelapse_running, timelapse_thread - # Check if the timelapse is already running - if not timelapse_running: - # Specify the interval between images (in seconds) - interval = 2 - - # Set the timelapse flag to True - timelapse_running = True - - # Create a new thread to run the timelapse function - timelapse_thread = threading.Thread(target=take_lapse, args=(interval,)) - - # Start the timelapse thread - timelapse_thread.start() - - return jsonify(success=True, message="Timelapse started successfully") - else: - print("Timelapse is already running") - return jsonify(success=True, message="Timelapse is already running") - -# Function to take images for timelapse -def take_lapse(interval): - global timelapse_running - while timelapse_running: - take_photo() - time.sleep(interval) - -def take_photo(): - global picam2, capture_settings - try: - timestamp = int(datetime.timestamp(datetime.now())) - image_name = f'pimage_{timestamp}' - filepath = os.path.join(app.config['UPLOAD_FOLDER'], image_name) - request = picam2.capture_request() - request.save("main", f'{filepath}.jpg') - if capture_settings["makeRaw"]: - request.save_dng(f'{filepath}.dng') - request.release() - #selected_resolution = capture_settings["Resolution"] - #resolution = capture_settings["available-resolutions"][selected_resolution] - #original_image = Image.open(filepath) - #resized_image = original_image.resize(resolution) - #resized_image.save(filepath) - logging.info(f"Image captured successfully. Path: {filepath}") - except Exception as e: - logging.error(f"Error capturing image: {e}") - -def take_snapshot(): - global picam2, capture_settings - try: - image_name = f'snapshot/pimage_snapshot' - filepath = os.path.join(app.config['UPLOAD_FOLDER'], image_name) - request = picam2.capture_request() - request.save("main", f'{filepath}.jpg') - logging.info(f"Image captured successfully. Path: {filepath}") - except Exception as e: - logging.error(f"Error capturing image: {e}") - -#################### -# Configure Camera -#################### - -def configure_camera(live_settings): - picam2.set_controls(live_settings) - time.sleep(0.5) - -def restart_configure_camera(restart_settings): - stop_camera_stream() - transform = Transform() - # Update settings that require a restart - for key, value in restart_settings.items(): - if key in restart_settings: - if key in ('hflip', 'vflip'): - setattr(transform, key, value) - video_config["transform"] = transform - start_camera_stream() #################### # Image Gallery Functions #################### -from datetime import datetime -import os - @app.route('/image_gallery') def image_gallery(): + # Assuming cameras is a dictionary containing your CameraObjects + cameras_data = [(camera_num, camera) for camera_num, camera in cameras.items()] + print(f'Camera Data {cameras_data}') + camera_list = [(camera_num, camera, camera.camera_info['Model']) for camera_num, camera in cameras.items()] try: image_files = [f for f in os.listdir(UPLOAD_FOLDER) if f.endswith('.jpg')] print(image_files) @@ -467,48 +492,13 @@ def image_gallery(): end_index = start_index + items_per_page files_and_timestamps_page = files_and_timestamps[start_index:end_index] - return render_template('image_gallery.html', image_files=files_and_timestamps_page, page=page, start_page=start_page, end_page=end_page) + return render_template('image_gallery.html', image_files=files_and_timestamps_page, page=page, start_page=start_page, end_page=end_page, cameras_data=cameras_data, camera_list=camera_list) except Exception as e: logging.error(f"Error loading image gallery: {e}") - return render_template('error.html', error=str(e)) + return render_template('error.html', error=str(e), cameras_data=cameras_data, camera_list=camera_list) -@app.route('/delete_image/', methods=['DELETE']) -def delete_image(filename): - try: - filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) - os.remove(filepath) - return jsonify(success=True, message="Image deleted successfully") - except Exception as e: - return jsonify(success=False, message=str(e)) - -@app.route('/view_image/', methods=['GET']) -def view_image(filename): - # Pass the filename or any other necessary information to the template - return render_template('view_image.html', filename=filename) - -@app.route('/download_image/', methods=['GET']) -def download_image(filename): - try: - image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) - return send_file(image_path, as_attachment=True) - except Exception as e: - print(f"Error downloading image: {e}") - abort(500) - -#################### -# Lets get the party started -#################### - if __name__ == "__main__": - # Configure logging - logging.basicConfig(level=logging.INFO) # Change the level to DEBUG for more detailed logging - - # Start Camera stream - start_camera_stream() - - # Load and set camera settings - configure_camera(live_settings) # Start the Flask application - app.run(debug=False, host='0.0.0.0', port=8080) + app.run(debug=False, host='0.0.0.0', port=8080) \ No newline at end of file diff --git a/camera-config.json b/camera-config.json index 3bdc84f..772e6f8 100644 --- a/camera-config.json +++ b/camera-config.json @@ -2,7 +2,7 @@ "controls": { "LensPosition": 1.0, "ExposureValue": 0.0, - "ExposureTime": 220417486, + "ExposureTime": 5883414, "AwbMode": 0, "AfSpeed": 0, "AfRange": 0, @@ -21,15 +21,15 @@ "ScalerCrop": [ 0, 0, - 4608, - 2592 + 3280, + 2464 ] }, "rotation": { "hflip": 0, "vflip": 0 }, - "sensor-mode": 0, + "sensor-mode": 7, "capture-settings": { "Resize": false, "makeRaw": false, diff --git a/camera-last-config.json b/camera-last-config.json new file mode 100644 index 0000000..4f6302b --- /dev/null +++ b/camera-last-config.json @@ -0,0 +1,18 @@ +{ + "cameras": [ + { + "Num": 0, + "Model": "imx219", + "Is_Pi_Cam": true, + "Has_Config": true, + "Config_Location": "camera-1.json" + }, + { + "Num": 1, + "Model": "imx708", + "Is_Pi_Cam": true, + "Has_Config": false, + "Config_Location": "default_imx708.json" + } + ] +} \ No newline at end of file diff --git a/camera-module-info.json b/camera-module-info.json index c1e3f76..50d0350 100644 --- a/camera-module-info.json +++ b/camera-module-info.json @@ -18,7 +18,8 @@ "f_stop": "F2.9", "max_exposure_time": "0.97", "lens_mount": "N/A", - "got_noir": "Yes" + "got_noir": "Yes", + "is_pi_cam": true }, { "module_name": "Camera Module v2", @@ -38,7 +39,8 @@ "f_stop": "F2.0", "max_exposure_time": "11.76", "lens_mount": "N/A", - "got_noir": "Yes" + "got_noir": "Yes", + "is_pi_cam": true }, { "module_name": "Camera Module v3", @@ -58,7 +60,8 @@ "f_stop": "F2.2", "max_exposure_time": "112", "lens_mount": "N/A", - "got_noir": "Yes" + "got_noir": "Yes", + "is_pi_cam": true }, { "module_name": "Camera Module v3 Wide Angle", @@ -78,7 +81,8 @@ "f_stop": "F2.2", "max_exposure_time": "112", "lens_mount": "N/A", - "got_noir": "Yes" + "got_noir": "Yes", + "is_pi_cam": true }, { "module_name": "HQ Camera", @@ -98,7 +102,8 @@ "f_stop": "Depends on lens", "max_exposure_time": "670.74", "lens_mount": "C/CS- or M12-mount", - "got_noir": "No" + "got_noir": "No", + "is_pi_cam": true }, { "module_name": "GS Camera", @@ -118,7 +123,8 @@ "f_stop": "Depends on lens", "max_exposure_time": "15.5", "lens_mount": "C/CS", - "got_noir": "No" + "got_noir": "No", + "is_pi_cam": true } ] } \ No newline at end of file diff --git a/static/camera_config/camera-1.json b/static/camera_config/camera-1.json new file mode 100644 index 0000000..88d17f3 --- /dev/null +++ b/static/camera_config/camera-1.json @@ -0,0 +1,60 @@ +{ + "controls": { + "LensPosition": 1.0, + "ExposureValue": 0.0, + "ExposureTime": 5883414, + "AwbMode": 0, + "AfSpeed": 0, + "AfRange": 0, + "AeMeteringMode": 0, + "AeFlickerPeriod": 1000000, + "AeFlickerMode": 0, + "AeExposureMode": 0, + "AeEnable": true, + "AwbEnable": true, + "AeConstraintMode": 0, + "Brightness": 0.0, + "Contrast": 1.0, + "Saturation": 1.0, + "Sharpness": 1.0, + "AfMode": 0, + "ScalerCrop": [ + 0, + 0, + 3280, + 2464 + ] + }, + "rotation": { + "hflip": 0, + "vflip": 0 + }, + "sensor-mode": 2, + "capture-settings": { + "Resize": false, + "makeRaw": false, + "Resolution": 0, + "available-resolutions": [ + [ + 4608, + 2592 + ], + [ + 2304, + 1296 + ], + [ + 1920, + 1080 + ], + [ + 1280, + 720 + ], + [ + 640, + 360 + ] + ] + } +} \ No newline at end of file diff --git a/static/camera_config/camera-2.json b/static/camera_config/camera-2.json new file mode 100644 index 0000000..ff34e07 --- /dev/null +++ b/static/camera_config/camera-2.json @@ -0,0 +1,60 @@ +{ + "controls": { + "LensPosition": 1.0, + "ExposureValue": 0.0, + "ExposureTime": 5883414, + "AwbMode": 0, + "AfSpeed": 0, + "AfRange": 0, + "AeMeteringMode": 0, + "AeFlickerPeriod": 1000000, + "AeFlickerMode": 0, + "AeExposureMode": 0, + "AeEnable": true, + "AwbEnable": true, + "AeConstraintMode": 0, + "Brightness": 0.0, + "Contrast": 1.0, + "Saturation": 1.0, + "Sharpness": 1.0, + "AfMode": 1, + "ScalerCrop": [ + 0, + 0, + 3280, + 2464 + ] + }, + "rotation": { + "hflip": 0, + "vflip": 0 + }, + "sensor-mode": 2, + "capture-settings": { + "Resize": false, + "makeRaw": false, + "Resolution": 0, + "available-resolutions": [ + [ + 4608, + 2592 + ], + [ + 2304, + 1296 + ], + [ + 1920, + 1080 + ], + [ + 1280, + 720 + ], + [ + 640, + 360 + ] + ] + } +} \ No newline at end of file diff --git a/static/camera_config/default_imx708.json b/static/camera_config/default_imx708.json new file mode 100644 index 0000000..ff34e07 --- /dev/null +++ b/static/camera_config/default_imx708.json @@ -0,0 +1,60 @@ +{ + "controls": { + "LensPosition": 1.0, + "ExposureValue": 0.0, + "ExposureTime": 5883414, + "AwbMode": 0, + "AfSpeed": 0, + "AfRange": 0, + "AeMeteringMode": 0, + "AeFlickerPeriod": 1000000, + "AeFlickerMode": 0, + "AeExposureMode": 0, + "AeEnable": true, + "AwbEnable": true, + "AeConstraintMode": 0, + "Brightness": 0.0, + "Contrast": 1.0, + "Saturation": 1.0, + "Sharpness": 1.0, + "AfMode": 1, + "ScalerCrop": [ + 0, + 0, + 3280, + 2464 + ] + }, + "rotation": { + "hflip": 0, + "vflip": 0 + }, + "sensor-mode": 2, + "capture-settings": { + "Resize": false, + "makeRaw": false, + "Resolution": 0, + "available-resolutions": [ + [ + 4608, + 2592 + ], + [ + 2304, + 1296 + ], + [ + 1920, + 1080 + ], + [ + 1280, + 720 + ], + [ + 640, + 360 + ] + ] + } +} \ No newline at end of file diff --git a/templates/about.html b/templates/about.html index 10c8a6b..2a2e698 100644 --- a/templates/about.html +++ b/templates/about.html @@ -15,7 +15,7 @@
-

About Picamera2 WebUI Lite

+

About Picamera2 WebUI

@@ -23,7 +23,7 @@

Lightweight web interface for the Raspber

Built on the Picamera2 library. This project provides a simple user interface to configure camera settings, capture photos, and manage images in a basic gallery.

- Updates on Github + Updates on Github

diff --git a/templates/base.html b/templates/base.html index 84e6bea..c87e9bb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,15 +3,15 @@ - Picamera2 WebUI Lite + Picamera2 WebUI - +