agrobot_base/Python/raspi/sensor_calibration_tool.py

829 lines
32 KiB
Python
Raw Normal View History

import os
import json
import time
import argparse
from datetime import datetime
import cv2
import numpy as np
from cam_3.multispectral_service import MultiSpectralService
from cam_3.stream_receiver import StreamReceiver
from cam_3.pi.raw_processor_core import RawProcessorCore
STREAM_PORT = 6001
PI_HOST = "192.168.105.6"
PC_HOST = "192.168.105.5"
# ============================================================
# Helpers
# ============================================================
def now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def ensure_dir(path: str):
os.makedirs(path, exist_ok=True)
def overlay_hud(
img_bgr: np.ndarray,
lines: list[str],
x: int = 12,
y: int = 22,
font_scale: float = 0.55,
line_step: int = 22,
):
yy = y
for s in lines:
cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), 1, cv2.LINE_AA)
yy += line_step
def to_bgr_u8_from_rgb01(rgb01: np.ndarray) -> np.ndarray:
rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
def gray_to_bgr_u8(gray01: np.ndarray) -> np.ndarray:
g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)
def resize_if_needed(img: np.ndarray, target_hw: tuple[int, int]) -> np.ndarray:
target_h, target_w = target_hw
if img.shape[:2] == (target_h, target_w):
return img
return cv2.resize(img, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
def validate_module_ready(status: dict, frame_type: str, raw_policy: str, capture_mode: str):
if not status.get("ok", True):
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
active_ids = list(status.get("active_camera_ids", []))
active_count = int(status.get("camera_count_active", 0))
if frame_type == "RAW_BRUTO":
if raw_policy == "require_triple":
missing = [cid for cid in ("cam0", "cam1", "cam2") if cid not in active_ids]
if missing:
raise RuntimeError(
f"RAW_BRUTO com política require_triple exige três câmeras ativas. "
f"Faltando: {missing}. Ativas atuais: {active_ids}"
)
else:
if active_count < 1:
raise RuntimeError("RAW_BRUTO requer ao menos uma câmera ativa, mas nenhuma foi detectada.")
return
raise RuntimeError(f"frame_type desconhecido para validação: {frame_type}")
def resolve_effective_capture_mode(requested_mode: str) -> str:
if requested_mode in ("SINGLE", "DOUBLE", "TRIPLE"):
return requested_mode
return "AUTO"
def build_empty_panel(shape_hw: tuple[int, int], title: str) -> np.ndarray:
h, w = shape_hw
img = np.zeros((h, w, 3), dtype=np.uint8)
overlay_hud(img, [title, "sem frame disponivel"], x=18, y=40, font_scale=0.8, line_step=34)
return img
def color_for_index(idx: int) -> tuple[int, int, int]:
palette = [
(0, 255, 255),
(0, 255, 0),
(255, 255, 0),
(255, 0, 255),
(255, 128, 0),
(128, 255, 0),
(0, 128, 255),
(200, 200, 255),
]
return palette[idx % len(palette)]
def compute_stats_from_roi(img01: np.ndarray, rect: tuple[int, int, int, int]) -> dict:
x0, y0, x1, y1 = rect
x0, x1 = sorted((int(x0), int(x1)))
y0, y1 = sorted((int(y0), int(y1)))
roi = img01[y0:y1, x0:x1]
if roi.size == 0:
return {
"valid": False,
"mean": 0.0,
"std": 0.0,
"min": 0.0,
"max": 0.0,
"p05": 0.0,
"p95": 0.0,
"pct_saturated": 0.0,
"pct_dark": 0.0,
"pixels": 0,
}
arr = roi.astype(np.float32).reshape(-1)
return {
"valid": True,
"mean": float(arr.mean()),
"std": float(arr.std()),
"min": float(arr.min()),
"max": float(arr.max()),
"p05": float(np.percentile(arr, 5)),
"p95": float(np.percentile(arr, 95)),
"pct_saturated": float((arr >= 0.98).mean() * 100.0),
"pct_dark": float((arr <= 0.02).mean() * 100.0),
"pixels": int(arr.size),
}
def compute_scene_health(img01: np.ndarray) -> dict:
arr = img01.astype(np.float32).reshape(-1)
mean = float(arr.mean())
std = float(arr.std())
pct_sat = float((arr >= 0.98).mean() * 100.0)
pct_dark = float((arr <= 0.02).mean() * 100.0)
p05 = float(np.percentile(arr, 5))
p95 = float(np.percentile(arr, 95))
comments = []
if pct_sat > 5.0:
comments.append("saturando")
if pct_dark > 40.0:
comments.append("muito escuro")
if std < 0.05:
comments.append("baixo contraste")
if not comments:
comments.append("ok")
return {
"mean": mean,
"std": std,
"p05": p05,
"p95": p95,
"pct_saturated": pct_sat,
"pct_dark": pct_dark,
"comment": ", ".join(comments),
}
def draw_rois(panel_bgr: np.ndarray, rois: list[dict]):
for idx, roi in enumerate(rois):
rect = roi.get("rect")
if rect is None:
continue
x0, y0, x1, y1 = rect
color = roi.get("color", color_for_index(idx))
cv2.rectangle(panel_bgr, (x0, y0), (x1, y1), color, 2)
label = roi.get("name", f"roi_{idx+1}")
cv2.putText(panel_bgr, label, (x0 + 4, max(18, y0 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
# ============================================================
# Decodificação do stream RAW_BRUTO
# ============================================================
class StreamDecoder:
def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"):
self.sensor_width = sensor_width
self.sensor_height = sensor_height
self.bayer_pattern = bayer_pattern
def decode_stream_cameras(self, frame, meta):
if not isinstance(frame, dict):
raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi")
camera_frames = meta.get("camera_frames", {}) or {}
decoded = {}
if "cam2" in frame:
rgb_bgr = frame["cam2"]
if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3:
raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}")
rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0
decoded["cam2"] = {
"name": "RGB",
"image": rgb,
"meta": camera_frames.get("cam2", {}),
}
for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")):
if cam_id not in frame:
continue
packed = frame[cam_id]
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
cam_meta = camera_frames.get(cam_id, {})
packed_width = int(cam_meta.get("width", packed.shape[1]))
height = int(cam_meta.get("height", packed.shape[0]))
bayer = cam_meta.get("bayer_pattern", self.bayer_pattern)
bit_depth = int(cam_meta.get("bit_depth", 10))
real_width = int((packed_width * 8) / 10) if bit_depth == 10 else packed_width
rp = RawProcessorCore(
sensor_width=real_width,
sensor_height=height,
bayer_pattern=bayer,
)
raw16 = rp.unpack_raw10_packed(packed)
max_val = float((1 << bit_depth) - 1)
single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
decoded[cam_id] = {
"name": spec_name,
"image": single,
"meta": cam_meta,
}
return decoded
# ============================================================
# Persistência dos parâmetros/snapshots
# ============================================================
def default_payload(args, effective_capture_mode: str):
return {
"schema": "manual_sensor_calibration_v1",
"saved_at": now_str(),
"pi_host": args.pi_host,
"pc_host": args.pc_host,
"stream_port": args.stream_port,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": args.capture_mode,
"capture_mode_effective": effective_capture_mode,
"raw_policy": args.raw_policy,
"sensor_width": args.width,
"sensor_height": args.height,
"bayer_pattern": args.bayer,
"notes": args.notes or "",
"camera_settings": {
"cam0": {},
"cam1": {},
"cam2": {},
},
"snapshots": [],
}
def load_payload(path: str, args, effective_capture_mode: str):
if not path or not os.path.isfile(path):
return default_payload(args, effective_capture_mode)
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
data.setdefault("schema", "manual_sensor_calibration_v1")
data.setdefault("camera_settings", {"cam0": {}, "cam1": {}, "cam2": {}})
data.setdefault("snapshots", [])
return data
def save_payload(path: str, data: dict):
ensure_dir(os.path.dirname(path) or ".")
data = dict(data)
data["saved_at"] = now_str()
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Ferramenta de calibração dos sensores RGB/RE/NIR com controle manual e ROIs em tempo real.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--pi_host", default=PI_HOST)
parser.add_argument("--pc_host", default=PC_HOST)
parser.add_argument("--stream_port", type=int, default=STREAM_PORT)
parser.add_argument("--server_port", type=int, default=5000)
parser.add_argument("--fps", type=int, default=20)
parser.add_argument("--width", type=int, default=640)
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--bayer", default="GBRG", choices=["GBRG", "GRBG", "RGGB", "BGGR"])
parser.add_argument("--capture_mode", default="AUTO", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"])
parser.add_argument("--raw_policy", default="allow_single", choices=["allow_single", "require_triple"])
parser.add_argument("--preview_scale", type=float, default=1.0)
parser.add_argument("--exp_step", type=int, default=1000, help="Passo de exposição em us")
parser.add_argument("--gain_step", type=float, default=0.10, help="Passo multiplicativo do ganho")
parser.add_argument("--out_json", default="calibration/sensor_calibration.json")
parser.add_argument("--load_json", default="")
parser.add_argument("--notes", default="")
args = parser.parse_args()
effective_capture_mode = resolve_effective_capture_mode(args.capture_mode)
receiver = StreamReceiver(host="0.0.0.0", port=args.stream_port)
svc = MultiSpectralService(host=args.pi_host, port=args.server_port, timeout=10)
decoder = StreamDecoder(sensor_width=args.width, sensor_height=args.height, bayer_pattern=args.bayer)
data_payload = load_payload(args.load_json, args, effective_capture_mode)
selected_cam = "cam2"
last_msg = ""
last_msg_t = 0.0
last_frame_id = -1
fps_view = 0.0
fps_stream = 0.0
t_view_fps = time.time()
t_stream_fps = time.time()
view_frames = 0
stream_frames_accum = 0
last_stream_frame_id = None
decoded_last = {}
window_name = "Sensor Calibration Tool"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
panel_rects = {
"cam2": None,
"cam0": None,
"cam1": None,
"data": None,
}
# Controle de câmera
camera_controls = {
"cam0": {
"ae_enable": False,
"awb_enable": False,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": None,
},
"cam1": {
"ae_enable": False,
"awb_enable": False,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": None,
},
"cam2": {
"ae_enable": True,
"awb_enable": True,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [1.0, 1.0],
},
}
rois = {
"cam2": [],
"cam0": [],
"cam1": [],
}
drawing_roi = False
roi_start = None
roi_current = None
def get_active_rect_for_mouse():
return panel_rects.get(selected_cam)
def on_mouse(event, x, y, flags, param):
nonlocal drawing_roi, roi_start, roi_current, last_msg, last_msg_t
rect = get_active_rect_for_mouse()
if rect is None:
return
x0, y0, x1, y1 = rect
inside = (x0 <= x < x1 and y0 <= y < y1)
if not inside:
return
lx = int(x - x0)
ly = int(y - y0)
if event == cv2.EVENT_LBUTTONDOWN:
drawing_roi = True
roi_start = (lx, ly)
roi_current = (lx, ly)
elif event == cv2.EVENT_MOUSEMOVE and drawing_roi:
roi_current = (lx, ly)
elif event == cv2.EVENT_LBUTTONUP and drawing_roi:
drawing_roi = False
roi_current = (lx, ly)
if roi_start is None:
return
rx0, ry0 = roi_start
rx1, ry1 = roi_current
rx0, rx1 = sorted((rx0, rx1))
ry0, ry1 = sorted((ry0, ry1))
if (rx1 - rx0) < 8 or (ry1 - ry0) < 8:
last_msg = "ROI muito pequena, ignorada"
last_msg_t = time.time()
roi_start = None
roi_current = None
return
name = f"roi_{len(rois[selected_cam]) + 1}"
roi = {
"name": name,
"rect": (rx0, ry0, rx1, ry1),
"color": color_for_index(len(rois[selected_cam])),
}
rois[selected_cam].append(roi)
last_msg = f"ROI criada em {selected_cam}: {name}"
last_msg_t = time.time()
roi_start = None
roi_current = None
cv2.setMouseCallback(window_name, on_mouse)
def apply_controls_to_selected_cam():
nonlocal last_msg, last_msg_t
ctrl = camera_controls[selected_cam]
try:
resp = svc.set_ae_enable(selected_cam, bool(ctrl["ae_enable"]))
ctrl["ae_enable"] = bool(resp.get("ae_enable", ctrl["ae_enable"]))
if selected_cam == "cam2":
resp = svc.set_awb_enable(selected_cam, bool(ctrl["awb_enable"]))
ctrl["awb_enable"] = bool(resp.get("awb_enable", ctrl["awb_enable"]))
if not ctrl["ae_enable"]:
if ctrl["exposure_time_us"] is not None:
resp = svc.set_exposure_time(selected_cam, int(ctrl["exposure_time_us"]))
exp_val = resp.get("exposure_time_us", ctrl["exposure_time_us"])
ctrl["exposure_time_us"] = int(exp_val) if exp_val is not None else None
if ctrl["analogue_gain"] is not None:
resp = svc.set_analogue_gain(selected_cam, float(ctrl["analogue_gain"]))
gain_val = resp.get("analogue_gain", ctrl["analogue_gain"])
ctrl["analogue_gain"] = float(gain_val) if gain_val is not None else None
last_msg = f"Controles aplicados em {selected_cam}"
last_msg_t = time.time()
except Exception as e:
last_msg = f"Falha ao aplicar controles: {e}"
last_msg_t = time.time()
def snapshot_current_state():
active_img = None
if selected_cam in decoded_last:
active_img = decoded_last[selected_cam]["image"]
if active_img is None:
return None
roi_entries = []
for roi in rois[selected_cam]:
stats = compute_stats_from_roi(active_img, roi["rect"])
roi_entries.append({
"name": roi["name"],
"rect": list(map(int, roi["rect"])),
"metrics": stats,
})
snap = {
"timestamp": now_str(),
"camera": selected_cam,
"camera_settings": json.loads(json.dumps(camera_controls[selected_cam])),
"scene_health": compute_scene_health(active_img),
"rois": roi_entries,
}
return snap
try:
receiver.start()
time.sleep(0.5)
print(f"[INFO] Verificando conexão com o módulo em {args.pi_host}:{args.server_port}...")
if not svc.check_connection(2):
raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.")
print("[OK] Módulo conectado e respondendo.")
svc.connect()
print("SET CAM0 RES:", svc.set_camera_resolution(0, args.width, args.height))
print("SET CAM1 RES:", svc.set_camera_resolution(1, args.width, args.height))
print("SET CAM2 RES:", svc.set_camera_resolution(2, args.width, args.height))
print("SET CAM0 BAYER:", svc.set_camera_bayer(0, args.bayer))
print("SET CAM1 BAYER:", svc.set_camera_bayer(1, args.bayer))
print("SET FPS:", svc.set_fps(args.fps))
print("SET CAPTURE MODE:", svc.set_capture_mode(effective_capture_mode))
print("SET FRAME TYPE:", svc.set_frame_type("RAW_BRUTO"))
print("SET OUTPUT DTYPE:", svc.set_output_dtype("float32"))
begin_resp = svc.begin(frame_type="RAW_BRUTO", output_dtype="float32", capture_mode=effective_capture_mode)
print("BEGIN:", begin_resp)
status = svc.get_status()
print("STATUS:", json.dumps({
"status": status.get("status"),
"detected_mode": status.get("detected_mode"),
"camera_count_active": status.get("camera_count_active"),
"active_camera_ids": status.get("active_camera_ids"),
}, ensure_ascii=False))
validate_module_ready(status, "RAW_BRUTO", args.raw_policy, effective_capture_mode)
print("START STREAM:", svc.start_stream(args.pc_host, args.stream_port, fps=args.fps))
try:
for cam_id in ("cam0", "cam1", "cam2"):
initial_ctrl = svc.get_camera_controls(cam_id)
camera_controls[cam_id]["ae_enable"] = bool(
initial_ctrl.get("ae_enable", camera_controls[cam_id]["ae_enable"])
)
camera_controls[cam_id]["awb_enable"] = bool(
initial_ctrl.get("awb_enable", camera_controls[cam_id]["awb_enable"])
)
exp_val = initial_ctrl.get("exposure_time_us", camera_controls[cam_id]["exposure_time_us"])
if exp_val is not None:
exp_val = int(exp_val)
camera_controls[cam_id]["exposure_time_us"] = exp_val
gain_val = initial_ctrl.get("analogue_gain", camera_controls[cam_id]["analogue_gain"])
if gain_val is not None:
gain_val = float(gain_val)
camera_controls[cam_id]["analogue_gain"] = gain_val
camera_controls[cam_id]["colour_gains"] = initial_ctrl.get(
"colour_gains",
camera_controls[cam_id]["colour_gains"]
)
except Exception as e:
print(f"[WARN] Falha ao ler controles iniciais: {e}")
while True:
t0 = time.time()
meta = receiver.last_meta
frame = receiver.last_frame
if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id:
last_frame_id = meta["frame_id"]
if not isinstance(frame, dict):
raise RuntimeError("Este calibrador espera RAW_BRUTO multi-payload como dict de câmeras.")
decoded_last = decoder.decode_stream_cameras(frame, meta)
curr_frame_id = meta.get("frame_id")
if curr_frame_id is not None and last_stream_frame_id != curr_frame_id:
stream_frames_accum += 1
last_stream_frame_id = curr_frame_id
dt_stream = time.time() - t_stream_fps
if dt_stream >= 1.0:
fps_stream = stream_frames_accum / dt_stream
stream_frames_accum = 0
t_stream_fps = time.time()
view_frames += 1
dt_view = time.time() - t_view_fps
if dt_view >= 1.0:
fps_view = view_frames / dt_view
view_frames = 0
t_view_fps = time.time()
if decoded_last:
rgb01 = decoded_last.get("cam2", {}).get("image")
re01 = decoded_last.get("cam0", {}).get("image")
nir01 = decoded_last.get("cam1", {}).get("image")
if rgb01 is None:
rgb_panel = build_empty_panel((args.height, args.width), "RGB")
base_h, base_w = args.height, args.width
else:
rgb_panel = to_bgr_u8_from_rgb01(rgb01)
base_h, base_w = rgb01.shape[:2]
re_panel = gray_to_bgr_u8(resize_if_needed(re01, (base_h, base_w))) if re01 is not None else build_empty_panel((base_h, base_w), "RE")
nir_panel = gray_to_bgr_u8(resize_if_needed(nir01, (base_h, base_w))) if nir01 is not None else build_empty_panel((base_h, base_w), "NIR")
draw_rois(rgb_panel, rois["cam2"])
draw_rois(re_panel, rois["cam0"])
draw_rois(nir_panel, rois["cam1"])
if drawing_roi and roi_start is not None and roi_current is not None:
active_panel = {"cam2": rgb_panel, "cam0": re_panel, "cam1": nir_panel}.get(selected_cam)
if active_panel is not None:
color = (0, 255, 255)
cv2.rectangle(active_panel, roi_start, roi_current, color, 1)
overlay_hud(rgb_panel, ["RGB (cam2)", f"ativo={selected_cam == 'cam2'}"])
overlay_hud(re_panel, ["RE (cam0)", f"ativo={selected_cam == 'cam0'}"])
overlay_hud(nir_panel, ["NIR (cam1)", f"ativo={selected_cam == 'cam1'}"])
ph = max(rgb_panel.shape[0], re_panel.shape[0], nir_panel.shape[0], base_h)
pw = max(rgb_panel.shape[1], re_panel.shape[1], nir_panel.shape[1], base_w)
def fit_panel(img):
if img.shape[:2] != (ph, pw):
return cv2.resize(img, (pw, ph), interpolation=cv2.INTER_NEAREST)
return img
rgb_panel = fit_panel(rgb_panel)
re_panel = fit_panel(re_panel)
nir_panel = fit_panel(nir_panel)
data_panel = np.zeros((ph, pw, 3), dtype=np.uint8)
panel_rects["cam2"] = (0, 0, pw, ph)
panel_rects["cam0"] = (pw, 0, pw * 2, ph)
panel_rects["cam1"] = (0, ph, pw, ph * 2)
panel_rects["data"] = (pw, ph, pw * 2, ph * 2)
top = np.hstack([rgb_panel, re_panel])
bottom = np.hstack([nir_panel, data_panel])
board = np.vstack([top, bottom])
active_img = decoded_last.get(selected_cam, {}).get("image")
global_stats = compute_scene_health(active_img) if active_img is not None else None
ctrl = camera_controls[selected_cam]
lines = [
f"CAM ATIVA: {selected_cam}",
f"AE={'ON' if ctrl['ae_enable'] else 'OFF'} | AWB={'ON' if ctrl['awb_enable'] else 'OFF'}",
f"EXP={ctrl['exposure_time_us']} us",
f"GAIN={ctrl['analogue_gain']:.2f}",
f"fps_stream={fps_stream:.1f} | fps_view={fps_view:.1f}",
]
if global_stats is not None:
lines.extend([
f"mean={global_stats['mean']:.3f} | std={global_stats['std']:.3f}",
f"p05={global_stats['p05']:.3f} | p95={global_stats['p95']:.3f}",
f"sat={global_stats['pct_saturated']:.2f}% | dark={global_stats['pct_dark']:.2f}%",
f"scene={global_stats['comment']}",
])
else:
lines.append("sem stats da cena")
lines.append("-")
lines.append(f"ROIs: {len(rois[selected_cam])}")
for idx, roi in enumerate(rois[selected_cam][:6]):
if active_img is None:
break
stats = compute_stats_from_roi(active_img, roi["rect"])
lines.append(f"{roi['name']}: mean={stats['mean']:.3f} std={stats['std']:.3f}")
lines.append(f" p95={stats['p95']:.3f} sat={stats['pct_saturated']:.1f}% dark={stats['pct_dark']:.1f}%")
lines.extend([
"-",
"1=RGB | 2=RE | 3=NIR | E=AE | B=AWB",
"I/K exp +/- | O/L gain +/- | A aplica",
"mouse: arrasta ROI | U desfaz ROI | C limpa ROIs",
"SPACE salva JSON | S salva snapshot | Q sai",
])
x0, y0, _, _ = panel_rects["data"]
overlay_hud(board, lines, x=x0 + 12, y=y0 + 22, font_scale=0.52, line_step=20)
if last_msg and (time.time() - last_msg_t) < 2.5:
cv2.putText(board, last_msg, (12, board.shape[0] - 16), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2, cv2.LINE_AA)
if args.preview_scale != 1.0:
board = cv2.resize(
board,
(int(board.shape[1] * args.preview_scale), int(board.shape[0] * args.preview_scale)),
interpolation=cv2.INTER_NEAREST,
)
cv2.imshow(window_name, board)
else:
blank = np.zeros((720, 1280, 3), dtype=np.uint8)
overlay_hud(blank, ["Aguardando frames do módulo..."], x=40, y=80, font_scale=1.0, line_step=34)
cv2.imshow(window_name, blank)
k = cv2.waitKey(1) & 0xFF
if k in (ord("q"), ord("Q"), 27):
break
elif k == ord("1"):
selected_cam = "cam2"
last_msg = "Selecionada: cam2 / RGB"
last_msg_t = time.time()
elif k == ord("2"):
selected_cam = "cam0"
last_msg = "Selecionada: cam0 / RE"
last_msg_t = time.time()
elif k == ord("3"):
selected_cam = "cam1"
last_msg = "Selecionada: cam1 / NIR"
last_msg_t = time.time()
elif k in (ord("e"), ord("E")):
camera_controls[selected_cam]["ae_enable"] = not camera_controls[selected_cam]["ae_enable"]
last_msg = f"AE {selected_cam} -> {'ON' if camera_controls[selected_cam]['ae_enable'] else 'OFF'}"
last_msg_t = time.time()
elif k in (ord("b"), ord("B")):
if selected_cam == "cam2":
camera_controls[selected_cam]["awb_enable"] = not camera_controls[selected_cam]["awb_enable"]
last_msg = f"AWB {selected_cam} -> {'ON' if camera_controls[selected_cam]['awb_enable'] else 'OFF'}"
else:
last_msg = "AWB só se aplica ao RGB"
last_msg_t = time.time()
elif k in (ord("i"), ord("I")):
if camera_controls[selected_cam]["exposure_time_us"] is None:
camera_controls[selected_cam]["exposure_time_us"] = 15000
else:
camera_controls[selected_cam]["exposure_time_us"] = int(
min(camera_controls[selected_cam]["exposure_time_us"] + args.exp_step, 200000)
)
last_msg = f"EXP {selected_cam} -> {camera_controls[selected_cam]['exposure_time_us']} us"
last_msg_t = time.time()
elif k in (ord("k"), ord("K")):
if camera_controls[selected_cam]["exposure_time_us"] is None:
camera_controls[selected_cam]["exposure_time_us"] = 15000
else:
camera_controls[selected_cam]["exposure_time_us"] = int(
max(camera_controls[selected_cam]["exposure_time_us"] - args.exp_step, 100)
)
last_msg = f"EXP {selected_cam} -> {camera_controls[selected_cam]['exposure_time_us']} us"
last_msg_t = time.time()
elif k in (ord("o"), ord("O")):
if camera_controls[selected_cam]["analogue_gain"] is None:
camera_controls[selected_cam]["analogue_gain"] = 1.0
else:
camera_controls[selected_cam]["analogue_gain"] = float(
min(camera_controls[selected_cam]["analogue_gain"] * (1.0 + args.gain_step), 32.0)
)
last_msg = f"GAIN {selected_cam} -> {camera_controls[selected_cam]['analogue_gain']:.2f}"
last_msg_t = time.time()
elif k in (ord("l"), ord("L")):
if camera_controls[selected_cam]["analogue_gain"] is None:
camera_controls[selected_cam]["analogue_gain"] = 1.0
else:
camera_controls[selected_cam]["analogue_gain"] = float(
max(camera_controls[selected_cam]["analogue_gain"] / (1.0 + args.gain_step), 1.0)
)
last_msg = f"GAIN {selected_cam} -> {camera_controls[selected_cam]['analogue_gain']:.2f}"
last_msg_t = time.time()
elif k in (ord("a"), ord("A")):
apply_controls_to_selected_cam()
elif k in (ord("u"), ord("U")):
if rois[selected_cam]:
removed = rois[selected_cam].pop()
last_msg = f"ROI removida: {removed['name']}"
last_msg_t = time.time()
elif k in (ord("c"), ord("C")):
rois[selected_cam] = []
last_msg = f"ROIs limpas em {selected_cam}"
last_msg_t = time.time()
elif k in (ord("s"), ord("S")):
snap = snapshot_current_state()
if snap is not None:
data_payload.setdefault("snapshots", []).append(snap)
last_msg = f"Snapshot salvo: {selected_cam} | rois={len(snap['rois'])}"
else:
last_msg = "Sem frame ativo para snapshot"
last_msg_t = time.time()
elif k == 32:
data_payload["camera_settings"] = json.loads(json.dumps(camera_controls))
save_payload(args.out_json, data_payload)
last_msg = f"JSON salvo em: {args.out_json}"
last_msg_t = time.time()
dt_loop = time.time() - t0
if dt_loop < 0.001:
time.sleep(0.001)
finally:
try:
print("STOP STREAM:", svc.stop_stream())
except Exception:
pass
try:
print("STOP:", svc.stop())
except Exception:
pass
try:
svc.disconnect()
except Exception:
pass
try:
receiver.stop()
except Exception:
pass
cv2.destroyAllWindows()
print("Fim da calibração dos sensores.")
if __name__ == "__main__":
main()