From 97c7772a4cc9e622663382adc8d679dd465eae0f Mon Sep 17 00:00:00 2001 From: Tempest Date: Tue, 2 Dec 2025 07:57:20 +0700 Subject: [PATCH] Revert "Changed color camera back to color, set all cameras to 20ms shutter speed" This reverts commit 413590d1a24c5605b6078be6cae1291ba29263ce. --- src/detectionSoftware/run.py | 196 +++++++++++++++-------------------- 1 file changed, 81 insertions(+), 115 deletions(-) diff --git a/src/detectionSoftware/run.py b/src/detectionSoftware/run.py index b491be71..fe5e0e89 100644 --- a/src/detectionSoftware/run.py +++ b/src/detectionSoftware/run.py @@ -11,7 +11,7 @@ TARGET_NUM_CAMS = 3 DEFAULT_W = 1280 DEFAULT_H = 720 -# --- PART 1: DETECTION (Unchanged) --- +# --- PART 1: DETECTION --- def scan_connected_cameras(): print("--- Scanning for Basler Cameras ---") detection_script = """ @@ -23,58 +23,45 @@ try: if not devices: print("NONE") else: - results = [] - for i in range(len(devices)): - cam = pylon.InstantCamera(tl_factory.CreateDevice(devices[i])) - cam.Open() - serial = cam.GetDeviceInfo().GetSerialNumber() - model = cam.GetDeviceInfo().GetModelName() - is_color = model.endswith("c") or "Color" in model + serials = [d.GetSerialNumber() for d in devices] + cam = pylon.InstantCamera(tl_factory.CreateDevice(devices[0])) + cam.Open() + try: + cam.BinningHorizontal.Value = 2 + cam.BinningVertical.Value = 2 w = cam.Width.GetValue() h = cam.Height.GetValue() - binning = 0 - try: - cam.BinningHorizontal.Value = 2 - cam.BinningVertical.Value = 2 - cam.BinningHorizontal.Value = 1 - cam.BinningVertical.Value = 1 - binning = 1 - except: pass - current_fmt = cam.PixelFormat.GetValue() - cam.Close() - results.append(f"{serial}:{w}:{h}:{binning}:{1 if is_color else 0}:{model}:{current_fmt}") - print("|".join(results)) -except Exception: print("NONE") + cam.BinningHorizontal.Value = 1 + cam.BinningVertical.Value = 1 + supported = 1 + except: + w = cam.Width.GetValue() + h = cam.Height.GetValue() + supported = 0 + cam.Close() + print(f"{','.join(serials)}|{w}|{h}|{supported}") +except Exception: + print("NONE") """ try: result = subprocess.run([sys.executable, "-c", detection_script], capture_output=True, text=True) output = result.stdout.strip() - if "NONE" in output or not output: return [] - camera_list = [] - entries = output.split('|') - for entry in entries: - parts = entry.split(':') - camera_list.append({ - "serial": parts[0], "width": int(parts[1]), "height": int(parts[2]), - "binning": (parts[3] == '1'), "is_color": (parts[4] == '1'), "model": parts[5] - }) - return camera_list - except: return [] + if "NONE" in output or not output: + return [], DEFAULT_W, DEFAULT_H, False + parts = output.split('|') + return parts[0].split(','), int(parts[1]), int(parts[2]), (parts[3] == '1') + except: return [], DEFAULT_W, DEFAULT_H, False -DETECTED_CAMS = scan_connected_cameras() -ACTUAL_CAMS_COUNT = len(DETECTED_CAMS) - -# --- RESOLUTION LOGIC --- -if ACTUAL_CAMS_COUNT > 0: - MASTER_W = DETECTED_CAMS[0]['width'] - MASTER_H = DETECTED_CAMS[0]['height'] -else: - MASTER_W = DEFAULT_W - MASTER_H = DEFAULT_H +DETECTED_SERIALS, CAM_W, CAM_H, BINNING_SUPPORTED = scan_connected_cameras() +ACTUAL_CAMS_COUNT = len(DETECTED_SERIALS) +# --- RESOLUTION & LAYOUT --- INTERNAL_WIDTH = 1280 -scale = INTERNAL_WIDTH / MASTER_W -INTERNAL_HEIGHT = int(MASTER_H * scale) +if ACTUAL_CAMS_COUNT > 0: + scale = INTERNAL_WIDTH / CAM_W + INTERNAL_HEIGHT = int(CAM_H * scale) +else: + INTERNAL_HEIGHT = 720 if INTERNAL_HEIGHT % 2 != 0: INTERNAL_HEIGHT += 1 WEB_WIDTH = 1280 @@ -83,9 +70,7 @@ scale_tiled = WEB_WIDTH / total_source_width WEB_HEIGHT = int(INTERNAL_HEIGHT * scale_tiled) if WEB_HEIGHT % 2 != 0: WEB_HEIGHT += 1 -print(f"LAYOUT: {TARGET_NUM_CAMS} Slots | Detected: {ACTUAL_CAMS_COUNT}") -for c in DETECTED_CAMS: - print(f" - Cam {c['serial']} ({c['model']}): {'COLOR' if c['is_color'] else 'MONO'}") +print(f"LAYOUT: {TARGET_NUM_CAMS} Slots | Detected: {ACTUAL_CAMS_COUNT} Cams") # --- FLASK & GSTREAMER --- import gi @@ -122,6 +107,7 @@ class GStreamerPipeline(threading.Thread): if not sample: return Gst.FlowReturn.ERROR frame_count += 1 + # Calculate FPS every 30 frames if frame_count % 30 == 0: elapsed = time.time() - start_time current_fps = 30 / elapsed if elapsed > 0 else 0 @@ -139,80 +125,52 @@ class GStreamerPipeline(threading.Thread): return Gst.FlowReturn.OK def build_pipeline(self): + # 1. CAMERA SETTINGS + # Note: We run cameras at 60 FPS for internal stability + cam_settings = ( + "cam::TriggerMode=Off " + "cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=60.0 " + "cam::ExposureAuto=Off " + "cam::ExposureTime=20000.0 " + "cam::GainAuto=Continuous " + "cam::DeviceLinkThroughputLimitMode=Off " + ) + if BINNING_SUPPORTED: + cam_settings += "cam::BinningHorizontal=2 cam::BinningVertical=2 " + sources_str = "" for i in range(TARGET_NUM_CAMS): - if i < len(DETECTED_CAMS): - cam_info = DETECTED_CAMS[i] - serial = cam_info['serial'] - is_color = cam_info['is_color'] + if i < len(DETECTED_SERIALS): + # --- REAL CAMERA SOURCE --- + serial = DETECTED_SERIALS[i] + print(f"Slot {i}: Linking Camera {serial}") - print(f"Slot {i}: Linking {serial} [{'Color' if is_color else 'Mono'}]") - - # --- 1. BASE SETTINGS (Common) --- - # We DISABLE Throughput Limit to allow high bandwidth - base_settings = ( - f"pylonsrc device-serial-number={serial} " - "cam::TriggerMode=Off " - "cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=60.0 " - "cam::DeviceLinkThroughputLimitMode=Off " - ) - - # Pre-scaler pre_scale = ( "nvvideoconvert compute-hw=1 ! " f"video/x-raw(memory:NVMM), format=NV12, width={INTERNAL_WIDTH}, height={INTERNAL_HEIGHT}, framerate=60/1 ! " ) - - if is_color: - # --- 2A. COLOR SETTINGS (High Speed) --- - # FIX: Force ExposureTime=20000.0 (20ms) even for Color. - # If we leave it on Auto, it will slow down the Mono cameras. - # We rely on 'GainAuto' to make the image bright enough. - color_settings = ( - f"{base_settings} " - "cam::ExposureAuto=Off cam::ExposureTime=20000.0 " - "cam::GainAuto=Continuous " - "cam::Width=1920 cam::Height=1080 cam::OffsetX=336 cam::OffsetY=484 " - "cam::PixelFormat=BayerBG8 " # Force Format - ) - - source = ( - f"{color_settings} ! " - "bayer2rgb ! " # Debayer - "videoconvert ! " - "video/x-raw,format=RGBA ! " - "nvvideoconvert compute-hw=1 ! " - f"video/x-raw(memory:NVMM), format=NV12 ! " - f"{pre_scale}" - f"m.sink_{i} " - ) - else: - # --- 2B. MONO SETTINGS (High Speed) --- - # Force ExposureTime=20000.0 - mono_settings = ( - f"{base_settings} " - "cam::ExposureAuto=Off cam::ExposureTime=20000.0 " - "cam::GainAuto=Continuous " - ) - if cam_info['binning']: - mono_settings += "cam::BinningHorizontal=2 cam::BinningVertical=2 " - - source = ( - f"{mono_settings} ! " - "video/x-raw,format=GRAY8 ! " - "videoconvert ! " - "video/x-raw,format=I420 ! " - "nvvideoconvert compute-hw=1 ! " - f"video/x-raw(memory:NVMM), format=NV12 ! " - f"{pre_scale}" - f"m.sink_{i} " - ) + + source = ( + f"pylonsrc device-serial-number={serial} {cam_settings} ! " + "video/x-raw,format=GRAY8 ! " + "videoconvert ! " + "video/x-raw,format=I420 ! " + "nvvideoconvert compute-hw=1 ! " + "video/x-raw(memory:NVMM) ! " + f"{pre_scale}" + f"m.sink_{i} " + ) else: # --- DISCONNECTED PLACEHOLDER --- + print(f"Slot {i}: Creating Placeholder (Synchronized)") + + # FIX 1: Add 'videorate' to enforce strict timing on the fake source + # This prevents the placeholder from running too fast/slow and jittering the muxer + source = ( f"videotestsrc pattern=black is-live=true ! " - f"videorate ! " + f"videorate ! " # <--- TIMING ENFORCER f"video/x-raw,width={INTERNAL_WIDTH},height={INTERNAL_HEIGHT},format=I420,framerate=60/1 ! " f"textoverlay text=\"DISCONNECTED\" valignment=center halignment=center font-desc=\"Sans, 48\" ! " "nvvideoconvert compute-hw=1 ! " @@ -223,21 +181,28 @@ class GStreamerPipeline(threading.Thread): sources_str += source # 3. MUXER & PROCESSING + # FIX 2: batched-push-timeout=33000 + # This tells the muxer: "If you have data, send it every 33ms (30fps). Don't wait forever." + + # FIX 3: Output Videorate + # We process internally at 60fps (best for camera driver), but we DROP to 30fps + # for the web stream. This makes the network stream buttery smooth and consistent. + processing = ( f"nvstreammux name=m batch-size={TARGET_NUM_CAMS} width={INTERNAL_WIDTH} height={INTERNAL_HEIGHT} " - f"live-source=1 batched-push-timeout=33000 ! " + f"live-source=1 batched-push-timeout=33000 ! " # <--- TIMEOUT FIX f"nvmultistreamtiler width={WEB_WIDTH} height={WEB_HEIGHT} rows=1 columns={TARGET_NUM_CAMS} ! " "nvvideoconvert compute-hw=1 ! " "video/x-raw(memory:NVMM) ! " - "videorate drop-only=true ! " - "video/x-raw(memory:NVMM), framerate=30/1 ! " + "videorate drop-only=true ! " # <--- DROPPING FRAMES CLEANLY + "video/x-raw(memory:NVMM), framerate=30/1 ! " # <--- Force 30 FPS Output f"nvjpegenc quality=60 ! " "appsink name=sink emit-signals=True sync=False max-buffers=1 drop=True" ) pipeline_str = f"{sources_str} {processing}" - print(f"Launching Optimized Pipeline (All Cams Forced to 20ms Shutter)...") + print(f"Launching SMOOTH Pipeline...") self.pipeline = Gst.parse_launch(pipeline_str) appsink = self.pipeline.get_by_name("sink") appsink.connect("new-sample", self.on_new_sample) @@ -260,7 +225,7 @@ def index(): -

Basler Final Feed

+

Basler 3-Cam (Smooth)

FPS: --
@@ -284,7 +249,8 @@ def video_feed(): with buffer_lock: if frame_buffer: yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_buffer + b'\r\n') - time.sleep(0.016) + # Sleep 33ms (30 FPS) + time.sleep(0.033) count += 1 if count % 200 == 0: gc.collect() return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')