Changed to adaptive camera count
This commit is contained in:
parent
4a46b12c05
commit
d12931641b
@ -5,68 +5,101 @@ import time
|
|||||||
import gc
|
import gc
|
||||||
from flask import Flask, Response, render_template_string
|
from flask import Flask, Response, render_template_string
|
||||||
|
|
||||||
# --- PART 1: ROBUST DETECTION ---
|
# --- PART 1: ADAPTIVE DETECTION ---
|
||||||
def detect_camera_config_isolated():
|
def scan_connected_cameras():
|
||||||
# Runs in a separate process to prevent driver locking
|
"""
|
||||||
|
Returns a list of serials ['400...', '400...'] and their config.
|
||||||
|
"""
|
||||||
|
print("--- Scanning for Basler Cameras ---")
|
||||||
detection_script = """
|
detection_script = """
|
||||||
import sys
|
import sys
|
||||||
try:
|
try:
|
||||||
from pypylon import pylon
|
from pypylon import pylon
|
||||||
tl_factory = pylon.TlFactory.GetInstance()
|
tl_factory = pylon.TlFactory.GetInstance()
|
||||||
devices = tl_factory.EnumerateDevices()
|
devices = tl_factory.EnumerateDevices()
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
print("0,0,0")
|
print("NONE")
|
||||||
else:
|
else:
|
||||||
|
# Collect all serials
|
||||||
|
serials = [d.GetSerialNumber() for d in devices]
|
||||||
|
|
||||||
|
# Open the first one just to check capabilities/resolution
|
||||||
cam = pylon.InstantCamera(tl_factory.CreateDevice(devices[0]))
|
cam = pylon.InstantCamera(tl_factory.CreateDevice(devices[0]))
|
||||||
cam.Open()
|
cam.Open()
|
||||||
|
|
||||||
|
# Check Binning support
|
||||||
|
supported = 0
|
||||||
try:
|
try:
|
||||||
# Check if Binning is supported
|
|
||||||
cam.BinningHorizontal.Value = 2
|
cam.BinningHorizontal.Value = 2
|
||||||
cam.BinningVertical.Value = 2
|
cam.BinningVertical.Value = 2
|
||||||
w = cam.Width.GetValue()
|
|
||||||
h = cam.Height.GetValue()
|
|
||||||
cam.BinningHorizontal.Value = 1
|
cam.BinningHorizontal.Value = 1
|
||||||
cam.BinningVertical.Value = 1
|
cam.BinningVertical.Value = 1
|
||||||
print(f"{w},{h},1")
|
supported = 1
|
||||||
except:
|
except:
|
||||||
print(f"{cam.Width.GetValue()},{cam.Height.GetValue()},0")
|
pass
|
||||||
|
|
||||||
|
w = cam.Width.GetValue()
|
||||||
|
h = cam.Height.GetValue()
|
||||||
cam.Close()
|
cam.Close()
|
||||||
except Exception:
|
|
||||||
print("0,0,0")
|
# Output format: SERIAL1,SERIAL2|WIDTH|HEIGHT|BINNING_SUPPORTED
|
||||||
|
print(f"{','.join(serials)}|{w}|{h}|{supported}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR:{e}")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run([sys.executable, "-c", detection_script], capture_output=True, text=True)
|
result = subprocess.run([sys.executable, "-c", detection_script], capture_output=True, text=True)
|
||||||
parts = result.stdout.strip().split(',')
|
output = result.stdout.strip()
|
||||||
w, h, supported = int(parts[0]), int(parts[1]), int(parts[2])
|
|
||||||
if w == 0: return 1920, 1080, False
|
|
||||||
return w, h, (supported == 1)
|
|
||||||
except: return 1920, 1080, False
|
|
||||||
|
|
||||||
CAM_W, CAM_H, BINNING_SUPPORTED = detect_camera_config_isolated()
|
if "NONE" in output or "ERROR" in output or not output:
|
||||||
|
print("No cameras detected!")
|
||||||
|
return [], 1920, 1080, False
|
||||||
|
|
||||||
# --- STABILITY CONFIGURATION ---
|
# Parse output
|
||||||
# We limit the internal processing resolution to 1280x960 (or 720p).
|
parts = output.split('|')
|
||||||
# This prevents the "Failed in mem copy" error by keeping buffers small.
|
serials_list = parts[0].split(',')
|
||||||
|
w = int(parts[1])
|
||||||
|
h = int(parts[2])
|
||||||
|
binning = (parts[3] == '1')
|
||||||
|
|
||||||
|
print(f"Found {len(serials_list)} cameras: {serials_list}")
|
||||||
|
return serials_list, w, h, binning
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Scanner failed: {e}")
|
||||||
|
return [], 1920, 1080, False
|
||||||
|
|
||||||
|
# Run Scan
|
||||||
|
DETECTED_SERIALS, CAM_W, CAM_H, BINNING_SUPPORTED = scan_connected_cameras()
|
||||||
|
NUM_CAMS = len(DETECTED_SERIALS)
|
||||||
|
|
||||||
|
# --- DYNAMIC RESOLUTION ---
|
||||||
INTERNAL_WIDTH = 1280
|
INTERNAL_WIDTH = 1280
|
||||||
scale = INTERNAL_WIDTH / CAM_W
|
scale = INTERNAL_WIDTH / CAM_W
|
||||||
INTERNAL_HEIGHT = int(CAM_H * scale)
|
INTERNAL_HEIGHT = int(CAM_H * scale)
|
||||||
if INTERNAL_HEIGHT % 2 != 0: INTERNAL_HEIGHT += 1
|
if INTERNAL_HEIGHT % 2 != 0: INTERNAL_HEIGHT += 1
|
||||||
|
|
||||||
TILED_WIDTH = 1280
|
# Web Tiling Logic
|
||||||
scale_tiled = TILED_WIDTH / (INTERNAL_WIDTH * 2)
|
WEB_WIDTH = 1280
|
||||||
TILED_HEIGHT = int(INTERNAL_HEIGHT * scale_tiled)
|
if NUM_CAMS > 0:
|
||||||
if TILED_HEIGHT % 2 != 0: TILED_HEIGHT += 1
|
# If 1 camera: Output is 1280x960
|
||||||
|
# If 2 cameras: Output is 1280x(Height scaled for 2-wide)
|
||||||
|
total_source_width = INTERNAL_WIDTH * NUM_CAMS
|
||||||
|
scale_tiled = WEB_WIDTH / total_source_width
|
||||||
|
WEB_HEIGHT = int(INTERNAL_HEIGHT * scale_tiled)
|
||||||
|
if WEB_HEIGHT % 2 != 0: WEB_HEIGHT += 1
|
||||||
|
else:
|
||||||
|
WEB_HEIGHT = 720 # Fallback
|
||||||
|
|
||||||
print(f"STABILITY MODE: Input {CAM_W}x{CAM_H} -> Pre-Scale {INTERNAL_WIDTH}x{INTERNAL_HEIGHT}")
|
print(f"ADAPTIVE MODE: Found {NUM_CAMS} Cams -> Layout {NUM_CAMS}x1 -> Web {WEB_WIDTH}x{WEB_HEIGHT}")
|
||||||
|
|
||||||
# --- FLASK & GSTREAMER ---
|
# --- FLASK & GSTREAMER ---
|
||||||
import gi
|
import gi
|
||||||
gi.require_version('Gst', '1.0')
|
gi.require_version('Gst', '1.0')
|
||||||
from gi.repository import Gst, GLib
|
from gi.repository import Gst, GLib
|
||||||
|
|
||||||
CAMERA_1_SERIAL = "40650847"
|
|
||||||
CAMERA_2_SERIAL = "40653314"
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
frame_buffer = None
|
frame_buffer = None
|
||||||
buffer_lock = threading.Lock()
|
buffer_lock = threading.Lock()
|
||||||
@ -104,7 +137,20 @@ class GStreamerPipeline(threading.Thread):
|
|||||||
return Gst.FlowReturn.OK
|
return Gst.FlowReturn.OK
|
||||||
|
|
||||||
def build_pipeline(self):
|
def build_pipeline(self):
|
||||||
# Settings optimized for USB3 stability
|
# Handle 0 Cameras gracefully (Placeholder)
|
||||||
|
if NUM_CAMS == 0:
|
||||||
|
print("Launching Placeholder Pipeline (No Cameras)...")
|
||||||
|
# Uses 'videotestsrc' to generate a test pattern so the web UI works
|
||||||
|
pipeline_str = (
|
||||||
|
f"videotestsrc pattern=smpte ! video/x-raw,width={WEB_WIDTH},height={WEB_HEIGHT},framerate=30/1 ! "
|
||||||
|
"jpegenc ! appsink name=sink emit-signals=True sync=False max-buffers=1 drop=True"
|
||||||
|
)
|
||||||
|
self.pipeline = Gst.parse_launch(pipeline_str)
|
||||||
|
appsink = self.pipeline.get_by_name("sink")
|
||||||
|
appsink.connect("new-sample", self.on_new_sample)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Settings
|
||||||
settings = (
|
settings = (
|
||||||
"cam::TriggerMode=Off "
|
"cam::TriggerMode=Off "
|
||||||
"cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=60.0 "
|
"cam::AcquisitionFrameRateEnable=true cam::AcquisitionFrameRate=60.0 "
|
||||||
@ -116,49 +162,41 @@ class GStreamerPipeline(threading.Thread):
|
|||||||
if BINNING_SUPPORTED:
|
if BINNING_SUPPORTED:
|
||||||
settings += "cam::BinningHorizontal=2 cam::BinningVertical=2 "
|
settings += "cam::BinningHorizontal=2 cam::BinningVertical=2 "
|
||||||
|
|
||||||
# --- PRE-SCALER ---
|
# Pre-scaler (Crucial for stability)
|
||||||
# Converts to NV12 and scales down immediately to save RAM
|
|
||||||
pre_scale = (
|
pre_scale = (
|
||||||
"nvvideoconvert compute-hw=1 ! "
|
"nvvideoconvert compute-hw=1 ! "
|
||||||
"video/x-raw(memory:NVMM), format=NV12, "
|
"video/x-raw(memory:NVMM), format=NV12, "
|
||||||
f"width={INTERNAL_WIDTH}, height={INTERNAL_HEIGHT} ! "
|
f"width={INTERNAL_WIDTH}, height={INTERNAL_HEIGHT} ! "
|
||||||
)
|
)
|
||||||
|
|
||||||
src1 = (
|
# 1. GENERATE SOURCES DYNAMICALLY
|
||||||
f"pylonsrc device-serial-number={CAMERA_1_SERIAL} {settings} ! "
|
sources_str = ""
|
||||||
|
for i, serial in enumerate(DETECTED_SERIALS):
|
||||||
|
sources_str += (
|
||||||
|
f"pylonsrc device-serial-number={serial} {settings} ! "
|
||||||
"video/x-raw,format=GRAY8 ! "
|
"video/x-raw,format=GRAY8 ! "
|
||||||
"videoconvert ! "
|
"videoconvert ! "
|
||||||
"video/x-raw,format=I420 ! "
|
"video/x-raw,format=I420 ! "
|
||||||
"nvvideoconvert compute-hw=1 ! "
|
"nvvideoconvert compute-hw=1 ! "
|
||||||
"video/x-raw(memory:NVMM) ! "
|
"video/x-raw(memory:NVMM) ! "
|
||||||
f"{pre_scale}"
|
f"{pre_scale}"
|
||||||
"m.sink_0 "
|
f"m.sink_{i} " # Link to the correct pad (0, 1, 2...)
|
||||||
)
|
)
|
||||||
|
|
||||||
src2 = (
|
# 2. CONFIGURE MUXER & TILER
|
||||||
f"pylonsrc device-serial-number={CAMERA_2_SERIAL} {settings} ! "
|
# Batch size MUST match number of cameras
|
||||||
"video/x-raw,format=GRAY8 ! "
|
|
||||||
"videoconvert ! "
|
|
||||||
"video/x-raw,format=I420 ! "
|
|
||||||
"nvvideoconvert compute-hw=1 ! "
|
|
||||||
"video/x-raw(memory:NVMM) ! "
|
|
||||||
f"{pre_scale}"
|
|
||||||
"m.sink_1 "
|
|
||||||
)
|
|
||||||
|
|
||||||
# Muxer -> Tiler -> MJPEG Encode
|
|
||||||
processing = (
|
processing = (
|
||||||
f"nvstreammux name=m batch-size=2 width={INTERNAL_WIDTH} height={INTERNAL_HEIGHT} live-source=1 ! "
|
f"nvstreammux name=m batch-size={NUM_CAMS} width={INTERNAL_WIDTH} height={INTERNAL_HEIGHT} live-source=1 ! "
|
||||||
f"nvmultistreamtiler width={TILED_WIDTH} height={TILED_HEIGHT} rows=1 columns=2 ! "
|
f"nvmultistreamtiler width={WEB_WIDTH} height={WEB_HEIGHT} rows=1 columns={NUM_CAMS} ! "
|
||||||
"nvvideoconvert compute-hw=1 ! "
|
"nvvideoconvert compute-hw=1 ! "
|
||||||
"video/x-raw(memory:NVMM) ! "
|
"video/x-raw(memory:NVMM) ! "
|
||||||
f"nvjpegenc quality=60 ! "
|
f"nvjpegenc quality=60 ! "
|
||||||
"appsink name=sink emit-signals=True sync=False max-buffers=1 drop=True"
|
"appsink name=sink emit-signals=True sync=False max-buffers=1 drop=True"
|
||||||
)
|
)
|
||||||
|
|
||||||
pipeline_str = f"{src1} {src2} {processing}"
|
pipeline_str = f"{sources_str} {processing}"
|
||||||
|
|
||||||
print(f"Launching Pipeline...")
|
print(f"Launching ADAPTIVE Pipeline ({NUM_CAMS} Cameras)...")
|
||||||
self.pipeline = Gst.parse_launch(pipeline_str)
|
self.pipeline = Gst.parse_launch(pipeline_str)
|
||||||
|
|
||||||
appsink = self.pipeline.get_by_name("sink")
|
appsink = self.pipeline.get_by_name("sink")
|
||||||
@ -169,15 +207,17 @@ class GStreamerPipeline(threading.Thread):
|
|||||||
def index():
|
def index():
|
||||||
return render_template_string('''
|
return render_template_string('''
|
||||||
<html><body style="background:#111; color:white; text-align:center;">
|
<html><body style="background:#111; color:white; text-align:center;">
|
||||||
<h1>Basler Stable Feed</h1>
|
<h1>Basler Feed ({{ num }} Cameras)</h1>
|
||||||
|
{% if num == 0 %}
|
||||||
|
<h2 style="color:red">NO CAMERAS DETECTED</h2>
|
||||||
|
{% endif %}
|
||||||
<img src="{{ url_for('video_feed') }}" style="border: 2px solid #4CAF50; width:95%;">
|
<img src="{{ url_for('video_feed') }}" style="border: 2px solid #4CAF50; width:95%;">
|
||||||
</body></html>
|
</body></html>
|
||||||
''')
|
''', num=NUM_CAMS)
|
||||||
|
|
||||||
@app.route('/video_feed')
|
@app.route('/video_feed')
|
||||||
def video_feed():
|
def video_feed():
|
||||||
def generate():
|
def generate():
|
||||||
# FIX: Local counter variable initialized here
|
|
||||||
count = 0
|
count = 0
|
||||||
while True:
|
while True:
|
||||||
with buffer_lock:
|
with buffer_lock:
|
||||||
@ -185,18 +225,13 @@ def video_feed():
|
|||||||
yield (b'--frame\r\n'
|
yield (b'--frame\r\n'
|
||||||
b'Content-Type: image/jpeg\r\n\r\n' + frame_buffer + b'\r\n')
|
b'Content-Type: image/jpeg\r\n\r\n' + frame_buffer + b'\r\n')
|
||||||
time.sleep(0.016)
|
time.sleep(0.016)
|
||||||
|
|
||||||
# FIX: Increment and check local counter
|
|
||||||
count += 1
|
count += 1
|
||||||
if count % 200 == 0:
|
if count % 200 == 0: gc.collect()
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Initial cleanup
|
|
||||||
subprocess.run([sys.executable, "-c", "import gc; gc.collect()"])
|
subprocess.run([sys.executable, "-c", "import gc; gc.collect()"])
|
||||||
|
|
||||||
gst_thread = GStreamerPipeline()
|
gst_thread = GStreamerPipeline()
|
||||||
gst_thread.daemon = True
|
gst_thread.daemon = True
|
||||||
gst_thread.start()
|
gst_thread.start()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user