From d9f83292fb6c3c128c7ce33669551c7b3a4c2089 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Wed, 28 Jan 2026 15:05:51 -0300 Subject: [PATCH] scripts teste gal5000 --- Python/gal5000/capture_one_raw.py | 136 ++++ Python/gal5000/dataset_capture_ae.py | 653 ++++++++++++++++++ Python/gal5000/dataset_caputre.py | 422 +++++++++++ Python/gal5000/dlls/HQVSDK.xml | 10 + Python/gal5000/dlls/HQV_AVIProcess64.dll | 3 + Python/gal5000/dlls/HQV_DataConvert64.dll | 3 + Python/gal5000/dlls/HQV_FilePro64.dll | 3 + Python/gal5000/dlls/HQV_TinyXML64.dll | 3 + Python/gal5000/dlls/HQV_USB64.dll | 3 + Python/gal5000/dlls/HQV_USS64.dll | 3 + Python/gal5000/dlls/KSJApi64.dll | 3 + Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll | 3 + Python/gal5000/dlls/VT_SDK64.dll | 3 + Python/gal5000/gal5000_test_params.py | 250 +++++++ Python/gal5000/preview_4ch_autoexposure.py | 547 +++++++++++++++ Python/gal5000/preview_auto_exposure.py | 560 +++++++++++++++ Python/gal5000/probe_params.py | 342 +++++++++ Python/gal5000/quick_sugar_vs_weed_overlay.py | 176 +++++ Python/gal5000/test_vt.py | 10 + 19 files changed, 3133 insertions(+) create mode 100644 Python/gal5000/capture_one_raw.py create mode 100644 Python/gal5000/dataset_capture_ae.py create mode 100644 Python/gal5000/dataset_caputre.py create mode 100644 Python/gal5000/dlls/HQVSDK.xml create mode 100644 Python/gal5000/dlls/HQV_AVIProcess64.dll create mode 100644 Python/gal5000/dlls/HQV_DataConvert64.dll create mode 100644 Python/gal5000/dlls/HQV_FilePro64.dll create mode 100644 Python/gal5000/dlls/HQV_TinyXML64.dll create mode 100644 Python/gal5000/dlls/HQV_USB64.dll create mode 100644 Python/gal5000/dlls/HQV_USS64.dll create mode 100644 Python/gal5000/dlls/KSJApi64.dll create mode 100644 Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll create mode 100644 Python/gal5000/dlls/VT_SDK64.dll create mode 100644 Python/gal5000/gal5000_test_params.py create mode 100644 Python/gal5000/preview_4ch_autoexposure.py create mode 100644 Python/gal5000/preview_auto_exposure.py create mode 100644 Python/gal5000/probe_params.py create mode 100644 Python/gal5000/quick_sugar_vs_weed_overlay.py create mode 100644 Python/gal5000/test_vt.py diff --git a/Python/gal5000/capture_one_raw.py b/Python/gal5000/capture_one_raw.py new file mode 100644 index 000000000..48e45aa55 --- /dev/null +++ b/Python/gal5000/capture_one_raw.py @@ -0,0 +1,136 @@ +import os +import ctypes as C +from ctypes import wintypes as W + +from PIL import Image # pip install pillow + +# ====== AJUSTE AQUI ====== +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" +TIMEOUT_MS = 2000 + +OUT_DIR = os.path.join(os.path.dirname(__file__), "out") +os.makedirs(OUT_DIR, exist_ok=True) + +# nomes de saída +OUT_RAW = os.path.join(OUT_DIR, "frame_000001.raw") # RAW cru (do buffer) +OUT_BMP = os.path.join(OUT_DIR, "frame_000001_preview.bmp") # preview gerado pelo SDK +OUT_PNG = os.path.join(OUT_DIR, "frame_000001_preview.png") # preview final pra rotular + +# ====== LOAD DLL ====== +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +# ====== CONSTANTES ====== +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 + +DATA_RAW = 0 + +# Observação: no seu teste anterior, "filetype=0" gerou um BMP. +# Então aqui a gente assume que 0 = BMP (preview). O RAW vamos salvar manualmente do buffer. +FILE_BMP = 0 + + +# ====== STRUCTS (compatível com o seu uso atual) ====== +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), # alinhamento + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + + +# ====== PROTÓTIPOS ====== +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_SingleFrameSavefile.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_char_p, C.c_int] +dll.VT_SingleFrameSavefile.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + + +def ck(ret: int, name: str): + if ret != 0: + raise RuntimeError(f"{name} falhou, ret={ret}") + + +def is_bmp(path: str) -> bool: + try: + with open(path, "rb") as f: + return f.read(2) == b"BM" + except Exception: + return False + + +def main(): + # 1) scan + n = C.c_ubyte(0) + ret = dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF) + print("DeviceScan ret=", ret, "n=", n.value) + ck(ret, "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhum dispositivo encontrado") + + # 2) open por índice 0 + idx = C.c_ubyte(0) + h = W.HANDLE() + ret = dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF) + print("DeviceOpen ret=", ret, "handle=", h.value) + ck(ret, "VT_DeviceOpen") + + # 3) captura 1 frame (RAW no buffer) + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + print("SingleFrameCapture ret=", ret, + "W,H=", fi.lWidth, fi.lHeight, + "pixbits=", fi.lPixBits, "buf=", fi.lBufSize, + "pixType=", fi.lPixType) + ck(ret, "VT_SingleFrameCapture") + + # 4) salva RAW CRU (do buffer) + raw_bytes = C.string_at(fi.pBufPtr, fi.lBufSize) + with open(OUT_RAW, "wb") as f: + f.write(raw_bytes) + print(f"RAW cru salvo: {OUT_RAW} | {len(raw_bytes)/1024/1024:.2f} MB") + + # 5) salva preview via SDK (BMP) e converte pra PNG + ret = dll.VT_SingleFrameSavefile(h, C.byref(fi), FILE_BMP, OUT_BMP.encode("utf-8"), 90) + print("SingleFrameSavefile (preview BMP) ret=", ret, "->", OUT_BMP) + ck(ret, "VT_SingleFrameSavefile") + + if not is_bmp(OUT_BMP): + print("⚠️ Preview não parece BMP (não começa com 'BM'). Mesmo assim vou tentar abrir...") + img = Image.open(OUT_BMP) + img.save(OUT_PNG) + print("Preview PNG salvo:", OUT_PNG) + + # 6) close + ret = dll.VT_DeviceClose(C.byref(h)) + print("DeviceClose ret=", ret) + ck(ret, "VT_DeviceClose") + + print("\nOK ✅") + print(" - RAW cru (treino/inferência):", OUT_RAW) + print(" - Preview (rotulagem):", OUT_PNG) + + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/dataset_capture_ae.py b/Python/gal5000/dataset_capture_ae.py new file mode 100644 index 000000000..749b6fdd5 --- /dev/null +++ b/Python/gal5000/dataset_capture_ae.py @@ -0,0 +1,653 @@ +import os +import time +import json +import math +import ctypes as C +from ctypes import wintypes as W +from datetime import datetime + +import numpy as np +import cv2 + +# ============================================================ +# GAL5000 dataset capture + robust software auto-exposure +# +# This version is aligned with our working VT SDK findings: +# - Uses ExposureRaw (0x3010) for exposure control. +# - Uses Digital Gain Raw (0x302A) as the secondary control. +# - Does NOT attempt to set Analog Gain via 0x3020 (known 4109). +# - Reads initial values via GET when available, then tracks locally. +# +# Keys: +# C / SPACE : save sample now +# A : toggle auto-save +# E : toggle auto-exposure +# M : toggle preview upscale (speed/clarity) +# Q / ESC : quit +# When AE is OFF: +# +/- : exposure +/- +# [ ] : exposure fast +/- +# V / X : digital gain +/- +# ============================================================ + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +# Output dataset root +OUT_ROOT = os.path.join(os.path.dirname(__file__), "dataset") +SESSION_DIR = os.path.join(OUT_ROOT, datetime.now().strftime("%Y%m%d")) +os.makedirs(SESSION_DIR, exist_ok=True) + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry +RAW_W = 2592 +RAW_H = 2056 + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 Dataset Capture (C/SPACE=save | A=auto-save | E=AE toggle | Q=quit)" + +# Preview +UPSCALE = 2 + +# Auto-save +CAPTURE_INTERVAL_S = 1.0 + +# ========================= +# PARAM IDs +# ========================= +BUF_SIZE = 256 + +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 # GET may work; SET may fail (4109) +PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A + +# Optional SFNC info (may fail on some firmware) +PARAM_ID_SFNC_SENSORWIDTH = 0x00001101 +PARAM_ID_SFNC_SENSORHEIGHT = 0x00001102 +PARAM_ID_SFNC_WIDTHMAX = 0x00001106 +PARAM_ID_SFNC_HEIGHTMAX = 0x00001107 +PARAM_ID_SFNC_WIDTH = 0x00001111 +PARAM_ID_SFNC_HEIGHT = 0x00001112 +PARAM_ID_SFNC_OFFSETX = 0x00001113 +PARAM_ID_SFNC_OFFSETY = 0x00001114 + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + +# ========================= +# LIMITS +# ========================= +EXP_MIN = 1 +EXP_MAX = 20000 +EXP_STEP = 200 +EXP_STEP_FAST = 1000 + +# Digital gain (we already confirmed GET/SET works) +GAIN_D_MIN, GAIN_D_MAX = 0, 8 # keep conservative; expand if you verify bigger range +GAIN_D_STEP = 1 + +# ROI for AE metrics (camera looks at ground) +ROI_Y0_FRAC = 0.55 +ROI_Y1_FRAC = 0.95 +ROI_X0_FRAC = 0.15 +ROI_X1_FRAC = 0.85 + +# AE targets +TARGET_P95 = 140.0 +DEADBAND = 6.0 +SAT_LIMIT = 0.01 + +# AE controller tuning (log-domain multiplicative) +K_LOG = 0.12 +MAX_STEP = 0.10 +EMA_ALPHA = 0.20 + + +def clamp(v, lo, hi): + return lo if v < lo else hi if v > hi else v + + +def ts_name() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + + +# ========================= +# VT SDK structures +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" + return p + + +# ========================= +# DLL load + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + + +def ck(ret: int, name: str): + if ret != 0: + raise RuntimeError(f"{name} falhou, ret={ret}") + + +def param_get_int(h: W.HANDLE, pid: int) -> int: + p = devparam_by_id(pid) + v = C.c_int(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return int(v.value) + + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(int(value)) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + + +# ========================= +# Capture + preview +# ========================= +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + + +def norm8(x, p_lo=2, p_hi=98): + lo = np.percentile(x, p_lo) + hi = np.percentile(x, p_hi) + if hi <= lo + 1: + return x.astype(np.uint8) + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + +def make_rgb_preview(raw: np.ndarray, upscale=2) -> np.ndarray: + # Pattern: + # R G + # IR B + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + Rn, Gn, Bn = norm8(R), norm8(G), norm8(B) + bgr = np.dstack([Bn, Gn, Rn]) + + if upscale and upscale != 1: + bgr = cv2.resize( + bgr, + (bgr.shape[1] * upscale, bgr.shape[0] * upscale), + interpolation=cv2.INTER_NEAREST, + ) + return bgr + + +def overlay_hud(img_bgr, lines): + y = 28 + for s in lines: + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 0), 3, cv2.LINE_AA) + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), 2, cv2.LINE_AA) + y += 28 + + +# ========================= +# AE metrics + controller +# ========================= +def measure_raw_g_metrics(raw: np.ndarray): + """Measure p90/p95 and saturation ratio on RAW green channel within ROI.""" + G = raw[0::2, 1::2] # H/2 x W/2 + h2, w2 = G.shape + + y0, y1 = int(h2 * ROI_Y0_FRAC), int(h2 * ROI_Y1_FRAC) + x0, x1 = int(w2 * ROI_X0_FRAC), int(w2 * ROI_X1_FRAC) + roi = G[y0:y1, x0:x1] + + p90 = float(np.percentile(roi, 90)) + p95 = float(np.percentile(roi, 95)) + sat = float(np.mean(roi >= 250)) + return p90, p95, sat + + +class AEController: + """Industrial-ish software AE (multiplicative in log space, with EMA + deadband). + + Primary actuator: ExposureRaw + Secondary actuator (only when exp hits limits): DigitalGainRaw + """ + + def __init__( + self, + exp_min=EXP_MIN, + exp_max=EXP_MAX, + target_p95=TARGET_P95, + deadband=DEADBAND, + k=K_LOG, + max_step=MAX_STEP, + ema_alpha=EMA_ALPHA, + sat_limit=SAT_LIMIT, + gain_d_min=GAIN_D_MIN, + gain_d_max=GAIN_D_MAX, + gain_d_step=GAIN_D_STEP, + ): + self.exp_min = exp_min + self.exp_max = exp_max + self.target = target_p95 + self.deadband = deadband + self.k = k + self.max_step = max_step + self.ema_alpha = ema_alpha + self.sat_limit = sat_limit + + self.gain_d_min = gain_d_min + self.gain_d_max = gain_d_max + self.gain_d_step = gain_d_step + + self.p95_ema = None + + def reset(self): + self.p95_ema = None + + def step(self, raw: np.ndarray, exp_raw: int, gain_d: int): + p90, p95, sat = measure_raw_g_metrics(raw) + + # EMA + if self.p95_ema is None: + self.p95_ema = p95 + else: + self.p95_ema = (1.0 - self.ema_alpha) * self.p95_ema + self.ema_alpha * p95 + + e = self.target - self.p95_ema + + # deadband hold + if abs(e) <= self.deadband and sat <= self.sat_limit: + dbg = {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "step": 0.0, "hold": True} + return exp_raw, gain_d, dbg + + # compute multiplicative exposure step + if sat > self.sat_limit: + step = -min(self.max_step, 0.12) + else: + ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6) + step = self.k * math.log(ratio) + step = clamp(step, -self.max_step, +self.max_step) + + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = int(clamp(new_exp, self.exp_min, self.exp_max)) + + new_gain_d = gain_d + + # Secondary: adjust digital gain only when exposure is saturated at limits + if new_exp >= self.exp_max and self.p95_ema < (self.target - self.deadband): + new_gain_d = int(clamp(gain_d + self.gain_d_step, self.gain_d_min, self.gain_d_max)) + + if new_exp <= self.exp_min and (self.p95_ema > (self.target + self.deadband) or sat > self.sat_limit): + new_gain_d = int(clamp(gain_d - self.gain_d_step, self.gain_d_min, self.gain_d_max)) + + dbg = {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "step": step, "hold": False} + return new_exp, new_gain_d, dbg + + +# ========================= +# Saving +# ========================= +def save_sample(raw: np.ndarray, bgr_preview: np.ndarray, meta: dict): + name = ts_name() + raw_path = os.path.join(SESSION_DIR, f"{name}.raw") + png_path = os.path.join(SESSION_DIR, f"{name}.png") + json_path = os.path.join(SESSION_DIR, f"{name}.json") + + raw.tofile(raw_path) + cv2.imwrite(png_path, bgr_preview) + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return raw_path, png_path, json_path + + +# ========================= +# MAIN +# ========================= +def main(): + # scan + n = C.c_ubyte(0) + ck(dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF), "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ck(dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF), "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + print("Saving to:", SESSION_DIR) + + # camera info (best-effort) + try: + sensor_w = param_get_int(h, PARAM_ID_SFNC_SENSORWIDTH) + sensor_h = param_get_int(h, PARAM_ID_SFNC_SENSORHEIGHT) + roi_w = param_get_int(h, PARAM_ID_SFNC_WIDTH) + roi_h = param_get_int(h, PARAM_ID_SFNC_HEIGHT) + roi_x = param_get_int(h, PARAM_ID_SFNC_OFFSETX) + roi_y = param_get_int(h, PARAM_ID_SFNC_OFFSETY) + width_max = param_get_int(h, PARAM_ID_SFNC_WIDTHMAX) + height_max = param_get_int(h, PARAM_ID_SFNC_HEIGHTMAX) + print(f"[CAM] sensor={sensor_w}x{sensor_h} roi={roi_w}x{roi_h}+{roi_x},{roi_y} max={width_max}x{height_max}") + except Exception as e: + print("[CAM] Info SFNC indisponível:", e) + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + + # local preview scale (avoid global mutation inside the loop) + upscale = UPSCALE + + # Read initial values (best-effort) + try: + exp_raw = param_get_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW) + except Exception: + exp_raw = 1500 + + # gain_a only for metadata (may be readable, but we won't set it) + try: + gain_a = param_get_int(h, PARAM_ID_SENSOR_GAINANALOGRAW) + except Exception: + gain_a = 0 + + try: + gain_d = param_get_int(h, PARAM_ID_SENSOR_GAINDIGITRAW) + except Exception: + gain_d = 2 + + print(f"[INIT] exp_raw={exp_raw} gainA(readonly?)={gain_a} gainD={gain_d}") + + # Apply initial exposure + digital gain (ignore failures gracefully) + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, int(exp_raw)) + except Exception as e: + print("[WARN] Falhou set exp inicial:", e) + + try: + param_set_int(h, PARAM_ID_SENSOR_GAINDIGITRAW, int(gain_d)) + except Exception as e: + print("[WARN] Falhou set gainD inicial:", e) + + ae = AEController() + ae_on = True + auto_save = False + last_auto_t = 0.0 + + # FPS + t0 = time.time() + frames = 0 + fps = 0.0 + + last_msg = "" + last_msg_t = 0.0 + + def set_exposure(new_exp: int) -> int: + new_exp = int(clamp(int(new_exp), EXP_MIN, EXP_MAX)) + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, new_exp) + return new_exp + + def set_gain_d(new_gain: int) -> int: + new_gain = int(clamp(int(new_gain), GAIN_D_MIN, GAIN_D_MAX)) + param_set_int(h, PARAM_ID_SENSOR_GAINDIGITRAW, new_gain) + return new_gain + + try: + while True: + raw = capture_raw8(h) + + # best-effort refresh analog gain (read-only meta) + try: + gain_a = param_get_int(h, PARAM_ID_SENSOR_GAINANALOGRAW) + except Exception: + pass + + # software AE + ae_dbg = {} + if ae_on: + new_exp, new_gain_d, ae_dbg = ae.step(raw, exp_raw, gain_d) + + if new_exp != exp_raw: + try: + exp_raw = set_exposure(new_exp) + except Exception as e: + print("[ERR] set exposure:", e) + ae_on = False + + if new_gain_d != gain_d: + try: + gain_d = set_gain_d(new_gain_d) + except Exception as e: + print("[ERR] set gainD:", e) + ae_on = False + else: + ae_dbg = {} + + # clean preview (this is what we save) + rgb_clean = make_rgb_preview(raw, upscale=upscale) + bgr = rgb_clean.copy() + + # FPS + frames += 1 + dt = time.time() - t0 + if dt >= 1.0: + fps = frames / dt + frames = 0 + t0 = time.time() + + # HUD + p95_disp = ae_dbg.get("p95_ema", ae_dbg.get("p95", 0.0)) + lines = [ + f"AE: {'ON' if ae_on else 'OFF'} | AutoSave: {'ON' if auto_save else 'OFF'} | Interval: {CAPTURE_INTERVAL_S:.1f}s", + f"exp_raw={exp_raw} gain_d={gain_d} (gain_a={gain_a}) | FPS={fps:.1f}", + f"AEdbg: p95={p95_disp:.1f} sat={ae_dbg.get('sat', 0):.3f} hold={ae_dbg.get('hold', False)}", + "Keys: C/SPACE=save | A=autosave | E=AE | M=toggle preview | Q quit", + "(AE OFF): +/- exp | [ ] exp fast | V/C gainD", + ] + overlay_hud(bgr, lines) + + # post-save message + if last_msg and (time.time() - last_msg_t) < 2.0: + cv2.putText( + bgr, + last_msg, + (12, bgr.shape[0] - 18), + cv2.FONT_HERSHEY_SIMPLEX, + 0.8, + (0, 255, 0), + 2, + cv2.LINE_AA, + ) + + cv2.imshow(WINDOW_NAME, bgr) + + # autosave + now = time.time() + if auto_save and (now - last_auto_t) >= CAPTURE_INTERVAL_S: + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, + "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "ae_dbg": {k: (float(v) if isinstance(v, (int, float, np.floating)) else v) for k, v in ae_dbg.items()}, + "note": "autosave", + } + raw_path, _, _ = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = now + last_auto_t = now + + k = cv2.waitKey(1) & 0xFF + if k in (ord("q"), ord("Q"), 27): + break + + elif k in (ord("a"), ord("A")): + auto_save = not auto_save + last_msg = f"AutoSave -> {'ON' if auto_save else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord("e"), ord("E")): + ae_on = not ae_on + if ae_on: + ae.reset() + last_msg = f"AE -> {'ON' if ae_on else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord("m"), ord("M")): + # quick toggle upscale (helps on slower PCs) + upscale = 0 if upscale else 2 + last_msg = f"Preview UPSCALE -> {upscale}" + last_msg_t = time.time() + + elif k in (ord("c"), ord("C"), 32): # C or SPACE + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, + "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "ae_dbg": {k2: (float(v2) if isinstance(v2, (int, float, np.floating)) else v2) for k2, v2 in ae_dbg.items()}, + "note": "manual", + } + raw_path, _, _ = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = time.time() + + # manual controls only when AE is off + elif not ae_on: + if k in (ord("+"), ord("=")): + exp_raw = int(clamp(exp_raw + EXP_STEP, EXP_MIN, EXP_MAX)) + try: + exp_raw = set_exposure(exp_raw) + except Exception as e: + print("[ERR] manual exp +:", e) + + elif k in (ord("-"), ord("_")): + exp_raw = int(clamp(exp_raw - EXP_STEP, EXP_MIN, EXP_MAX)) + try: + exp_raw = set_exposure(exp_raw) + except Exception as e: + print("[ERR] manual exp -:", e) + + elif k == ord("]"): + exp_raw = int(clamp(exp_raw + EXP_STEP_FAST, EXP_MIN, EXP_MAX)) + try: + exp_raw = set_exposure(exp_raw) + except Exception as e: + print("[ERR] manual exp fast +:", e) + + elif k == ord("["): + exp_raw = int(clamp(exp_raw - EXP_STEP_FAST, EXP_MIN, EXP_MAX)) + try: + exp_raw = set_exposure(exp_raw) + except Exception as e: + print("[ERR] manual exp fast -:", e) + + elif k in (ord("v"), ord("V")): + try: + gain_d = set_gain_d(gain_d + GAIN_D_STEP) + print(f"[MANUAL] GainD -> {gain_d}") + except Exception as e: + print("[ERR] manual gainD +:", e) + + elif k in (ord("c"), ord("C")): + # Note: C is already save; we keep this branch unreachable. + pass + + elif k in (ord("x"), ord("X")): + # convenience: use X as gainD - (since C is save) + try: + gain_d = set_gain_d(gain_d - GAIN_D_STEP) + print(f"[MANUAL] GainD -> {gain_d}") + except Exception as e: + print("[ERR] manual gainD -:", e) + + finally: + try: + ret = dll.VT_DeviceClose(C.byref(h)) + if ret != 0: + print("VT_DeviceClose retornou:", ret) + except Exception as e: + print("Erro ao fechar:", e) + cv2.destroyAllWindows() + + print("Fim.") + + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/dataset_caputre.py b/Python/gal5000/dataset_caputre.py new file mode 100644 index 000000000..0867fb1b5 --- /dev/null +++ b/Python/gal5000/dataset_caputre.py @@ -0,0 +1,422 @@ +import os +import time +import json +import math +import ctypes as C +from ctypes import wintypes as W +from datetime import datetime + +import numpy as np +import cv2 + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +# Onde salvar o dataset +OUT_ROOT = os.path.join(os.path.dirname(__file__), "dataset") +SESSION_DIR = os.path.join(OUT_ROOT, datetime.now().strftime("%Y%m%d")) +os.makedirs(SESSION_DIR, exist_ok=True) + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (se mudar no futuro, ajuste) +RAW_W = 2592 +RAW_H = 2056 + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 Dataset Capture (C/SPACE=save | A=auto-save | E=AE toggle | Q=quit)" + +# Preview +UPSCALE = 2 + +# Auto-save +CAPTURE_INTERVAL_S = 1.0 + +# Param IDs (VT_Param.h) +BUF_SIZE = 256 +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + +# Exposure/Gain limits (ajuste depois conforme o sensor aceitar) +EXP_MIN = 1 +EXP_MAX = 20000 + +GAIN_A_MIN, GAIN_A_MAX = 0, 255 +GAIN_D_MIN, GAIN_D_MAX = 0, 255 + +# ========================= +# Helpers +# ========================= +def ck(ret: int, name: str): + if ret != 0: + raise RuntimeError(f"{name} falhou, ret={ret}") + +def ts_name() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + +def clamp(v, lo, hi): + return lo if v < lo else hi if v > hi else v + +def norm8(x, p_lo=2, p_hi=98): + lo = np.percentile(x, p_lo) + hi = np.percentile(x, p_hi) + if hi <= lo + 1: + return x.astype(np.uint8) + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + +def make_rgb_preview(raw: np.ndarray, upscale=2) -> np.ndarray: + # pattern: + # R G + # IR B + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + Rn, Gn, Bn = norm8(R), norm8(G), norm8(B) + bgr = np.dstack([Bn, Gn, Rn]) # OpenCV usa BGR + if upscale and upscale != 1: + bgr = cv2.resize(bgr, (bgr.shape[1]*upscale, bgr.shape[0]*upscale), interpolation=cv2.INTER_NEAREST) + return bgr + +def measure_raw_g_metrics(raw: np.ndarray): + """ + Mede brilho no canal G cru usando uma ROI na base (mais parecido com chão). + Retorna p90/p95 e fração saturada. + """ + G = raw[0::2, 1::2] # H/2 x W/2 + h2, w2 = G.shape + + # ROI: base da imagem, cortando laterais + y0, y1 = int(h2 * 0.55), int(h2 * 0.95) + x0, x1 = int(w2 * 0.15), int(w2 * 0.85) + roi = G[y0:y1, x0:x1] + + p90 = float(np.percentile(roi, 90)) + p95 = float(np.percentile(roi, 95)) + sat = float(np.mean(roi >= 250)) + return p90, p95, sat + +class RobustAE: + """ + Controle soft de exposure (sem depender do GET da camera): + - mede p95 do canal G cru em ROI + - usa EMA + deadband (pra não ficar "descendo até 16" como você viu) + - passo multiplicativo em log, com limite de passo + """ + def __init__(self, + exp_min=EXP_MIN, exp_max=EXP_MAX, + target_p95=140.0, + deadband=6.0, + k=0.12, + max_step=0.10, + ema_alpha=0.20, + sat_limit=0.01): + self.exp_min = exp_min + self.exp_max = exp_max + self.target = target_p95 + self.deadband = deadband + self.k = k + self.max_step = max_step + self.ema_alpha = ema_alpha + self.sat_limit = sat_limit + self.p95_ema = None + + def step(self, raw, exp_raw): + p90, p95, sat = measure_raw_g_metrics(raw) + + # EMA do p95 (estabiliza) + if self.p95_ema is None: + self.p95_ema = p95 + else: + self.p95_ema = (1 - self.ema_alpha) * self.p95_ema + self.ema_alpha * p95 + + e = self.target - self.p95_ema # erro em nível de pixel + + # deadband: segura a mão perto do alvo + if abs(e) <= self.deadband and sat <= self.sat_limit: + return exp_raw, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "hold": True} + + # saturou: garante redução + if sat > self.sat_limit: + step = -min(self.max_step, 0.12) + else: + ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6) + step = self.k * math.log(ratio) + step = max(-self.max_step, min(self.max_step, step)) + + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = max(self.exp_min, min(self.exp_max, new_exp)) + + return new_exp, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "step": step, "hold": False} + +# ========================= +# STRUCTS + Param API +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(int(value)) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + +def overlay_hud(img_bgr, lines): + y = 28 + for s in lines: + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0,0,0), 3, cv2.LINE_AA) + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255,255,255), 2, cv2.LINE_AA) + y += 28 + +def save_sample(raw: np.ndarray, bgr_preview: np.ndarray, meta: dict): + name = ts_name() + raw_path = os.path.join(SESSION_DIR, f"{name}.raw") + png_path = os.path.join(SESSION_DIR, f"{name}.png") + json_path = os.path.join(SESSION_DIR, f"{name}.json") + + raw.tofile(raw_path) + cv2.imwrite(png_path, bgr_preview) + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return raw_path, png_path, json_path + +def main(): + # scan + n = C.c_ubyte(0) + ck(dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF), "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ck(dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF), "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + print("Saving to:", SESSION_DIR) + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + + # Estado local (não dependemos de GET) + exp_raw = 1500 + gain_a = 0 + gain_d = 0 + + # Aplica estado inicial + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + param_set_int(h, PARAM_ID_SENSOR_GAINANALOGRAW, gain_a) + param_set_int(h, PARAM_ID_SENSOR_GAINDIGITRAW, gain_d) + except Exception as e: + print("[WARN] Falhou set inicial:", e) + + ae = RobustAE(target_p95=140.0, deadband=6.0, k=0.12, max_step=0.10, ema_alpha=0.20, sat_limit=0.01) + ae_on = True + auto_save = False + last_auto_t = 0.0 + + # FPS + t0 = time.time() + frames = 0 + fps = 0.0 + + last_msg = "" + last_msg_t = 0.0 + + try: + while True: + raw = capture_raw8(h) + + # soft AE + ae_dbg = {} + if ae_on: + new_exp, ae_dbg = ae.step(raw, exp_raw) + if new_exp != exp_raw: + exp_raw = new_exp + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + # se set falhar, desliga AE pra não ficar insistindo + print("[ERR] set exposure:", e) + ae_on = False + + # preview RGB bonitão + rgb_clean = make_rgb_preview(raw, upscale=UPSCALE) + bgr = rgb_clean.copy() + + # FPS + frames += 1 + dt = time.time() - t0 + if dt >= 1.0: + fps = frames / dt + frames = 0 + t0 = time.time() + + # HUD + lines = [ + f"AE: {'ON' if ae_on else 'OFF'} | AutoSave: {'ON' if auto_save else 'OFF'} | Interval: {CAPTURE_INTERVAL_S:.1f}s", + f"exp_raw={exp_raw} gain_a={gain_a} gain_d={gain_d} | FPS={fps:.1f}", + f"AEdbg: p95={ae_dbg.get('p95_ema', ae_dbg.get('p95', 0)):.1f} sat={ae_dbg.get('sat', 0):.3f} hold={ae_dbg.get('hold', False)}", + "Keys: C/SPACE=save | A=toggle autosave | E=toggle AE | +/- exp | Q/ESC quit", + ] + overlay_hud(bgr, lines) + + # msg pós-save + if last_msg and (time.time() - last_msg_t) < 2.0: + cv2.putText(bgr, last_msg, (12, bgr.shape[0] - 18), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,0), 2, cv2.LINE_AA) + + cv2.imshow(WINDOW_NAME, bgr) + + # autosave + now = time.time() + if auto_save and (now - last_auto_t) >= CAPTURE_INTERVAL_S: + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "note": "autosave", + } + raw_path, png_path, json_path = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = now + last_auto_t = now + + k = cv2.waitKey(1) & 0xFF + if k in (ord('q'), ord('Q'), 27): + break + + elif k in (ord('a'), ord('A')): + auto_save = not auto_save + last_msg = f"AutoSave -> {'ON' if auto_save else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord('e'), ord('E')): + ae_on = not ae_on + last_msg = f"AE -> {'ON' if ae_on else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord('c'), ord('C'), 32): # C ou SPACE + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "note": "manual", + } + raw_path, png_path, json_path = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = time.time() + + elif k in (ord('+'), ord('=')): + exp_raw = clamp(exp_raw + 200, EXP_MIN, EXP_MAX) + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + print("[ERR] manual exp +:", e) + + elif k in (ord('-'), ord('_')): + exp_raw = clamp(exp_raw - 200, EXP_MIN, EXP_MAX) + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + print("[ERR] manual exp -:", e) + + finally: + try: + ret = dll.VT_DeviceClose(C.byref(h)) + if ret != 0: + print("VT_DeviceClose retornou:", ret) + except Exception as e: + print("Erro ao fechar:", e) + cv2.destroyAllWindows() + + print("Fim.") + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/dlls/HQVSDK.xml b/Python/gal5000/dlls/HQVSDK.xml new file mode 100644 index 000000000..bf94c7afd --- /dev/null +++ b/Python/gal5000/dlls/HQVSDK.xml @@ -0,0 +1,10 @@ + + + + 0 + 0 + 0 + 0 + 500 + + diff --git a/Python/gal5000/dlls/HQV_AVIProcess64.dll b/Python/gal5000/dlls/HQV_AVIProcess64.dll new file mode 100644 index 000000000..bf7c17703 --- /dev/null +++ b/Python/gal5000/dlls/HQV_AVIProcess64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7072895a3c25aaeb77bd81ce7982c9b06337180548283ef795a4b214b799c868 +size 34304 diff --git a/Python/gal5000/dlls/HQV_DataConvert64.dll b/Python/gal5000/dlls/HQV_DataConvert64.dll new file mode 100644 index 000000000..c51275eaf --- /dev/null +++ b/Python/gal5000/dlls/HQV_DataConvert64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96701a0bc3b7269dcd7e20f4e32c77f75660a77e8f4a6efe6fcbf56ce7f56254 +size 29184 diff --git a/Python/gal5000/dlls/HQV_FilePro64.dll b/Python/gal5000/dlls/HQV_FilePro64.dll new file mode 100644 index 000000000..e59747e7b --- /dev/null +++ b/Python/gal5000/dlls/HQV_FilePro64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:721fd6affb499453e5f8de47822e6466b31f18403fd1c53e5f04d12e9d253c07 +size 2792960 diff --git a/Python/gal5000/dlls/HQV_TinyXML64.dll b/Python/gal5000/dlls/HQV_TinyXML64.dll new file mode 100644 index 000000000..08e1ca567 --- /dev/null +++ b/Python/gal5000/dlls/HQV_TinyXML64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:916cfd492e4c82836cc6062097e487114853d8c7ce08275737013e69643d6cb1 +size 104960 diff --git a/Python/gal5000/dlls/HQV_USB64.dll b/Python/gal5000/dlls/HQV_USB64.dll new file mode 100644 index 000000000..bf1fc7dd7 --- /dev/null +++ b/Python/gal5000/dlls/HQV_USB64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e9b47e8cc0fd5dd6e568b6437b1d094cab80e9911f669384c9b0da740791ea9 +size 163840 diff --git a/Python/gal5000/dlls/HQV_USS64.dll b/Python/gal5000/dlls/HQV_USS64.dll new file mode 100644 index 000000000..24ac60e34 --- /dev/null +++ b/Python/gal5000/dlls/HQV_USS64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3d515cf79ab725bb094f7e2bd820a412efa1a0fdf46a46ef93763e673592ca7 +size 103424 diff --git a/Python/gal5000/dlls/KSJApi64.dll b/Python/gal5000/dlls/KSJApi64.dll new file mode 100644 index 000000000..bbd5dc7ed --- /dev/null +++ b/Python/gal5000/dlls/KSJApi64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a460745cecc5e219fbb8a7e446479f0b538f1ec485b32897278d493f0eb90ee0 +size 1052160 diff --git a/Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll b/Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll new file mode 100644 index 000000000..e4cfda0af --- /dev/null +++ b/Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e040a3012029b53aa347c59a88ccf0bb280f543772de7e12ccb37705c10f6dc2 +size 151552 diff --git a/Python/gal5000/dlls/VT_SDK64.dll b/Python/gal5000/dlls/VT_SDK64.dll new file mode 100644 index 000000000..46013c87d --- /dev/null +++ b/Python/gal5000/dlls/VT_SDK64.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3c70e2e5e7bf80dee62cb8b12c9ea33bf1d7a9d484da4c105dd7ee3b31468d2 +size 1286144 diff --git a/Python/gal5000/gal5000_test_params.py b/Python/gal5000/gal5000_test_params.py new file mode 100644 index 000000000..1167fcf12 --- /dev/null +++ b/Python/gal5000/gal5000_test_params.py @@ -0,0 +1,250 @@ +import math +import os +import time +import ctypes as C +from ctypes import wintypes as W + +import numpy as np +import cv2 + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 4CH Preview (E=AEC toggle, G=AGC toggle, Q=quit)" + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (como você já capturou) +RAW_W = 2592 +RAW_H = 2056 + +# Bayer+NIR pattern (2x2): +# R G +# IR B +# => R = [0::2,0::2], G=[0::2,1::2], IR=[1::2,0::2], B=[1::2,1::2] + +# ========================= +# PARAM IDs (VT_Param.h) +# ========================= +BUF_SIZE = 256 + +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_EXPOSUREAUTOENABLE = 0x00003011 +PARAM_ID_SENSOR_GAINANALOGAUTOENABLE = 0x00003021 +PARAM_ID_SENSOR_GAINANALOGAGCMAX = 0x00003022 +PARAM_ID_COMMON_DISPLAYFPS = 0x00000203 # float R (média efetiva) :contentReference[oaicite:9]{index=9} + + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + + +# ========================= +# STRUCTS (mínimo necessário) +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" # <- CORRETO: bytes (fica zerado / string vazia) + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +# Param API +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + + +def ck(ret: int, name: str): + if ret != 0: + print(f"{name} falhou, ret={ret}") + raise RuntimeError(f"{name} falhou, ret={ret}") + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(value) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + +def param_get_int(h: W.HANDLE, pid: int) -> int: + p = devparam_by_id(pid) + v = C.c_int(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return int(v.value) + +def param_get_float(h: W.HANDLE, pid: int) -> float: + p = devparam_by_id(pid) + v = C.c_float(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_FLOAT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return float(v.value) + +def set_bool(h: W.HANDLE, pid: int, enabled: bool): + param_set_int(h, pid, 1 if enabled else 0) # boolean no SDK é int 0/1 + +def param_supported(h, pid, vtype): + p = devparam_by_id(pid) + minv = C.c_int() + maxv = C.c_int() + inc = C.c_int() + ret = dll.VT_ParamGetRange(h, p, + C.byref(minv), + C.byref(maxv), + C.byref(inc), + vtype) + return ret == 0 + +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + if w != RAW_W or hh != RAW_H: + # Se em algum momento você mudar resolução/ROI, aqui te avisa. + print(f"[WARN] Res mudou: {w}x{hh} (esperado {RAW_W}x{RAW_H})") + + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + # garante reshape correto + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + +def make_rgb_preview(raw: np.ndarray, upscale=2): + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + # normalização leve só para display (p2-p98) + def norm8(x): + lo = np.percentile(x, 2) + hi = np.percentile(x, 98) + if hi <= lo + 1: + return x + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + Rn, Gn, Bn = map(norm8, [R, G, B]) + + rgb = np.dstack([Bn, Gn, Rn]) # OpenCV = BGR + if upscale and upscale != 1: + rgb = cv2.resize(rgb, (rgb.shape[1]*upscale, rgb.shape[0]*upscale), interpolation=cv2.INTER_NEAREST) + return rgb + + +def main(): + # scan + n = C.c_ubyte(0) + ret = dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF) + ck(ret, "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ret = dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF) + ck(ret, "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + + cv2.namedWindow("RGB", cv2.WINDOW_NORMAL) + + aec_on = False + agc_on = False + exp_raw = 1500 + gain_a = 0 + gain_d = 0 + + has_hw_aec = param_supported(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE, VALUE_INT) + has_hw_agc = param_supported(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE, VALUE_INT) + has_exp_raw = param_supported(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, VALUE_INT) + has_gain_a = param_supported(h, PARAM_ID_SENSOR_GAINANALOGRAW, VALUE_INT) + + if (has_hw_aec): + aec_on = bool(param_get_int(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE)) + if (has_hw_agc): + agc_on = bool(param_get_int(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE)) + if (has_exp_raw): + exp_raw = param_get_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW) + if (has_gain_a): + gain_a = param_get_int(h, PARAM_ID_SENSOR_GAINANALOGRAW) + + print(f"has_hw_aec: {has_hw_aec}, aec_on: {aec_on}\r\nhas_hw_agc: {has_hw_agc}, agc_on: {agc_on}\r\nhas_exp_raw: {has_exp_raw}, exp_raw: {exp_raw}\r\nhas_gain_a: {has_gain_a}, gain_a: {gain_a}") + + while True: + raw = capture_raw8(h) + rgb = make_rgb_preview(raw, upscale=2) + cv2.imshow("RGB", rgb) + + k = cv2.waitKey(1) & 0xFF + if k in (ord('q'), ord('Q'), 27): + break + elif k in (ord('e'), ord('E')): # exemplo: E alterna AEC do hardware + aec_on = not aec_on + set_bool(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE, aec_on) + print("AEC(hw) =", aec_on) + elif k in (ord('g'), ord('G')): # G alterna AGC do hardware + agc_on = not agc_on + set_bool(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE, agc_on) + print("AGC(hw) =", agc_on) + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/preview_4ch_autoexposure.py b/Python/gal5000/preview_4ch_autoexposure.py new file mode 100644 index 000000000..d05fe2ca1 --- /dev/null +++ b/Python/gal5000/preview_4ch_autoexposure.py @@ -0,0 +1,547 @@ +import math +import os +import time +import ctypes as C +from ctypes import wintypes as W + +import numpy as np +import cv2 + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 4CH Preview (E=AEC toggle, G=AGC toggle, Q=quit)" + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (como você já capturou) +RAW_W = 2592 +RAW_H = 2056 + +# Bayer+NIR pattern (2x2): +# R G +# IR B +# => R = [0::2,0::2], G=[0::2,1::2], IR=[1::2,0::2], B=[1::2,1::2] + +# ========================= +# PARAM IDs (VT_Param.h) +# ========================= +BUF_SIZE = 256 + +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_EXPOSUREAUTOENABLE = 0x00003011 +PARAM_ID_SENSOR_GAINANALOGAUTOENABLE = 0x00003021 +PARAM_ID_SENSOR_GAINANALOGAGCMAX = 0x00003022 +PARAM_ID_COMMON_DISPLAYFPS = 0x00000203 # float R (média efetiva) :contentReference[oaicite:9]{index=9} + + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + +# ========================= +# MANUAL EXPOSURE HOTKEYS +# ========================= +EXP_MIN = 1 +EXP_MAX = 20000 # ajuste depois conforme o sensor aceitar +EXP_STEP = 200 # passo “normal” +EXP_STEP_FAST = 1000 # passo “rápido” + +TARGET_P95 = 160.0 +SAT_LIMIT = 0.02 +K = 0.35 +MAX_STEP = 0.18 +GAIN_A_MIN, GAIN_A_MAX = 0, 255 # ajuste conforme seu sensor +GAIN_STEP = 2 + +def clamp(v, lo, hi): + return lo if v < lo else hi if v > hi else v + +def measure_raw_g_metrics(raw: np.ndarray): + """ + Mede brilho no canal G cru (8-bit) usando ROI (chão) e retorna: + - p90/p95 (brilho) + - sat (fração saturada) + """ + H, W = raw.shape[:2] + + # canal G cru (mesmo que você já usa em soft_ae_step) :contentReference[oaicite:1]{index=1} + G = raw[0::2, 1::2] # tamanho ~ H/2 x W/2 + + h2, w2 = G.shape + # ROI: base da imagem, cortando laterais + y0, y1 = int(h2 * 0.55), int(h2 * 0.95) + x0, x1 = int(w2 * 0.15), int(w2 * 0.85) + + roi = G[y0:y1, x0:x1] + + p90 = float(np.percentile(roi, 90)) + p95 = float(np.percentile(roi, 95)) + sat = float(np.mean(roi >= 250)) + return p90, p95, sat + +def measure_brightness_and_sat(img_bgr): + h, w = img_bgr.shape[:2] + y0, y1 = int(h * 0.55), int(h * 0.95) + x0, x1 = int(w * 0.15), int(w * 0.85) + + roi = img_bgr[y0:y1, x0:x1] + g = roi[:, :, 1].astype(np.uint8) + + p95 = float(np.percentile(g, 95)) + sat = float(np.mean(g >= 250)) + return p95, sat + +def auto_exposure_step(img_bgr, exp_raw, gain_a): + # mede + p95, sat = measure_brightness_and_sat(img_bgr) + + # se está saturando, reduz exposição com prioridade + if sat > SAT_LIMIT: + # força erro “negativo” + err = math.log((TARGET_P95 + 1e-6) / (p95 + 1e-6)) # pode ser positivo/negativo + err = min(err, -0.15) # garante redução + else: + err = math.log((TARGET_P95 + 1e-6) / (p95 + 1e-6)) + + # limita o tamanho do passo por iteração (evita oscilar) + step = clamp(K * err, -MAX_STEP, +MAX_STEP) + + # atualiza exposição (multiplicativo) + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = clamp(new_exp, EXP_MIN, EXP_MAX) + + # Ganho: só mexe se exposição já “bateu no teto/chão” + new_gain = gain_a + + if new_exp >= EXP_MAX and p95 < (TARGET_P95 * 0.85): + new_gain = clamp(gain_a + GAIN_STEP, GAIN_A_MIN, GAIN_A_MAX) + elif new_exp <= EXP_MIN and (p95 > (TARGET_P95 * 1.15) or sat > SAT_LIMIT): + new_gain = clamp(gain_a - GAIN_STEP, GAIN_A_MIN, GAIN_A_MAX) + + dbg = {"p95": p95, "sat": sat, "err": err, "step": step} + return new_exp, new_gain, dbg + +class RobustAE: + def __init__(self, + exp_min=1, exp_max=20000, + target_p95=140.0, + deadband=6.0, + k=0.12, + max_step=0.10, + ema_alpha=0.20, + sat_limit=0.01): + self.exp_min = exp_min + self.exp_max = exp_max + self.target = target_p95 + self.deadband = deadband + self.k = k + self.max_step = max_step + self.ema_alpha = ema_alpha + self.sat_limit = sat_limit + + self.p95_ema = None + + def step(self, raw, exp_raw): + p90, p95, sat = measure_raw_g_metrics(raw) + + # EMA do p95 pra tirar tremedeira + if self.p95_ema is None: + self.p95_ema = p95 + else: + self.p95_ema = (1 - self.ema_alpha) * self.p95_ema + self.ema_alpha * p95 + + e = self.target - self.p95_ema # erro em "nível de pixel" + + # deadband: se tá perto do alvo, NÃO mexe + if abs(e) <= self.deadband and sat <= self.sat_limit: + return exp_raw, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "hold": True} + + # Se saturou, força reduzir exposição + if sat > self.sat_limit: + # passo negativo garantido + step = -min(self.max_step, 0.12) + else: + # controle em log: step proporcional ao erro relativo + ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6) + step = self.k * math.log(ratio) + step = max(-self.max_step, min(self.max_step, step)) + + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = max(self.exp_min, min(self.exp_max, new_exp)) + + return new_exp, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "step": step, "hold": False} + + +# ========================= +# STRUCTS (mínimo necessário) +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" # <- CORRETO: bytes (fica zerado / string vazia) + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +# Param API +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + + +def ck(ret: int, name: str): + if ret != 0: + print(f"{name} falhou, ret={ret}") + raise RuntimeError(f"{name} falhou, ret={ret}") + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(value) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + +def param_get_int(h: W.HANDLE, pid: int) -> int: + p = devparam_by_id(pid) + v = C.c_int(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return int(v.value) + +def param_get_float(h: W.HANDLE, pid: int) -> float: + p = devparam_by_id(pid) + v = C.c_float(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_FLOAT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return float(v.value) + +def set_bool(h: W.HANDLE, pid: int, enabled: bool): + param_set_int(h, pid, 1 if enabled else 0) # boolean no SDK é int 0/1 + +def param_supported(h, pid, vtype): + p = devparam_by_id(pid) + minv = C.c_int() + maxv = C.c_int() + inc = C.c_int() + ret = dll.VT_ParamGetRange(h, p, + C.byref(minv), + C.byref(maxv), + C.byref(inc), + vtype) + return ret == 0 + +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + if w != RAW_W or hh != RAW_H: + # Se em algum momento você mudar resolução/ROI, aqui te avisa. + print(f"[WARN] Res mudou: {w}x{hh} (esperado {RAW_W}x{RAW_H})") + + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + # garante reshape correto + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + +def make_montage_4ch(raw: np.ndarray): + # pattern: + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + IR = raw[1::2, 0::2] + B = raw[1::2, 1::2] + + # para visual: normaliza levemente (só pra ficar agradável) + # sem mexer nos dados crus do treino, isso é só display. + def norm8(x): + # estica por percentil p2-p98 pra ver melhor em campo + lo = np.percentile(x, 2) + hi = np.percentile(x, 98) + if hi <= lo + 1: + return x + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + Rn, Gn, IRn, Bn = map(norm8, [R, G, IR, B]) + + top = np.hstack([Rn, Gn]) + bot = np.hstack([IRn, Bn]) + mont = np.vstack([top, bot]) + + # labels (coloca texto no montage) + mont_bgr = cv2.cvtColor(mont, cv2.COLOR_GRAY2BGR) + + h2, w2 = Rn.shape # cada plane é H/2 x W/2 + # posições de texto + cv2.putText(mont_bgr, "R", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "G", (w2 + 10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "IR", (10, h2 + 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "B", (w2 + 10, h2 + 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + + return mont_bgr + +def make_rgb_preview(raw: np.ndarray, upscale=2): + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + # normalização leve só para display (p2-p98) + def norm8(x): + lo = np.percentile(x, 2) + hi = np.percentile(x, 98) + if hi <= lo + 1: + return x + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + Rn, Gn, Bn = map(norm8, [R, G, B]) + + rgb = np.dstack([Bn, Gn, Rn]) # OpenCV = BGR + if upscale and upscale != 1: + rgb = cv2.resize(rgb, (rgb.shape[1]*upscale, rgb.shape[0]*upscale), interpolation=cv2.INTER_NEAREST) + return rgb + +def overlay_hud(img, aec_on, agc_on, exp_raw, gain_a, gain_d, fps): + lines = [ + f"AEC: {'ON' if aec_on else 'OFF'} | AGC: {'ON' if agc_on else 'OFF'}", + f"ExposureRaw: {exp_raw}", + f"Gain A: {gain_a} | Gain D: {gain_d}", + f"FPS: {fps:.1f}", + "Keys: + - [ ] | A=AE | Q=quit", + ] + + y = 35 + for s in lines: + draw_text(img, s, (12, y), scale=0.85) + y += 32 + +def draw_text(img, text, pos, scale=0.8): + x, y = pos + # sombra + cv2.putText(img, text, (x+2, y+2), + cv2.FONT_HERSHEY_SIMPLEX, scale, + (0, 0, 0), 3, cv2.LINE_AA) + # texto principal + cv2.putText(img, text, (x, y), + cv2.FONT_HERSHEY_SIMPLEX, scale, + (255, 255, 255), 2, cv2.LINE_AA) + +def main(): + # scan + n = C.c_ubyte(0) + ret = dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF) + ck(ret, "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ret = dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF) + ck(ret, "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + cv2.namedWindow("RGB", cv2.WINDOW_NORMAL) + show_rgb = True + + t0 = time.time() + frames = 0 + fps = 0.0 + + ae = RobustAE(exp_min=EXP_MIN, exp_max=EXP_MAX, target_p95=140.0) + ae_every_n = 4 + ae_i = 0 + aec_on = False + agc_on = False + exp_raw = 1500 # valor inicial que você escolhe + gain_a = 0 + gain_d = 0 + + has_exp_raw = param_supported(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, VALUE_INT) + has_gain_a = param_supported(h, PARAM_ID_SENSOR_GAINANALOGRAW, VALUE_INT) + has_hw_aec = param_supported(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE, VALUE_INT) + has_hw_agc = param_supported(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE, VALUE_INT) + + if (has_hw_aec): + aec_on = bool(param_get_int(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE)) + if (has_hw_agc): + agc_on = bool(param_get_int(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE)) + if (has_exp_raw): + exp_raw = param_get_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW) + if (has_gain_a): + gain_a = param_get_int(h, PARAM_ID_SENSOR_GAINANALOGRAW) + + try: + def set_exposure_manual(new_exp: int): + new_exp = int(max(EXP_MIN, min(EXP_MAX, new_exp))) + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, new_exp) + return new_exp + + while True: + raw = capture_raw8(h) + + if aec_on: + ae_i += 1 + if ae_i % ae_every_n == 0: + new_exp, dbg = ae.step(raw, exp_raw) + if new_exp != exp_raw: + exp_raw = set_exposure_manual(new_exp) + # debug opcional: + # print(f"[AE] p95={dbg['p95']:.1f} ema={dbg['p95_ema']:.1f} sat={dbg['sat']*100:.2f}% exp={exp_raw} hold={dbg.get('hold')}") + + montage = make_montage_4ch(raw) + + # AUTO-EXPOSURE (sem GET) + if aec_on: + ae_i += 1 + if ae_i % ae_every_n == 0: + try: + new_exp, new_gain_a, dbg = auto_exposure_step(montage, exp_raw, gain_a) + + if new_exp != exp_raw: + exp_raw = set_exposure_manual(new_exp) + + # Se você quiser mexer em ganho também: + if new_gain_a != gain_a: + param_set_int(h, PARAM_ID_SENSOR_GAINANALOGRAW, int(new_gain_a)) + gain_a = int(new_gain_a) + + # debug opcional + # print(f"[AE] p95={dbg['p95']:.1f} sat={dbg['sat']*100:.2f}% exp={exp_raw} gainA={gain_a}") + except Exception as e: + print("[AE] erro:", e) + + frames += 1 + dt = time.time() - t0 + if dt >= 1.0: + fps = frames / dt + frames = 0 + t0 = time.time() + + overlay_hud(montage, aec_on, agc_on, exp_raw, gain_a, gain_d, fps) + cv2.imshow(WINDOW_NAME, montage) + if show_rgb: + rgb = make_rgb_preview(raw, upscale=2) + cv2.imshow("RGB", rgb) + + k = cv2.waitKey(1) & 0xFF + if k in (ord('q'), ord('Q'), 27): + break + + elif k in (ord('e'), ord('E')): # exemplo: E alterna AEC do hardware + aec_on = not aec_on + set_bool(h, PARAM_ID_SENSOR_EXPOSUREAUTOENABLE, aec_on) + print("AEC(hw) =", aec_on) + + elif k in (ord('g'), ord('G')): # G alterna AGC do hardware + agc_on = not agc_on + set_bool(h, PARAM_ID_SENSOR_GAINANALOGAUTOENABLE, agc_on) + print("AGC(hw) =", agc_on) + + elif k in (ord('v'), ord('V')): + show_rgb = not show_rgb + if not show_rgb: + cv2.destroyWindow("RGB") + else: + cv2.namedWindow("RGB", cv2.WINDOW_NORMAL) + + elif aec_on == False: + if k in (ord('+'), ord('=')): # '=' costuma ser '+' sem shift em alguns teclados + try: + exp_raw = set_exposure_manual(exp_raw + EXP_STEP) + print(f"[MANUAL] ExposureRaw -> {exp_raw}") + except Exception as e: + print("[ERR] manual exp +:", e) + + elif k in (ord('-'), ord('_')): + try: + exp_raw = set_exposure_manual(exp_raw - EXP_STEP) + print(f"[MANUAL] ExposureRaw -> {exp_raw}") + except Exception as e: + print("[ERR] manual exp -:", e) + + elif k == ord(']'): # fast + + try: + exp_raw = set_exposure_manual(exp_raw + EXP_STEP_FAST) + print(f"[MANUAL] ExposureRaw (fast) -> {exp_raw}") + except Exception as e: + print("[ERR] manual exp fast +:", e) + + elif k == ord('['): # fast - + try: + exp_raw = set_exposure_manual(exp_raw - EXP_STEP_FAST) + print(f"[MANUAL] ExposureRaw (fast) -> {exp_raw}") + except Exception as e: + print("[ERR] manual exp fast -:", e) + + finally: + try: + ret = dll.VT_DeviceClose(C.byref(h)) + if ret != 0: + print("VT_DeviceClose retornou:", ret) + except Exception as e: + print("Erro ao fechar:", e) + cv2.destroyAllWindows() + + print("Fim.") + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/preview_auto_exposure.py b/Python/gal5000/preview_auto_exposure.py new file mode 100644 index 000000000..7a075a96a --- /dev/null +++ b/Python/gal5000/preview_auto_exposure.py @@ -0,0 +1,560 @@ +import os +import time +import math +import ctypes as C +from ctypes import wintypes as W + +import numpy as np +import cv2 + +# ========================= +# CONFIG +# ========================= + +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 4CH Preview (A=AE, Q=quit)" + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (já conhecido da GAL5000) +RAW_W = 2592 +RAW_H = 2056 + +# PARAM IDs (apenas os que sabemos que existem) +BUF_SIZE = 256 + +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A + +PARAM_ID_SFNC_SENSORWIDTH = 0x00001101 +PARAM_ID_SFNC_SENSORHEIGHT = 0x00001102 +PARAM_ID_SFNC_WIDTHMAX = 0x00001106 +PARAM_ID_SFNC_HEIGHTMAX = 0x00001107 +PARAM_ID_SFNC_WIDTH = 0x00001111 +PARAM_ID_SFNC_HEIGHT = 0x00001112 +PARAM_ID_SFNC_OFFSETX = 0x00001113 +PARAM_ID_SFNC_OFFSETY = 0x00001114 +PARAM_ID_SFNC_EXPOSURETIME = 0x0000121A # pode ou não refletir algo útil + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + +# ========================= +# LIMITES / HOTKEYS +# ========================= + +# Exposição em unidades RAW (linhas) +EXP_MIN = 1 +EXP_MAX = 20000 # ajusta depois se ver que a câmera aceita mais/menos +EXP_STEP = 200 # passo “normal” (+/-) +EXP_STEP_FAST = 1000 # passo rápido ([ ]) + +# Ganho analógico +GAIN_A_MIN = 0 +GAIN_A_MAX = 255 +GAIN_A_STEP = 2 + +# Ganho digital +GAIN_D_MIN = 0 +GAIN_D_MAX = 8 # chute conservador; hoje está em 2 +GAIN_D_STEP = 1 + +# ROI para análise (chão) +ROI_Y0_FRAC = 0.55 +ROI_Y1_FRAC = 0.95 +ROI_X0_FRAC = 0.15 +ROI_X1_FRAC = 0.85 + +# Alvo de brilho / saturação +TARGET_P95 = 140.0 # alvo de brilho (0..255) +DEADBAND = 6.0 # zona morta em torno do alvo +SAT_LIMIT = 0.02 # fração máxima de pixels saturados (2%) + +# Controle log / suavização +K_LOG = 0.12 # ganho do controlador em log +MAX_STEP = 0.10 # limite do passo (em espaço log) por iteração +EMA_ALPHA = 0.20 # suavização do p95 + +def clamp(v, lo, hi): + return lo if v < lo else hi if v > hi else v + +# ========================= +# STRUCTS +# ========================= + +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= + +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + +def ck(ret: int, name: str): + if ret != 0: + raise RuntimeError(f"{name} falhou, ret={ret}") + +def param_get_int(h: W.HANDLE, pid: int) -> int: + p = devparam_by_id(pid) + v = C.c_int(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return int(v.value) + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(int(value)) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + +def param_get_float(h: W.HANDLE, pid: int) -> float: + p = devparam_by_id(pid) + v = C.c_float(0.0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_FLOAT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return float(v.value) + +# ========================= +# CAPTURA / VISUALIZAÇÃO +# ========================= + +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + if w != RAW_W or hh != RAW_H: + print(f"[WARN] Res mudou: {w}x{hh} (esperado {RAW_W}x{RAW_H})") + + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + +def make_montage_4ch(raw: np.ndarray) -> np.ndarray: + # Bayer+NIR pattern: + # R G + # IR B + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + IR = raw[1::2, 0::2] + B = raw[1::2, 1::2] + + def norm8(x): + lo = np.percentile(x, 2) + hi = np.percentile(x, 98) + if hi <= lo + 1: + return x + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + Rn, Gn, IRn, Bn = map(norm8, [R, G, IR, B]) + + top = np.hstack([Rn, Gn]) + bot = np.hstack([IRn, Bn]) + mont = np.vstack([top, bot]) + + mont_bgr = cv2.cvtColor(mont, cv2.COLOR_GRAY2BGR) + + h2, w2 = Rn.shape + cv2.putText(mont_bgr, "R", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "G", (w2 + 10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "IR", (10, h2 + 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + cv2.putText(mont_bgr, "B", (w2 + 10, h2 + 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA) + + return mont_bgr + +def make_rgb_preview(raw: np.ndarray, upscale=2) -> np.ndarray: + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + def norm8(x): + lo = np.percentile(x, 2) + hi = np.percentile(x, 98) + if hi <= lo + 1: + return x + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + + Rn, Gn, Bn = map(norm8, [R, G, B]) + rgb = np.dstack([Bn, Gn, Rn]) # OpenCV = BGR + + if upscale and upscale != 1: + rgb = cv2.resize(rgb, (rgb.shape[1]*upscale, rgb.shape[0]*upscale), + interpolation=cv2.INTER_NEAREST) + return rgb + +def draw_text(img, text, pos, scale=0.8): + x, y = pos + cv2.putText(img, text, (x+2, y+2), + cv2.FONT_HERSHEY_SIMPLEX, scale, + (0, 0, 0), 3, cv2.LINE_AA) + cv2.putText(img, text, (x, y), + cv2.FONT_HERSHEY_SIMPLEX, scale, + (255, 255, 255), 2, cv2.LINE_AA) + +def overlay_hud(img, ae_enabled, exp_raw, gain_a, gain_d, fps, dbg): + p95 = dbg.get("p95", None) + sat = dbg.get("sat", None) + + lines = [ + f"AE: {'ON' if ae_enabled else 'OFF'}", + f"ExposureRaw: {exp_raw}", + f"Gain A: {gain_a} | Gain D: {gain_d}", + f"FPS: {fps:.1f}", + ] + if p95 is not None and sat is not None: + lines.append(f"p95: {p95:.1f} | sat: {sat*100:.2f}%") + lines.append("Keys: A=AE +/- / [ ] exp Z/X gainA C/V gainD Q=quit") + + y = 30 + for s in lines: + draw_text(img, s, (12, y), scale=0.80) + y += 26 + +# ========================= +# AE CONTROLLER (software) +# ========================= + +def measure_raw_g_metrics(raw: np.ndarray): + """ + Mede p90/p95/saturação no canal G cru (8-bit), + usando apenas uma ROI voltada ao chão. + """ + H, W = raw.shape[:2] + + # canal G cru (Bayer layout R/G/IR/B): + G = raw[0::2, 1::2] # ~ H/2 x W/2 + + h2, w2 = G.shape + y0 = int(h2 * ROI_Y0_FRAC) + y1 = int(h2 * ROI_Y1_FRAC) + x0 = int(w2 * ROI_X0_FRAC) + x1 = int(w2 * ROI_X1_FRAC) + + roi = G[y0:y1, x0:x1] + + p90 = float(np.percentile(roi, 90)) + p95 = float(np.percentile(roi, 95)) + sat = float(np.mean(roi >= 250)) + return p90, p95, sat + +class AEController: + def __init__(self, + exp_min=EXP_MIN, + exp_max=EXP_MAX, + target_p95=TARGET_P95, + deadband=DEADBAND, + k=K_LOG, + max_step=MAX_STEP, + ema_alpha=EMA_ALPHA, + sat_limit=SAT_LIMIT, + use_gain=True): + self.exp_min = exp_min + self.exp_max = exp_max + self.target = target_p95 + self.deadband = deadband + self.k = k + self.max_step = max_step + self.ema_alpha = ema_alpha + self.sat_limit = sat_limit + self.use_gain = use_gain + + self.p95_ema = None + + def step(self, raw: np.ndarray, exp_raw: int, + gain_a: int, gain_d: int): + """ + Retorna (new_exp, new_gain_a, new_gain_d, dbg) + """ + p90, p95, sat = measure_raw_g_metrics(raw) + + # EMA do p95 + if self.p95_ema is None: + self.p95_ema = p95 + else: + self.p95_ema = (1.0 - self.ema_alpha) * self.p95_ema + self.ema_alpha * p95 + + e = self.target - self.p95_ema + + # deadband: se está perto do alvo e não saturando, não mexe + if abs(e) <= self.deadband and sat <= self.sat_limit: + dbg = { + "p90": p90, + "p95": p95, + "p95_ema": self.p95_ema, + "sat": sat, + "step": 0.0, + "hold": True + } + return exp_raw, gain_a, gain_d, dbg + + # cálculo do step em log + if sat > self.sat_limit: + # saturou: garante um passo negativo mínimo + step = -min(self.max_step, 0.12) + else: + ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6) + step = self.k * math.log(ratio) + step = clamp(step, -self.max_step, +self.max_step) + + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = clamp(new_exp, self.exp_min, self.exp_max) + + new_gain_a = gain_a + new_gain_d = gain_d + + if self.use_gain: + # se exposição chegou no teto e ainda está escuro, sobe ganho analógico + if new_exp >= self.exp_max and self.p95_ema < (self.target - self.deadband): + new_gain_a = clamp(gain_a + GAIN_A_STEP, GAIN_A_MIN, GAIN_A_MAX) + + # se exposição chegou no chão e está muito claro/saturando, baixa ganho analógico + if new_exp <= self.exp_min and (self.p95_ema > (self.target + self.deadband) or sat > self.sat_limit): + new_gain_a = clamp(gain_a - GAIN_A_STEP, GAIN_A_MIN, GAIN_A_MAX) + + dbg = { + "p90": p90, + "p95": p95, + "p95_ema": self.p95_ema, + "sat": sat, + "step": step, + "hold": False + } + return new_exp, new_gain_a, new_gain_d, dbg + +# ========================= +# MAIN +# ========================= + +def main(): + # scan + n = C.c_ubyte(0) + ret = dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF) + ck(ret, "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ret = dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF) + ck(ret, "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + + # Info básica do sensor / ROI (opcional, mas útil pra log) + try: + sensor_w = param_get_int(h, PARAM_ID_SFNC_SENSORWIDTH) + sensor_h = param_get_int(h, PARAM_ID_SFNC_SENSORHEIGHT) + width_max = param_get_int(h, PARAM_ID_SFNC_WIDTHMAX) + height_max= param_get_int(h, PARAM_ID_SFNC_HEIGHTMAX) + roi_w = param_get_int(h, PARAM_ID_SFNC_WIDTH) + roi_h = param_get_int(h, PARAM_ID_SFNC_HEIGHT) + roi_x = param_get_int(h, PARAM_ID_SFNC_OFFSETX) + roi_y = param_get_int(h, PARAM_ID_SFNC_OFFSETY) + print(f"[CAM] sensor={sensor_w}x{sensor_h} roi={roi_w}x{roi_h}+{roi_x},{roi_y} max={width_max}x{height_max}") + except Exception as e: + print("[CAM] Não foi possível ler info SFNC:", e) + + # ler exp/gains atuais + try: + exp_raw = param_get_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW) + except Exception: + exp_raw = 1500 + + try: + gain_a = param_get_int(h, PARAM_ID_SENSOR_GAINANALOGRAW) + except Exception: + gain_a = 0 + + try: + gain_d = param_get_int(h, PARAM_ID_SENSOR_GAINDIGITRAW) + except Exception: + gain_d = 0 + + print(f"[INIT] exp_raw={exp_raw} gainA={gain_a} gainD={gain_d}") + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + cv2.namedWindow("RGB", cv2.WINDOW_NORMAL) + show_rgb = True + + t0 = time.time() + frames = 0 + fps = 0.0 + + ae = AEController() + ae_enabled = True + dbg_last = {} + + def set_exposure(new_exp: int) -> int: + new_exp = clamp(int(new_exp), EXP_MIN, EXP_MAX) + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, new_exp) + return new_exp + + def set_gain_a(new_gain: int) -> int: + new_gain = clamp(int(new_gain), GAIN_A_MIN, GAIN_A_MAX) + param_set_int(h, PARAM_ID_SENSOR_GAINANALOGRAW, new_gain) + return new_gain + + def set_gain_d(new_gain: int) -> int: + new_gain = clamp(int(new_gain), GAIN_D_MIN, GAIN_D_MAX) + param_set_int(h, PARAM_ID_SENSOR_GAINDIGITRAW, new_gain) + return new_gain + + try: + while True: + raw = capture_raw8(h) + + # Auto-exposure em software + if ae_enabled: + new_exp, new_gain_a, new_gain_d, dbg = ae.step(raw, exp_raw, gain_a, gain_d) + + if new_exp != exp_raw: + exp_raw = set_exposure(new_exp) + + if new_gain_a != gain_a and False: + gain_a = set_gain_a(new_gain_a) + + if new_gain_d != gain_d: + gain_d = set_gain_d(new_gain_d) + + dbg_last = dbg + else: + dbg_last = {} + + montage = make_montage_4ch(raw) + + # FPS calculado + frames += 1 + dt = time.time() - t0 + if dt >= 1.0: + fps = frames / dt + frames = 0 + t0 = time.time() + + overlay_hud(montage, ae_enabled, exp_raw, gain_a, gain_d, fps, dbg_last) + cv2.imshow(WINDOW_NAME, montage) + + if show_rgb: + rgb = make_rgb_preview(raw, upscale=2) + cv2.imshow("RGB", rgb) + + k = cv2.waitKey(1) & 0xFF + + if k in (ord('q'), ord('Q'), 27): + break + + elif k in (ord('a'), ord('A')): + ae_enabled = not ae_enabled + print("AE (software) =", ae_enabled) + + elif k in (ord('m'), ord('M')): + show_rgb = not show_rgb + if not show_rgb: + cv2.destroyWindow("RGB") + else: + cv2.namedWindow("RGB", cv2.WINDOW_NORMAL) + + # Controles manuais só quando AE está desligado + elif not ae_enabled: + if k in (ord('+'), ord('=')): + exp_raw = set_exposure(exp_raw + EXP_STEP) + print(f"[MANUAL] ExposureRaw -> {exp_raw}") + elif k in (ord('-'), ord('_')): + exp_raw = set_exposure(exp_raw - EXP_STEP) + print(f"[MANUAL] ExposureRaw -> {exp_raw}") + elif k == ord(']'): + exp_raw = set_exposure(exp_raw + EXP_STEP_FAST) + print(f"[MANUAL] ExposureRaw (fast) -> {exp_raw}") + elif k == ord('['): + exp_raw = set_exposure(exp_raw - EXP_STEP_FAST) + print(f"[MANUAL] ExposureRaw (fast) -> {exp_raw}") + elif k in (ord('z'), ord('Z')): + gain_a = set_gain_a(gain_a - GAIN_A_STEP) + print(f"[MANUAL] GainA -> {gain_a}") + elif k in (ord('x'), ord('X')): + gain_a = set_gain_a(gain_a + GAIN_A_STEP) + print(f"[MANUAL] GainA -> {gain_a}") + elif k in (ord('c'), ord('C')): + gain_d = set_gain_d(gain_d - GAIN_D_STEP) + print(f"[MANUAL] GainD -> {gain_d}") + elif k in (ord('v'), ord('V')): + gain_d = set_gain_d(gain_d + GAIN_D_STEP) + print(f"[MANUAL] GainD -> {gain_d}") + else: + pass + + finally: + try: + ret = dll.VT_DeviceClose(C.byref(h)) + if ret != 0: + print("VT_DeviceClose retornou:", ret) + except Exception as e: + print("Erro ao fechar:", e) + cv2.destroyAllWindows() + print("Fim.") + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/probe_params.py b/Python/gal5000/probe_params.py new file mode 100644 index 000000000..b52000a33 --- /dev/null +++ b/Python/gal5000/probe_params.py @@ -0,0 +1,342 @@ +import os +import ctypes as C +from ctypes import wintypes as W + + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 4CH Preview (E=AEC toggle, G=AGC toggle, Q=quit)" + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (como você já capturou) +RAW_W = 2592 +RAW_H = 2056 + +# Bayer+NIR pattern (2x2): +# R G +# IR B +# => R = [0::2,0::2], G=[0::2,1::2], IR=[1::2,0::2], B=[1::2,1::2] + +# ========================= +# PARAM IDs (VT_Param.h) +# ========================= +BUF_SIZE = 256 + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + + +# ========================= +# PARAM IDs (recorte útil do VT_Param.h) +# ========================= + +# COMMON / Display interno +PARAM_ID_COMMON_DISPLAYENABLE = 0x00000200 +PARAM_ID_COMMON_DISPLAYHWND = 0x00000201 +PARAM_ID_COMMON_DISPLAYVSYNC = 0x00000202 +PARAM_ID_COMMON_DISPLAYFPS = 0x00000203 +PARAM_ID_COMMON_DISPLAYWIDTH = 0x00000204 +PARAM_ID_COMMON_DISPLAYHEIGHT = 0x00000205 +PARAM_ID_COMMON_DISPLAYPOSX = 0x00000206 +PARAM_ID_COMMON_DISPLAYPOSY = 0x00000207 + +# SFNC Device info +PARAM_ID_SFNC_DEVICETYPE = 0x00001001 +PARAM_ID_SFNC_DEVICESCANTYPE = 0x00001002 +PARAM_ID_SFNC_DEVICEVENDORNAME = 0x00001003 +PARAM_ID_SFNC_DEVICEMODELNAME = 0x00001004 +PARAM_ID_SFNC_DEVICEFAMILYNAME = 0x00001005 +PARAM_ID_SFNC_DEVICEMANUFACTURERINFO = 0x00001006 +PARAM_ID_SFNC_DEVICEVERSION = 0x00001007 +PARAM_ID_SFNC_DEVICEFIRMWAREVERSION = 0x00001008 +PARAM_ID_SFNC_DEVICESERIALNUMBER = 0x00001009 +PARAM_ID_SFNC_DEVICEUSERID = 0x0000100B + +# SFNC image format / ROI +PARAM_ID_SFNC_SENSORWIDTH = 0x00001101 +PARAM_ID_SFNC_SENSORHEIGHT = 0x00001102 +PARAM_ID_SFNC_WIDTHMAX = 0x00001106 +PARAM_ID_SFNC_HEIGHTMAX = 0x00001107 +PARAM_ID_SFNC_WIDTH = 0x00001111 +PARAM_ID_SFNC_HEIGHT = 0x00001112 +PARAM_ID_SFNC_OFFSETX = 0x00001113 +PARAM_ID_SFNC_OFFSETY = 0x00001114 + +# SFNC Acquisition / Trigger / Exposure +PARAM_ID_SFNC_ACQUISITIONFRAMERATEENABLE = 0x00001209 +PARAM_ID_SFNC_ACQUISITIONLINERATE = 0x0000120A +PARAM_ID_SFNC_ACQUISITIONLINERATEENABLE = 0x0000120B +PARAM_ID_SFNC_TRIGGERSELECTOR = 0x0000120E +PARAM_ID_SFNC_TRIGGERMODE = 0x0000120F +PARAM_ID_SFNC_TRIGGERSOFTWARE = 0x00001210 +PARAM_ID_SFNC_TRIGGERSOURCE = 0x00001211 +PARAM_ID_SFNC_TRIGGERACTIVATION = 0x00001212 +PARAM_ID_SFNC_TRIGGERDELAY = 0x00001214 +PARAM_ID_SFNC_TRIGGERDIVIDER = 0x00001215 +PARAM_ID_SFNC_TRIGGERMULTIPLIER = 0x00001216 +PARAM_ID_SFNC_EXPOSUREMODE = 0x00001217 +PARAM_ID_SFNC_EXPOSURETIMEMODE = 0x00001218 +PARAM_ID_SFNC_EXPOSURETIMESELECTOR = 0x00001219 +PARAM_ID_SFNC_EXPOSURETIME = 0x0000121A +PARAM_ID_SFNC_EXPOSUREAUTO = 0x0000121B + +# SENSOR (ROI + Exposure/Gain, etc) +PARAM_ID_SENSOR_OFFSETV = 0x00003001 +PARAM_ID_SENSOR_WIDTH = 0x00003002 +PARAM_ID_SENSOR_HEIGHT = 0x00003003 +PARAM_ID_SENSOR_BLANKINGH = 0x00003004 +PARAM_ID_SENSOR_BLANKINGV = 0x00003005 +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_EXPOSUREAUTOENABLE = 0x00003011 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_GAINANALOGAUTOENABLE = 0x00003021 +PARAM_ID_SENSOR_GAINANALOGAGCMAX = 0x00003022 +PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A +PARAM_ID_SENSOR_GAINDIGITAUTOENABLE = 0x0000302B +PARAM_ID_SENSOR_GAINDIGITAGCMAX = 0x0000302C +PARAM_ID_SENSOR_BITLUT = 0x00003040 + +# IPU (Image Process Unit) +PARAM_ID_IPU_BRIGHTNESS = 0x00003100 +PARAM_ID_IPU_CONTRAST = 0x00003101 + +# GRAB (imagem / stream) +PARAM_ID_GRAB_FRAMERATE = 0x00005001 + +# ========================= +# TABELA DE PROBE +# (nome, id, tipo do VALUE_*) +# ========================= + +PARAMS_TO_PROBE = [ + # COMMON display + ("COMMON_DISPLAYENABLE", PARAM_ID_COMMON_DISPLAYENABLE, VALUE_INT), + ("COMMON_DISPLAYVSYNC", PARAM_ID_COMMON_DISPLAYVSYNC, VALUE_INT), + ("COMMON_DISPLAYFPS", PARAM_ID_COMMON_DISPLAYFPS, VALUE_FLOAT), + ("COMMON_DISPLAYWIDTH", PARAM_ID_COMMON_DISPLAYWIDTH, VALUE_INT), + ("COMMON_DISPLAYHEIGHT", PARAM_ID_COMMON_DISPLAYHEIGHT, VALUE_INT), + ("COMMON_DISPLAYPOSX", PARAM_ID_COMMON_DISPLAYPOSX, VALUE_INT), + ("COMMON_DISPLAYPOSY", PARAM_ID_COMMON_DISPLAYPOSY, VALUE_INT), + + # Device info + ("SFNC_DEVICETYPE", PARAM_ID_SFNC_DEVICETYPE, VALUE_INT), + ("SFNC_DEVICESCANTYPE", PARAM_ID_SFNC_DEVICESCANTYPE, VALUE_INT), + ("SFNC_DEVICEVENDORNAME", PARAM_ID_SFNC_DEVICEVENDORNAME, VALUE_STR), + ("SFNC_DEVICEMODELNAME", PARAM_ID_SFNC_DEVICEMODELNAME, VALUE_STR), + ("SFNC_DEVICEFAMILYNAME", PARAM_ID_SFNC_DEVICEFAMILYNAME, VALUE_STR), + ("SFNC_DEVICEVERSION", PARAM_ID_SFNC_DEVICEVERSION, VALUE_STR), + ("SFNC_DEVICEFIRMWAREVERSION",PARAM_ID_SFNC_DEVICEFIRMWAREVERSION, VALUE_STR), + ("SFNC_DEVICESERIALNUMBER", PARAM_ID_SFNC_DEVICESERIALNUMBER, VALUE_STR), + ("SFNC_DEVICEUSERID", PARAM_ID_SFNC_DEVICEUSERID, VALUE_STR), + + # SFNC ROI / image + ("SFNC_SENSORWIDTH", PARAM_ID_SFNC_SENSORWIDTH, VALUE_INT), + ("SFNC_SENSORHEIGHT", PARAM_ID_SFNC_SENSORHEIGHT, VALUE_INT), + ("SFNC_WIDTHMAX", PARAM_ID_SFNC_WIDTHMAX, VALUE_INT), + ("SFNC_HEIGHTMAX", PARAM_ID_SFNC_HEIGHTMAX, VALUE_INT), + ("SFNC_WIDTH", PARAM_ID_SFNC_WIDTH, VALUE_INT), + ("SFNC_HEIGHT", PARAM_ID_SFNC_HEIGHT, VALUE_INT), + ("SFNC_OFFSETX", PARAM_ID_SFNC_OFFSETX, VALUE_INT), + ("SFNC_OFFSETY", PARAM_ID_SFNC_OFFSETY, VALUE_INT), + + # SFNC acquisition / trigger / exposure + ("SFNC_ACQFRAMERATEENABLE", PARAM_ID_SFNC_ACQUISITIONFRAMERATEENABLE, VALUE_INT), + ("SFNC_ACQLINERATE", PARAM_ID_SFNC_ACQUISITIONLINERATE, VALUE_FLOAT), + ("SFNC_ACQLINERATEENABLE", PARAM_ID_SFNC_ACQUISITIONLINERATEENABLE, VALUE_INT), + ("SFNC_TRIGGERSELECTOR", PARAM_ID_SFNC_TRIGGERSELECTOR, VALUE_INT), + ("SFNC_TRIGGERMODE", PARAM_ID_SFNC_TRIGGERMODE, VALUE_INT), + ("SFNC_TRIGGERSOFTWARE", PARAM_ID_SFNC_TRIGGERSOFTWARE, VALUE_INT), + ("SFNC_TRIGGERSOURCE", PARAM_ID_SFNC_TRIGGERSOURCE, VALUE_INT), + ("SFNC_TRIGGERACTIVATION", PARAM_ID_SFNC_TRIGGERACTIVATION, VALUE_INT), + ("SFNC_TRIGGERDELAY", PARAM_ID_SFNC_TRIGGERDELAY, VALUE_FLOAT), + ("SFNC_TRIGGERDIVIDER", PARAM_ID_SFNC_TRIGGERDIVIDER, VALUE_INT), + ("SFNC_TRIGGERMULTIPLIER", PARAM_ID_SFNC_TRIGGERMULTIPLIER, VALUE_INT), + ("SFNC_EXPOSUREMODE", PARAM_ID_SFNC_EXPOSUREMODE, VALUE_INT), + ("SFNC_EXPOSURETIMEMODE", PARAM_ID_SFNC_EXPOSURETIMEMODE, VALUE_INT), + ("SFNC_EXPOSURETIMESELECTOR",PARAM_ID_SFNC_EXPOSURETIMESELECTOR, VALUE_INT), + ("SFNC_EXPOSURETIME", PARAM_ID_SFNC_EXPOSURETIME, VALUE_FLOAT), + ("SFNC_EXPOSUREAUTO", PARAM_ID_SFNC_EXPOSUREAUTO, VALUE_INT), + + # SENSOR ROI + gains + ("SENSOR_OFFSETV", PARAM_ID_SENSOR_OFFSETV, VALUE_INT), + ("SENSOR_WIDTH", PARAM_ID_SENSOR_WIDTH, VALUE_INT), + ("SENSOR_HEIGHT", PARAM_ID_SENSOR_HEIGHT, VALUE_INT), + ("SENSOR_BLANKINGH", PARAM_ID_SENSOR_BLANKINGH, VALUE_INT), + ("SENSOR_BLANKINGV", PARAM_ID_SENSOR_BLANKINGV, VALUE_INT), + ("SENSOR_EXPOSURETIMERAW", PARAM_ID_SENSOR_EXPOSURETIMERAW, VALUE_INT), + ("SENSOR_EXPOSUREAUTOENABLE",PARAM_ID_SENSOR_EXPOSUREAUTOENABLE, VALUE_INT), + ("SENSOR_GAINANALOGRAW", PARAM_ID_SENSOR_GAINANALOGRAW, VALUE_INT), + ("SENSOR_GAINANALOGAUTOENABLE",PARAM_ID_SENSOR_GAINANALOGAUTOENABLE, VALUE_INT), + ("SENSOR_GAINANALOGAGCMAX", PARAM_ID_SENSOR_GAINANALOGAGCMAX, VALUE_INT), + ("SENSOR_GAINDIGITRAW", PARAM_ID_SENSOR_GAINDIGITRAW, VALUE_INT), + ("SENSOR_GAINDIGITAUTOENABLE",PARAM_ID_SENSOR_GAINDIGITAUTOENABLE, VALUE_INT), + ("SENSOR_GAINDIGITAGCMAX", PARAM_ID_SENSOR_GAINDIGITAGCMAX, VALUE_INT), + ("SENSOR_BITLUT", PARAM_ID_SENSOR_BITLUT, VALUE_INT), + + # IPU + ("IPU_BRIGHTNESS", PARAM_ID_IPU_BRIGHTNESS, VALUE_INT), + ("IPU_CONTRAST", PARAM_ID_IPU_CONTRAST, VALUE_INT), + + # Grab + ("GRAB_FRAMERATE", PARAM_ID_GRAB_FRAMERATE, VALUE_FLOAT), +] + + + +# ========================= +# STRUCTS (mínimo necessário) +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" # <- CORRETO: bytes (fica zerado / string vazia) + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +# Param API +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + + +def ck(ret: int, name: str): + if ret != 0: + print(f"{name} falhou, ret={ret}") + raise RuntimeError(f"{name} falhou, ret={ret}") + +def param_get_int(h: W.HANDLE, pid: int) -> int: + p = devparam_by_id(pid) + v = C.c_int(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return int(v.value) + +def param_get_float(h: W.HANDLE, pid: int) -> float: + p = devparam_by_id(pid) + v = C.c_float(0) + ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_FLOAT) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + return float(v.value) + +def param_get_str(h: W.HANDLE, pid: int) -> str: + p = devparam_by_id(pid) + # buffer de tamanho razoável (ajusta se precisar) + buf_size = 256 + buf = C.create_string_buffer(buf_size) + ret = dll.VT_ParamGetValue(h, p, buf, VALUE_STR) + ck(ret, f"VT_ParamGetValue({hex(pid)})") + # strip em caso de lixo no final + return buf.value.decode(errors="ignore").strip() + +def param_supported(h, pid, vtype): + p = devparam_by_id(pid) + minv = C.c_int() + maxv = C.c_int() + inc = C.c_int() + ret = dll.VT_ParamGetRange(h, p, + C.byref(minv), + C.byref(maxv), + C.byref(inc), + vtype) + return ret == 0 + +def probe_params(h: W.HANDLE): + print("\n==== PARAM PROBE (VT SDK) ====") + print(f"{'Name':35s} {'ID':10s} {'Supported':10s} Value") + print("-" * 70) + + for name, pid, vtype in PARAMS_TO_PROBE: + supported = param_supported(h, pid, vtype) + + if not supported: + print(f"{name:35s} {hex(pid):10s} {'NO':10s} -") + continue + + # tentar ler o valor atual + try: + if vtype == VALUE_INT: + val = param_get_int(h, pid) + elif vtype == VALUE_FLOAT: + val = param_get_float(h, pid) + elif vtype == VALUE_STR: + val = param_get_str(h, pid) + else: + val = "" + + print(f"{name:35s} {hex(pid):10s} {'YES':10s} {val}") + except Exception as e: + print(f"{name:35s} {hex(pid):10s} {'YES':10s} ") + +def main(): + # scan + n = C.c_ubyte(0) + ret = dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF) + ck(ret, "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ret = dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF) + ck(ret, "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + + probe_params(h) + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/quick_sugar_vs_weed_overlay.py b/Python/gal5000/quick_sugar_vs_weed_overlay.py new file mode 100644 index 000000000..083f7b537 --- /dev/null +++ b/Python/gal5000/quick_sugar_vs_weed_overlay.py @@ -0,0 +1,176 @@ +import os +import cv2 +import numpy as np + +# ========= CONFIG ========= +RAW_PATH = r"dataset/ds/raws/20260115_152748_007.raw" # ajuste se precisar +W = 2592 # largura do RAW (pixels do mosaico) +H = 2056 # altura do RAW +UPSCALE = 2 # aumenta preview (2x fica bom) + +# overlay +ALPHA = 0.45 # transparência da máscara +MIN_IR = 15 # ignora pixels muito escuros no IR (ruído) +MIN_G = 20 # ignora pixels muito escuros no G (ruído) + +# ========= RAW decode ========= +def read_raw_mosaic(path, w, h): + raw = np.fromfile(path, dtype=np.uint8) + if raw.size != w * h: + raise RuntimeError(f"RAW size mismatch: got {raw.size}, expected {w*h}. " + f"Confira W/H.") + return raw.reshape(h, w) + +def split_4ch(raw): + # 2x2 pattern: + # [R, G] + # [IR,B] + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + IR = raw[1::2, 0::2] + B = raw[1::2, 1::2] + return R, G, IR, B + +def norm8(x, p_lo=2, p_hi=98): + lo = np.percentile(x, p_lo) + hi = np.percentile(x, p_hi) + if hi <= lo + 1: + return x.astype(np.uint8) + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + +def make_rgb_preview(R, G, B, upscale=2): + Rn, Gn, Bn = norm8(R), norm8(G), norm8(B) + bgr = np.dstack([Bn, Gn, Rn]) # OpenCV = BGR + if upscale != 1: + bgr = cv2.resize(bgr, (bgr.shape[1]*upscale, bgr.shape[0]*upscale), interpolation=cv2.INTER_NEAREST) + return bgr + +# ========= Simple spectral classifier ========= +def classify_cane_weed(G, IR, thr_ratio, thr_ir_bias): + """ + Retorna mask_cane, mask_weed em resolução H/2 x W/2. + + Padrões: + - ERVA: ratio = G/(IR+1) maior + - CANA: IR relativamente maior + ratio menor + + thr_ratio: limiar principal de G/IR + thr_ir_bias: adicional: favorece CANA quando IR está alto + """ + Gf = G.astype(np.float32) + IRf = IR.astype(np.float32) + + ratio = Gf / (IRf + 1.0) + + valid = (Gf >= MIN_G) & (IRf >= MIN_IR) + + # regra: erva se ratio > thr_ratio + weed = valid & (ratio >= thr_ratio) + + # cana: ratio baixo OU IR alto (bias) + # IR alto relativo: IR > (G - thr_ir_bias) ajuda puxar cana + cane = valid & (ratio < thr_ratio) + + + # resolve conflitos: se cair em ambos, usa ratio como desempate + both = weed & cane + if np.any(both): + # se ratio alto -> weed, senão -> cane + weed[both] = ratio[both] >= thr_ratio + cane[both] = ~weed[both] + + # pixels válidos mas não classificados: decide pelo ratio + undec = valid & ~(weed | cane) + if np.any(undec): + weed[undec] = ratio[undec] >= thr_ratio + cane[undec] = ~weed[undec] + + return cane, weed, ratio, valid + +def morph_cleanup(mask, k=3): + if k <= 1: + return mask + ker = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) + m = mask.astype(np.uint8) * 255 + m = cv2.medianBlur(m, 3) + m = cv2.morphologyEx(m, cv2.MORPH_OPEN, ker, iterations=1) + m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, ker, iterations=1) + return m > 0 + +def overlay_classes(bgr, cane_mask, weed_mask, upscale=2): + # sobe masks pro tamanho do preview + h2, w2 = cane_mask.shape + if upscale != 1: + cane = cv2.resize(cane_mask.astype(np.uint8)*255, (w2*upscale, h2*upscale), interpolation=cv2.INTER_NEAREST) + weed = cv2.resize(weed_mask.astype(np.uint8)*255, (w2*upscale, h2*upscale), interpolation=cv2.INTER_NEAREST) + else: + cane = cane_mask.astype(np.uint8)*255 + weed = weed_mask.astype(np.uint8)*255 + + out = bgr.copy() + + # cores (BGR): cana=azul, erva=verde + cane_col = np.zeros_like(out) + cane_col[:, :, 0] = cane # Blue + + weed_col = np.zeros_like(out) + weed_col[:, :, 1] = weed # Green + + # combina overlays + mask_any = (cane > 0) | (weed > 0) + overlay = np.clip(cane_col + weed_col, 0, 255).astype(np.uint8) + + out[mask_any] = (out[mask_any].astype(np.float32) * (1 - ALPHA) + overlay[mask_any].astype(np.float32) * ALPHA).astype(np.uint8) + return out + +def main(): + raw = read_raw_mosaic(RAW_PATH, W, H) + R, G, IR, B = split_4ch(raw) + + base = make_rgb_preview(R, G, B, upscale=UPSCALE) + + cv2.namedWindow("overlay", cv2.WINDOW_NORMAL) + cv2.namedWindow("debug", cv2.WINDOW_NORMAL) + + # sliders + # ratio em escala 0..300 -> 0.00..3.00 + cv2.createTrackbar("thr_ratio x100", "overlay", 270, 500, lambda v: None) # 2.70 inicial + cv2.createTrackbar("ir_bias", "overlay", 5, 100, lambda v: None) # 5 inicial + cv2.createTrackbar("morph_k", "overlay", 5, 21, lambda v: None) # 5 inicial + + while True: + thr_ratio = cv2.getTrackbarPos("thr_ratio x100", "overlay") / 100.0 + thr_ir_bias = float(cv2.getTrackbarPos("ir_bias", "overlay")) + mk = cv2.getTrackbarPos("morph_k", "overlay") + if mk % 2 == 0: + mk += 1 + + cane, weed, ratio, valid = classify_cane_weed(G, IR, thr_ratio, thr_ir_bias) + + cane2 = morph_cleanup(cane, k=mk) + weed2 = morph_cleanup(weed, k=mk) + + out = overlay_classes(base, cane2, weed2, upscale=UPSCALE) + + # debug views + ratio_vis = norm8(ratio, 2, 98) + if UPSCALE != 1: + ratio_vis = cv2.resize(ratio_vis, (ratio_vis.shape[1]*UPSCALE, ratio_vis.shape[0]*UPSCALE), interpolation=cv2.INTER_NEAREST) + + # desenha texto rápido + txt = f"thr_ratio={thr_ratio:.2f} ir_bias={thr_ir_bias:.0f} morph_k={mk}" + cv2.putText(out, txt, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,0), 3, cv2.LINE_AA) + cv2.putText(out, txt, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255,255,255), 2, cv2.LINE_AA) + + cv2.imshow("overlay", out) + cv2.imshow("debug", ratio_vis) + + k = cv2.waitKey(10) & 0xFF + if k in (ord('q'), 27): + break + + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() diff --git a/Python/gal5000/test_vt.py b/Python/gal5000/test_vt.py new file mode 100644 index 000000000..12d21b64c --- /dev/null +++ b/Python/gal5000/test_vt.py @@ -0,0 +1,10 @@ +import os +import ctypes as C + +SDK_DIR = r"C:\ZendionINC\agrobot_base\Python\gal5000\dlls" # <-- ajuste pro seu caminho real + +# Garante que o processo Python consegue achar as DLLs dependentes +os.add_dll_directory(SDK_DIR) + +dll = C.WinDLL(os.path.join(SDK_DIR, "VT_SDK64.dll")) +print("DLL carregada OK:", dll)