Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -179,6 +179,22 @@ def get_sam3_model_and_processor(model_id="facebook/sam3", hf_token=None):
|
|
| 179 |
"and provided a valid access token in the text box."
|
| 180 |
)
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, progress=gr.Progress()):
|
| 183 |
"""
|
| 184 |
Core callback function:
|
|
@@ -215,14 +231,14 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 215 |
|
| 216 |
orig_fps = orig_fps if orig_fps and orig_fps > 0 else 15.0
|
| 217 |
|
| 218 |
-
#
|
| 219 |
-
target_fps =
|
| 220 |
frame_skip = max(1, int(orig_fps / target_fps))
|
| 221 |
fps = orig_fps / frame_skip
|
| 222 |
max_frames = int(max_duration * fps)
|
| 223 |
|
| 224 |
# Resize resolution to speed up CPU inference dramatically
|
| 225 |
-
max_dim =
|
| 226 |
scale = 1.0
|
| 227 |
if max(height, width) > max_dim:
|
| 228 |
scale = max_dim / max(height, width)
|
|
@@ -246,14 +262,33 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 246 |
if not frames:
|
| 247 |
return None, "# ⚠️ Error\nNo frames could be loaded from the video."
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
progress(0.15, desc="Initializing SAM 3 video session...")
|
| 250 |
|
| 251 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 252 |
dtype = torch.float32 if device == "cpu" else torch.bfloat16
|
| 253 |
|
| 254 |
-
# Initialize video session
|
| 255 |
session = processor.init_video_session(
|
| 256 |
-
video=
|
| 257 |
inference_device=device,
|
| 258 |
inference_state_device=device,
|
| 259 |
processing_device=device,
|
|
@@ -270,11 +305,11 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 270 |
obj_to_prompt = {}
|
| 271 |
track_db = defaultdict(set)
|
| 272 |
|
| 273 |
-
progress(0.20, desc="Running SAM 3 propagation across
|
| 274 |
|
| 275 |
session = to_device_recursive(session, device)
|
| 276 |
|
| 277 |
-
frame_count = len(
|
| 278 |
with torch.no_grad():
|
| 279 |
for model_outputs in model.propagate_in_video_iterator(
|
| 280 |
inference_session=session,
|
|
@@ -282,7 +317,7 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 282 |
max_frame_num_to_track=frame_count,
|
| 283 |
):
|
| 284 |
processed_outputs = processor.postprocess_outputs(session, model_outputs)
|
| 285 |
-
|
| 286 |
|
| 287 |
object_ids = processed_outputs["object_ids"]
|
| 288 |
masks = processed_outputs["masks"]
|
|
@@ -305,9 +340,9 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 305 |
if mask_2d.ndim == 3:
|
| 306 |
mask_2d = mask_2d.squeeze()
|
| 307 |
mask_2d = (mask_2d > 0.0).astype(np.uint8)
|
| 308 |
-
masks_by_frame[
|
| 309 |
|
| 310 |
-
progress(0.20 + 0.60 * ((
|
| 311 |
|
| 312 |
session = to_device_recursive(session, "cpu")
|
| 313 |
|
|
@@ -322,7 +357,9 @@ def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, pro
|
|
| 322 |
font = ImageFont.load_default()
|
| 323 |
|
| 324 |
for frame_idx, frame in enumerate(frames):
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
# Overlay masks on frame
|
| 327 |
annotated_img = overlay_masks_on_frame(frame, frame_masks, color_by_obj, alpha=0.55)
|
| 328 |
|
|
|
|
| 179 |
"and provided a valid access token in the text box."
|
| 180 |
)
|
| 181 |
|
| 182 |
+
def is_different_enough(img1, img2, threshold=0.92):
|
| 183 |
+
"""Detects if two images are visually different enough based on HSV histogram correlation."""
|
| 184 |
+
# Convert PIL Images to HSV numpy arrays
|
| 185 |
+
arr1 = cv2.cvtColor(np.array(img1), cv2.COLOR_RGB2HSV)
|
| 186 |
+
arr2 = cv2.cvtColor(np.array(img2), cv2.COLOR_RGB2HSV)
|
| 187 |
+
|
| 188 |
+
# Calculate histogram
|
| 189 |
+
hist1 = cv2.calcHist([arr1], [0, 1], None, [30, 32], [0, 180, 0, 256])
|
| 190 |
+
hist2 = cv2.calcHist([arr2], [0, 1], None, [30, 32], [0, 180, 0, 256])
|
| 191 |
+
|
| 192 |
+
cv2.normalize(hist1, hist1, 0, 1, cv2.NORM_MINMAX)
|
| 193 |
+
cv2.normalize(hist2, hist2, 0, 1, cv2.NORM_MINMAX)
|
| 194 |
+
|
| 195 |
+
corr = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
|
| 196 |
+
return corr < threshold
|
| 197 |
+
|
| 198 |
def analyze_video_sam3(video_path, text_prompts, hf_token, max_duration=8.0, progress=gr.Progress()):
|
| 199 |
"""
|
| 200 |
Core callback function:
|
|
|
|
| 231 |
|
| 232 |
orig_fps = orig_fps if orig_fps and orig_fps > 0 else 15.0
|
| 233 |
|
| 234 |
+
# Target 8 FPS for smooth output rendering
|
| 235 |
+
target_fps = 8.0
|
| 236 |
frame_skip = max(1, int(orig_fps / target_fps))
|
| 237 |
fps = orig_fps / frame_skip
|
| 238 |
max_frames = int(max_duration * fps)
|
| 239 |
|
| 240 |
# Resize resolution to speed up CPU inference dramatically
|
| 241 |
+
max_dim = 320
|
| 242 |
scale = 1.0
|
| 243 |
if max(height, width) > max_dim:
|
| 244 |
scale = max_dim / max(height, width)
|
|
|
|
| 262 |
if not frames:
|
| 263 |
return None, "# ⚠️ Error\nNo frames could be loaded from the video."
|
| 264 |
|
| 265 |
+
# Extract motion-based keyframes to minimize model run times
|
| 266 |
+
progress(0.12, desc="Extracting keyframes...")
|
| 267 |
+
keyframes = [frames[0]]
|
| 268 |
+
keyframe_indices = [0]
|
| 269 |
+
max_gap = 6 # force a keyframe at least every 6 frames to keep temporal coherence
|
| 270 |
+
min_gap = 2 # wait at least 2 frames between keyframes to save time
|
| 271 |
+
|
| 272 |
+
for i in range(1, len(frames)):
|
| 273 |
+
gap = i - keyframe_indices[-1]
|
| 274 |
+
if gap >= max_gap:
|
| 275 |
+
keyframes.append(frames[i])
|
| 276 |
+
keyframe_indices.append(i)
|
| 277 |
+
elif gap >= min_gap:
|
| 278 |
+
if is_different_enough(frames[i], keyframes[-1], threshold=0.94):
|
| 279 |
+
keyframes.append(frames[i])
|
| 280 |
+
keyframe_indices.append(i)
|
| 281 |
+
|
| 282 |
+
print(f"Total frames: {len(frames)}, Extracted keyframes: {len(keyframes)}")
|
| 283 |
+
|
| 284 |
progress(0.15, desc="Initializing SAM 3 video session...")
|
| 285 |
|
| 286 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 287 |
dtype = torch.float32 if device == "cpu" else torch.bfloat16
|
| 288 |
|
| 289 |
+
# Initialize video session with keyframes only
|
| 290 |
session = processor.init_video_session(
|
| 291 |
+
video=keyframes,
|
| 292 |
inference_device=device,
|
| 293 |
inference_state_device=device,
|
| 294 |
processing_device=device,
|
|
|
|
| 305 |
obj_to_prompt = {}
|
| 306 |
track_db = defaultdict(set)
|
| 307 |
|
| 308 |
+
progress(0.20, desc="Running SAM 3 propagation across keyframes...")
|
| 309 |
|
| 310 |
session = to_device_recursive(session, device)
|
| 311 |
|
| 312 |
+
frame_count = len(keyframes)
|
| 313 |
with torch.no_grad():
|
| 314 |
for model_outputs in model.propagate_in_video_iterator(
|
| 315 |
inference_session=session,
|
|
|
|
| 317 |
max_frame_num_to_track=frame_count,
|
| 318 |
):
|
| 319 |
processed_outputs = processor.postprocess_outputs(session, model_outputs)
|
| 320 |
+
k_frame_idx = model_outputs.frame_idx # index in keyframes list
|
| 321 |
|
| 322 |
object_ids = processed_outputs["object_ids"]
|
| 323 |
masks = processed_outputs["masks"]
|
|
|
|
| 340 |
if mask_2d.ndim == 3:
|
| 341 |
mask_2d = mask_2d.squeeze()
|
| 342 |
mask_2d = (mask_2d > 0.0).astype(np.uint8)
|
| 343 |
+
masks_by_frame[k_frame_idx][current_obj_id] = mask_2d
|
| 344 |
|
| 345 |
+
progress(0.20 + 0.60 * ((k_frame_idx + 1) / frame_count), desc=f"Propagating: Keyframe {k_frame_idx + 1}/{frame_count}...")
|
| 346 |
|
| 347 |
session = to_device_recursive(session, "cpu")
|
| 348 |
|
|
|
|
| 357 |
font = ImageFont.load_default()
|
| 358 |
|
| 359 |
for frame_idx, frame in enumerate(frames):
|
| 360 |
+
# Find the closest keyframe index
|
| 361 |
+
closest_k = min(range(len(keyframe_indices)), key=lambda k: abs(keyframe_indices[k] - frame_idx))
|
| 362 |
+
frame_masks = masks_by_frame.get(closest_k, {})
|
| 363 |
# Overlay masks on frame
|
| 364 |
annotated_img = overlay_masks_on_frame(frame, frame_masks, color_by_obj, alpha=0.55)
|
| 365 |
|