scripts teste gal5000
This commit is contained in:
parent
19a4145405
commit
d9f83292fb
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue