scripts teste gal5000

This commit is contained in:
Diego Freitas 2026-01-28 15:05:51 -03:00
parent 19a4145405
commit d9f83292fb
19 changed files with 3133 additions and 0 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<HQVSDK>
<BASIC>
<LogEnable>0</LogEnable>
<TraceEnable>0</TraceEnable>
<ConfigEnable>0</ConfigEnable>
<DefaultParamConfig>0</DefaultParamConfig>
<DetectTimeOut>500</DetectTimeOut>
</BASIC>
</HQVSDK>

BIN
Python/gal5000/dlls/HQV_AVIProcess64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/HQV_DataConvert64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/HQV_FilePro64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/HQV_TinyXML64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/HQV_USB64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/HQV_USS64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/KSJApi64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/MT_KSJBayerFilter_x64.dll (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Python/gal5000/dlls/VT_SDK64.dll (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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 = "<unknown type>"
print(f"{name:35s} {hex(pid):10s} {'YES':10s} {val}")
except Exception as e:
print(f"{name:35s} {hex(pid):10s} {'YES':10s} <get error: {e}>")
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()

View File

@ -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()

10
Python/gal5000/test_vt.py Normal file
View File

@ -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)