Spaces:
Sleeping
Sleeping
File size: 8,672 Bytes
bcf391c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | import sys
import os
from os import path as osp
import argparse
import warnings
import torch
import numpy as np
from PIL import Image
from detectron2.config import instantiate, LazyConfig
from torchvision import transforms
from typing import List, Tuple, Optional, Union
sys.path.append(osp.dirname(osp.dirname(__file__)))
from utils import *
warnings.simplefilter(action="ignore", category=FutureWarning)
Color = Tuple[int, int, int] # BGR (OpenCV)
def overlay_heatmap_on_image(image_bgr: np.ndarray,
heatmap_01: np.ndarray,
boxes: Tuple[int, int, int, int],
gaze_xy: Tuple[float, float], # NEW: (x, y)
inout: bool = True, # NEW: whether in‐out prediction
color: Tuple[int, int, int] = (0, 255, 0), # BGR
color_inout: Tuple[int, int, int] = (255, 0, 0), # BGR for in-out
thickness: int = 4,
alpha: float = 0.2, # small blend
colormap: int = cv2.COLORMAP_JET,
mask_thresh: float = 0.10 # ignore tiny heat
) -> Tuple[np.ndarray, np.ndarray]:
"""
Draws:
1) Heatmap overlay (masked by mask_thresh)
2) Head bounding box
3) A green line from head-box center to gaze point
4) A thick green dot at the gaze point
Returns:
overlay_bgr: image with overlays
colored_heat_bgr: colorized heatmap (BGR)
"""
H, W = image_bgr.shape[:2]
# # --- heatmap -> resized -> [0,1] -> uint8 for colormap ---
heat_resized = cv2.resize(heatmap_01.astype(np.float32), (W, H), interpolation=cv2.INTER_LINEAR)
heat_resized = np.clip(heat_resized, 0.0, 1.0)
heat_uint8 = (heat_resized * 255).round().astype(np.uint8)
colored_heat_bgr = cv2.applyColorMap(heat_uint8, colormap)
# --- inputs to float32 for blending ---
img_u8 = image_bgr if image_bgr.dtype == np.uint8 else np.clip(image_bgr, 0, 255).astype(np.uint8)
img_f = img_u8.astype(np.float32)
heat_f = colored_heat_bgr.astype(np.float32)
# # --- standard blended image ---
blended = cv2.addWeighted(heat_f, alpha, img_f, 1 - alpha, 0.0)
# # --- only apply where heat is meaningful ---
mask = (heat_resized >= mask_thresh).astype(np.float32)[..., None] # (H,W,1)
overlay_f = np.where(mask > 0, blended, img_f)
overlay_bgr = np.clip(overlay_f, 0, 255).astype(np.uint8)
# overlay_bgr = img_u8
# --- robust single-box handling ---
b = np.array(boxes, dtype=np.float32).reshape(-1)
if b.size != 4:
raise ValueError(f"'boxes' must be 4 numbers, got shape {np.array(boxes).shape} / size {b.size}")
x1, y1, x2, y2 = map(int, b)
x1 = int(max(0, min(W - 1, x1))); y1 = int(max(0, min(H - 1, y1)))
x2 = int(max(0, min(W - 1, x2))); y2 = int(max(0, min(H - 1, y2)))
if x2 < x1: x1, x2 = x2, x1
if y2 < y1: y1, y2 = y2, y1
if inout:
# Draw head bounding box
cv2.rectangle(overlay_bgr, (x1, y1), (x2, y2), color, thickness)
# --- compute head center ---
cx = int(round((x1 + x2) * 0.5))
cy = int(round((y1 + y2) * 0.5))
# --- handle gaze point (clip to image and draw) ---
gx, gy = gaze_xy
gx_i = int(round(max(0, min(W - 1, gx))))
gy_i = int(round(max(0, min(H - 1, gy))))
# Draw line from head center to gaze point
line_thickness = max(2, thickness) # slightly robust
cv2.line(overlay_bgr, (cx, cy), (gx_i, gy_i), color, line_thickness, lineType=cv2.LINE_AA)
# Draw a thicker filled dot at the gaze point
gaze_radius = max(12, thickness * 2)
cv2.circle(overlay_bgr, (gx_i, gy_i), gaze_radius, color, thickness=-1, lineType=cv2.LINE_AA)
else:
# Draw head bounding box in different color for out-of-frame
cv2.rectangle(overlay_bgr, (x1, y1), (x2, y2), color_inout, thickness)
return overlay_bgr
def inference_gaze(image_paths, texts, model, visualization_dir, use_dark_inference=True):
img = [Image.open(image_path).convert("RGB") for image_path in image_paths]
save_paths = [image_path.split("/")[-1] for image_path in image_paths]
img_size = [img.size for img in img]
if not isinstance(texts, List):
texts = [texts] * len(image_paths)
image_transform = transforms.Compose(
[
transforms.Resize((512, 512)),
transforms.ToTensor(),
transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
]
)
for i in range(len(img)):
img[i] = image_transform(img[i]).unsqueeze(0).cuda()
img = torch.cat(img, dim=0)
with torch.no_grad():
gaze_heatmap_pred, inout_pred, bbox_pred = model.inference(img, texts)
gaze_heatmap_pred = (
gaze_heatmap_pred.squeeze(1).cpu().detach().numpy()
)
inout_pred = (
inout_pred.cpu().detach().numpy()
)
bbox_pred = (
bbox_pred.cpu().detach().numpy()
)
for b_i in range(len(gaze_heatmap_pred)):
visualization_save_path = osp.join(
visualization_dir,
save_paths[b_i]
)
inout = (inout_pred[b_i] > 0.5)
head_bbox = [0.0, 0.0, img_size[b_i][0], img_size[b_i][1]]
head_bbox[0] = (bbox_pred[b_i][0] - bbox_pred[b_i][2] / 2) * img_size[b_i][0] # x1
head_bbox[1] = (bbox_pred[b_i][1] - bbox_pred[b_i][3] / 2) * img_size[b_i][1] # y1
head_bbox[2] = (bbox_pred[b_i][0] + bbox_pred[b_i][2] / 2) * img_size[b_i][0] # x2
head_bbox[3] = (bbox_pred[b_i][1] + bbox_pred[b_i][3] / 2) * img_size[b_i][1] # y2
os.makedirs(osp.dirname(visualization_save_path), exist_ok=True)
# AUC: area under curve of ROC
if use_dark_inference:
pred_x, pred_y = dark_inference(gaze_heatmap_pred[b_i])
else:
pred_x, pred_y = argmax_pts(gaze_heatmap_pred[b_i])
norm_p = [
pred_x / gaze_heatmap_pred[b_i].shape[-2],
pred_y / gaze_heatmap_pred[b_i].shape[-1],
]
scaled_heatmap = np.array(
Image.fromarray(gaze_heatmap_pred[b_i]).resize(
img_size[b_i],
resample=Image.BILINEAR,
)
)
flat_index = np.argmax(scaled_heatmap)
gaze_x, gaze_y = np.unravel_index(flat_index, scaled_heatmap.shape)
image = cv2.imread(image_paths[b_i])
overlay_bgr = overlay_heatmap_on_image(image, scaled_heatmap, head_bbox, gaze_xy=(gaze_y, gaze_x), inout=inout)
# overlay_bgr, colored_heat_bgr = overlay_heatmap_on_image(image, scaled_heatmap, head_bbox, alpha=0.1)
visualization_pred = overlay_bgr
cv2.imwrite(visualization_save_path, visualization_pred)
def do_test(image_paths, text, model, visualization_dir, use_dark_inference=False):
model.train(False)
for i in range(0, len(image_paths), 16):
batch_image_paths = image_paths[i:i+16]
inference_gaze(batch_image_paths, text, model, visualization_dir, use_dark_inference=use_dark_inference)
def main(args):
cfg = LazyConfig.load(args.config_file)
image_paths = os.listdir(args.input_path)
image_paths = [osp.join(args.input_path, image_path) for image_path in image_paths]
text = args.text
batch_size = 16
visualization_dir = args.output_path # osp.join(args.output_path, "visualization")
if not osp.exists(visualization_dir):
os.makedirs(visualization_dir)
model: torch.Module = instantiate(cfg.model)
model.load_state_dict(torch.load(args.model_weights, weights_only=False)["model"])
model = model.cuda()
do_test(image_paths, text, model, visualization_dir, use_dark_inference=args.use_dark_inference)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config_file", type=str, help="config file")
parser.add_argument("--input_path", type=str, help="input path")
parser.add_argument("--text", type=str, help="input text")
parser.add_argument("--output_path", type=str, help="output path")
parser.add_argument(
"--model_weights",
type=str,
help="model weights",
)
parser.add_argument("--use_dark_inference", action="store_true")
args = parser.parse_args()
main(args)
|