diff --git a/src/detectionSoftware/run.py b/src/detectionSoftware/run.py index 626413bb..9909eabd 100644 --- a/src/detectionSoftware/run.py +++ b/src/detectionSoftware/run.py @@ -1,85 +1,51 @@ import sys import threading -import math import gi import logging from flask import Flask, Response, render_template_string -from pypylon import pylon # GStreamer dependencies gi.require_version('Gst', '1.0') from gi.repository import Gst, GLib # --- Configuration --- +CAMERA_1_SERIAL = "40650847" +CAMERA_2_SERIAL = "40653314" + STREAM_WIDTH = 1920 STREAM_HEIGHT = 1080 -# The final output resolution of the tiled web stream -WEB_OUTPUT_WIDTH = 1920 -WEB_OUTPUT_HEIGHT = 1080 +TILED_WIDTH = 1920 +TILED_HEIGHT = 1080 -# --- Flask Setup --- app = Flask(__name__) frame_buffer = None buffer_lock = threading.Lock() -def discover_cameras(): - """ - Uses pypylon to find all connected Basler cameras. - Returns a list of Serial Numbers. - """ - tl_factory = pylon.TlFactory.GetInstance() - devices = tl_factory.EnumerateDevices() - - serials = [] - for dev in devices: - serials.append(dev.GetSerialNumber()) - - if not serials: - print("CRITICAL ERROR: No Basler cameras detected via Pylon.") - sys.exit(1) - - print(f"Discovered {len(serials)} cameras: {serials}") - return serials - class GStreamerPipeline(threading.Thread): - def __init__(self, camera_serials): + def __init__(self): super().__init__() - self.camera_serials = camera_serials self.loop = GLib.MainLoop() self.pipeline = None - def calculate_grid(self, num_cams): - """ - Calculates the most square-like grid (rows, cols) for N cameras. - """ - rows = int(math.ceil(math.sqrt(num_cams))) - cols = int(math.ceil(num_cams / rows)) - return rows, cols - def run(self): Gst.init(None) - self.build_dynamic_pipeline() + self.build_pipeline() self.pipeline.set_state(Gst.State.PLAYING) try: self.loop.run() except Exception as e: - print(f"Error in GStreamer loop: {e}") + print(f"Error: {e}") finally: self.pipeline.set_state(Gst.State.NULL) def on_new_sample(self, sink): - """ - Callback: grabs the already-encoded JPEG from the pipeline. - """ sample = sink.emit("pull-sample") - if not sample: - return Gst.FlowReturn.ERROR + if not sample: return Gst.FlowReturn.ERROR buffer = sample.get_buffer() success, map_info = buffer.map(Gst.MapFlags.READ) - if not success: - return Gst.FlowReturn.ERROR + if not success: return Gst.FlowReturn.ERROR global frame_buffer with buffer_lock: @@ -88,65 +54,56 @@ class GStreamerPipeline(threading.Thread): buffer.unmap(map_info) return Gst.FlowReturn.OK - def build_dynamic_pipeline(self): - num_cams = len(self.camera_serials) - rows, cols = self.calculate_grid(num_cams) - - print(f"Building pipeline for {num_cams} cameras (Grid: {cols}x{rows})") + def build_pipeline(self): + # FIX: Added 'compute-hw=1' to nvvideoconvert. + # This forces the conversion to happen on the GPU (CUDA) instead of the VIC, + # which fixes the "RGB/BGR not supported" error. - # 1. Construct Sources - # We need to build N pylonsrc elements, each linking to a specific pad on the muxer. - sources_str = "" - for i, serial in enumerate(self.camera_serials): - # We explicitly link to muxer sink pad: m.sink_0, m.sink_1, etc. - sources_str += ( - f"pylonsrc camera-device-serial-number={serial} ! " - f"videoconvert ! nvvideoconvert ! m.sink_{i} " - ) - - # 2. Configure Muxer - # batch-size must match number of cameras - muxer_str = ( - f"nvstreammux name=m batch-size={num_cams} " - f"width={STREAM_WIDTH} height={STREAM_HEIGHT} live-source=1 " + # Source 1 + src1 = ( + f"pylonsrc device-serial-number={CAMERA_1_SERIAL} " + "cam::TriggerMode=Off cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=30.0 ! " + "videoconvert ! " + "nvvideoconvert compute-hw=1 ! " + "m.sink_0 " ) - # 3. Configure Tiler - # This combines the batch into one 2D image - tiler_str = ( - f"nvmultistreamtiler width={WEB_OUTPUT_WIDTH} height={WEB_OUTPUT_HEIGHT} " - f"rows={rows} columns={cols} " + # Source 2 + src2 = ( + f"pylonsrc device-serial-number={CAMERA_2_SERIAL} " + "cam::TriggerMode=Off cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=30.0 ! " + "videoconvert ! " + "nvvideoconvert compute-hw=1 ! " + "m.sink_1 " ) - # 4. Final Processing (Convert -> JPEG -> AppSink) - output_str = ( - "nvvideoconvert ! video/x-raw, format=I420 ! " + # Muxer -> Tiler -> Output + processing = ( + f"nvstreammux name=m batch-size=2 width={STREAM_WIDTH} height={STREAM_HEIGHT} live-source=1 ! " + f"nvmultistreamtiler width={TILED_WIDTH} height={TILED_HEIGHT} rows=2 columns=1 ! " + "nvvideoconvert ! " + "video/x-raw, format=I420 ! " "jpegenc quality=85 ! " "appsink name=sink emit-signals=True sync=False max-buffers=1 drop=True" ) - # Combine all parts - full_pipeline_str = f"{sources_str} {muxer_str} ! {tiler_str} ! {output_str}" + pipeline_str = f"{src1} {src2} {processing}" - print(f"Pipeline String:\n{full_pipeline_str}") + print(f"Launching Pipeline (GPU Mode)...") + self.pipeline = Gst.parse_launch(pipeline_str) - self.pipeline = Gst.parse_launch(full_pipeline_str) - - # Link callback appsink = self.pipeline.get_by_name("sink") appsink.connect("new-sample", self.on_new_sample) -# --- Flask Routes --- +# --- Flask --- @app.route('/') def index(): return render_template_string(''' - - -

Basler Auto-Discovery Feed

- - - + +

Basler Feed

+ + ''') @app.route('/video_feed') @@ -157,21 +114,12 @@ def video_feed(): if frame_buffer: yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_buffer + b'\r\n') - GLib.usleep(15000) # ~60fps poll cap + GLib.usleep(33000) return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame') -# --- Main --- - if __name__ == "__main__": - # 1. Discover Cameras - found_serials = discover_cameras() - - # 2. Start Pipeline Thread - gst_thread = GStreamerPipeline(found_serials) + gst_thread = GStreamerPipeline() gst_thread.daemon = True gst_thread.start() - - # 3. Start Web Server - print(f"Stream available at http://0.0.0.0:5000") app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)