You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MAI/autogaze_runner.py

343 lines
12 KiB
Python

import time
import threading
from collections import deque
import cv2
import numpy as np
import torch
from config import *
from helpers import clip_box, box_center, box_area
class AutoGazeROIWorker:
def __init__(self):
self.enabled = bool(AUTOGAZE_ENABLE)
self.ready = False
self.last_error = ""
self._autogaze_cls = None
self._processor_cls = None
if self.enabled:
try:
from autogaze.models.autogaze import AutoGaze, AutoGazeImageProcessor
self._autogaze_cls = AutoGaze
self._processor_cls = AutoGazeImageProcessor
except Exception as e:
self.last_error = f"import failed: {e}"
self.device = torch.device(f"cuda:{int(DEVICE)}") if torch.cuda.is_available() else torch.device("cpu")
self.clip_len = int(max(1, AUTOGAZE_CLIP_LEN))
self.update_every = int(max(1, AUTOGAZE_UPDATE_EVERY))
self.result_ttl = int(max(1, AUTOGAZE_RESULT_TTL))
self.topk = int(max(1, AUTOGAZE_TOPK))
self.min_active_cells = int(max(1, AUTOGAZE_MIN_ACTIVE_CELLS))
self.margin_cells = int(max(0, AUTOGAZE_ROI_MARGIN_CELLS))
self.min_side = float(max(8.0, AUTOGAZE_ROI_MIN_SIDE))
self.gazing_ratio = float(min(1.0, max(0.01, AUTOGAZE_GAZING_RATIO)))
self.task_loss_requirement = float(min(1.0, max(0.0, AUTOGAZE_TASK_LOSS_REQUIREMENT)))
self.model = None
self.processor = None
self.frame_buf = deque(maxlen=self.clip_len)
self.req = deque(maxlen=1)
self.lock = threading.Lock()
self.running = False
self.thread = threading.Thread(target=self._loop, daemon=True)
self.latest = None
self.last_submit_frame = -10**9
def status_line(self):
if not self.enabled:
return "AutoGaze disabled by config"
if self.ready:
return f"AutoGaze ready: model={AUTOGAZE_MODEL_ID} device={self.device}"
if self.last_error:
return f"AutoGaze unavailable: {self.last_error}"
return "AutoGaze unavailable"
def start(self):
if not self.enabled:
return
if self._autogaze_cls is None or self._processor_cls is None:
return
if not self._load_model():
return
if self.running:
return
self.running = True
self.thread.start()
def stop(self):
self.running = False
if self.thread.is_alive():
self.thread.join(timeout=1.0)
def submit(self, frame_eff_bgr, frame_id):
if (not self.running) or (not self.ready):
return
frame_copy = np.ascontiguousarray(frame_eff_bgr.copy())
fid = int(frame_id)
with self.lock:
self.frame_buf.append((fid, frame_copy))
if len(self.frame_buf) < self.clip_len:
return
if (fid - self.last_submit_frame) < self.update_every:
return
items = list(self.frame_buf)
clip = [f for _, f in items]
end_fid = int(items[-1][0])
self.req.append((clip, end_fid))
self.last_submit_frame = fid
def get_latest(self, current_frame_id, frame_w, frame_h, pred_ref_box=None):
info = {
"enabled": int(self.enabled),
"ready": int(self.ready),
"status": self.status_line(),
"stale": 1,
"age": -1,
"active_cells": 0,
"infer_ms": 0.0,
}
if not self.ready:
return None, [], info
with self.lock:
latest = None if self.latest is None else dict(self.latest)
if latest is None:
return None, [], info
age = int(current_frame_id) - int(latest["frame_id"])
info["age"] = age
info["active_cells"] = int(latest.get("active_cells", 0))
info["infer_ms"] = float(latest.get("infer_ms", 0.0))
if age > self.result_ttl:
return None, [], info
rois = []
for r in latest.get("rois", []):
rois.append(clip_box(np.array(r, dtype=np.float32), frame_w, frame_h))
roi = self._pick_roi(rois, pred_ref_box)
info["stale"] = 0
return roi, rois, info
def _load_model(self):
if self.ready and self.model is not None and self.processor is not None:
return True
try:
self.processor = self._processor_cls.from_pretrained(
AUTOGAZE_MODEL_ID,
local_files_only=bool(AUTOGAZE_LOCAL_FILES_ONLY),
)
self.model = self._autogaze_cls.from_pretrained(
AUTOGAZE_MODEL_ID,
use_flash_attn=bool(AUTOGAZE_USE_FLASH_ATTN),
local_files_only=bool(AUTOGAZE_LOCAL_FILES_ONLY),
)
self.model.to(self.device)
self.model.eval()
self.ready = True
self.last_error = ""
return True
except Exception as e:
self.model = None
self.processor = None
self.ready = False
self.last_error = f"load failed: {e}"
return False
def _loop(self):
while self.running:
item = None
with self.lock:
if self.req:
item = self.req.pop()
self.req.clear()
if item is None:
time.sleep(0.001)
continue
clip_frames, frame_id = item
infer_ms = 0.0
rois = []
active_cells = 0
try:
t0 = time.perf_counter()
rois, active_cells = self._infer_rois(clip_frames)
infer_ms = (time.perf_counter() - t0) * 1000.0
except Exception as e:
self.last_error = f"infer failed: {e}"
rois = []
active_cells = 0
infer_ms = 0.0
with self.lock:
self.latest = {
"frame_id": int(frame_id),
"rois": rois,
"active_cells": int(active_cells),
"infer_ms": float(infer_ms),
}
def _infer_rois(self, clip_frames_bgr):
if (self.model is None) or (self.processor is None):
return [], 0
if len(clip_frames_bgr) < self.clip_len:
return [], 0
h, w = clip_frames_bgr[-1].shape[:2]
clip_rgb = [cv2.cvtColor(f, cv2.COLOR_BGR2RGB) for f in clip_frames_bgr[-self.clip_len:]]
processed = self.processor([clip_rgb], return_tensors="pt")
pixel_values = processed.get("pixel_values", None)
if pixel_values is None:
return [], 0
if isinstance(pixel_values, list):
pixel_values = torch.as_tensor(np.asarray(pixel_values))
if pixel_values.ndim == 4:
pixel_values = pixel_values.unsqueeze(0)
video = pixel_values.to(self.device)
if video.dtype != torch.float32 and self.device.type == "cpu":
video = video.float()
with torch.inference_mode():
outputs = self.model(
{"video": video},
gazing_ratio=self.gazing_ratio,
task_loss_requirement=self.task_loss_requirement,
generate_only=True,
)
grid = self._extract_fine_grid(outputs)
if grid is None:
return [], 0
rois, active_cells = self._grid_to_rois(grid, w, h)
return rois, active_cells
def _extract_fine_grid(self, outputs):
gazing_mask = outputs.get("gazing_mask", None)
if gazing_mask is None or len(gazing_mask) == 0:
return None
fine = gazing_mask[-1]
if isinstance(fine, torch.Tensor):
arr = fine.detach().float().cpu().numpy()
else:
arr = np.asarray(fine)
if arr.ndim == 2:
arr = arr[None, ...]
if arr.ndim != 3 or arr.shape[0] <= 0 or arr.shape[1] <= 0:
return None
vec = arr[0, -1]
if vec.size <= 0:
return None
side = int(round(np.sqrt(float(vec.size))))
if side * side != int(vec.size):
return None
grid = vec.reshape(side, side)
return (grid > 0.5).astype(np.uint8)
def _grid_to_rois(self, grid, frame_w, frame_h):
if grid is None or grid.size == 0:
return [], 0
mask = (grid > 0).astype(np.uint8)
active_cells = int(mask.sum())
if active_cells < self.min_active_cells:
return [], active_cells
gh, gw = mask.shape
cell_w = float(frame_w) / float(max(1, gw))
cell_h = float(frame_h) / float(max(1, gh))
candidates = []
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=4)
for lbl in range(1, int(n_labels)):
x = int(stats[lbl, cv2.CC_STAT_LEFT])
y = int(stats[lbl, cv2.CC_STAT_TOP])
ww = int(stats[lbl, cv2.CC_STAT_WIDTH])
hh = int(stats[lbl, cv2.CC_STAT_HEIGHT])
area = int(stats[lbl, cv2.CC_STAT_AREA])
if area < self.min_active_cells:
continue
gx1 = max(0, x - self.margin_cells)
gy1 = max(0, y - self.margin_cells)
gx2 = min(gw, x + ww + self.margin_cells)
gy2 = min(gh, y + hh + self.margin_cells)
roi = np.array(
[gx1 * cell_w, gy1 * cell_h, gx2 * cell_w, gy2 * cell_h],
dtype=np.float32,
)
roi = self._ensure_min_side(roi, frame_w, frame_h)
candidates.append((area, roi))
if not candidates:
ys, xs = np.where(mask > 0)
if xs.size <= 0:
return [], active_cells
gx1 = max(0, int(xs.min()) - self.margin_cells)
gy1 = max(0, int(ys.min()) - self.margin_cells)
gx2 = min(gw, int(xs.max()) + 1 + self.margin_cells)
gy2 = min(gh, int(ys.max()) + 1 + self.margin_cells)
roi = np.array(
[gx1 * cell_w, gy1 * cell_h, gx2 * cell_w, gy2 * cell_h],
dtype=np.float32,
)
roi = self._ensure_min_side(roi, frame_w, frame_h)
candidates.append((active_cells, roi))
candidates.sort(key=lambda x: x[0], reverse=True)
rois = [np.array(r, dtype=np.float32) for _, r in candidates[: self.topk]]
return rois, active_cells
def _ensure_min_side(self, roi, frame_w, frame_h):
roi = clip_box(roi, frame_w, frame_h)
rw = float(roi[2] - roi[0])
rh = float(roi[3] - roi[1])
if rw >= self.min_side and rh >= self.min_side:
return roi
cx = 0.5 * (float(roi[0]) + float(roi[2]))
cy = 0.5 * (float(roi[1]) + float(roi[3]))
side = max(self.min_side, rw, rh)
return clip_box(
np.array([cx - 0.5 * side, cy - 0.5 * side, cx + 0.5 * side, cy + 0.5 * side], dtype=np.float32),
frame_w,
frame_h,
)
def _pick_roi(self, rois, pred_ref_box):
if not rois:
return None
if pred_ref_box is None:
return rois[0]
pref = np.array(pred_ref_box, dtype=np.float32)
pc = box_center(pref)
best = None
best_key = (1e9, 1e9)
for r in rois:
c = box_center(r)
dist = float(np.linalg.norm(c - pc))
a = float(box_area(r))
key = (dist, -a)
if key < best_key:
best_key = key
best = r
return best