142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
"""Media hashing and mask-focus helpers shared by HF and GGUF backends."""
|
|
|
|
import hashlib
|
|
|
|
import torch
|
|
|
|
MASK_FOCUS_INSTRUCTION = (
|
|
"The image has a mask highlight: the selected area is shown at normal brightness and the surrounding "
|
|
"context is dimmed. Analyze and describe only the normal-brightness selected area. Use the dimmed "
|
|
"surroundings only for spatial context and do not describe them."
|
|
)
|
|
|
|
MASK_RECONSTRUCTION_INSTRUCTION = (
|
|
"The selected area is deliberately concealed and contains no usable information about its former "
|
|
"contents. Never identify or describe anything that may have occupied it. Use only the visible "
|
|
"surroundings to infer the most natural scene continuation through that area, and describe the "
|
|
"completed scene without mentioning masks, removal, editing, or missing content."
|
|
)
|
|
|
|
|
|
def tensor_to_pil(tensor):
|
|
"""Convert tensor to PIL Image with memory optimization"""
|
|
if tensor is None:
|
|
return None
|
|
try:
|
|
if tensor.dim() == 4:
|
|
tensor = tensor[0]
|
|
|
|
# More aggressive memory management
|
|
if tensor.is_floating_point():
|
|
# Scale to 0-255 range more efficiently
|
|
tensor = tensor.clamp(0, 1)
|
|
|
|
# Reduce sample size for memory efficiency
|
|
array = tensor.cpu().numpy()
|
|
if tensor.numel() > 0:
|
|
# Take smaller sample for hash to save memory
|
|
sample_size = min(50, tensor.numel() // 4) # Reduced from 100
|
|
sample_pixels = array.flatten()[:sample_size].tolist() if array.size > 0 else []
|
|
else:
|
|
sample_pixels = []
|
|
|
|
content = f"{tensor.shape}_{tensor.dtype}_{sample_pixels[:10]}"
|
|
return hashlib.md5(content.encode()).hexdigest()[:16]
|
|
except:
|
|
return None
|
|
|
|
def get_image_hash(image):
|
|
"""Generate hash for image tensor"""
|
|
if image is None:
|
|
return None
|
|
try:
|
|
# Use image tensor properties for hash
|
|
shape = str(image.shape)
|
|
dtype = str(image.dtype)
|
|
# Sample a few pixels for content hash (avoid full tensor for performance)
|
|
if len(image.shape) >= 3:
|
|
sample_pixels = image.flatten()[:100].tolist() if image.numel() > 0 else []
|
|
else:
|
|
sample_pixels = image.flatten().tolist() if image.numel() > 0 else []
|
|
|
|
content = f"{shape}_{dtype}_{sample_pixels[:10]}" # Limit sample size
|
|
return hashlib.md5(content.encode()).hexdigest()[:16]
|
|
except:
|
|
return None
|
|
|
|
|
|
def apply_mask_highlight(image, mask, mode="focus"):
|
|
"""Prepare a masked image for focus analysis or background reconstruction."""
|
|
if mask is None:
|
|
return image, None
|
|
if image is None:
|
|
raise ValueError("[QwenVL] A mask requires an image input.")
|
|
if not torch.is_tensor(image) or image.ndim not in (3, 4):
|
|
raise ValueError("[QwenVL] IMAGE must have shape [H,W,C] or [B,H,W,C].")
|
|
if not torch.is_tensor(mask):
|
|
raise ValueError("[QwenVL] MASK must be a tensor.")
|
|
if mode not in {"focus", "reconstruct"}:
|
|
raise ValueError("[QwenVL] mask mode must be 'focus' or 'reconstruct'.")
|
|
|
|
if mask.ndim == 2:
|
|
selected_mask = mask
|
|
elif mask.ndim == 3:
|
|
selected_mask = mask[0]
|
|
else:
|
|
raise ValueError("[QwenVL] MASK must have shape [H,W] or [B,H,W].")
|
|
|
|
selected_mask = selected_mask.detach().float().clamp(0.0, 1.0)
|
|
if not torch.isfinite(selected_mask).all():
|
|
raise ValueError("[QwenVL] MASK contains non-finite values.")
|
|
if not torch.any(selected_mask > 0):
|
|
raise ValueError("[QwenVL] MASK is empty; select at least one pixel.")
|
|
|
|
frame = image[0] if image.ndim == 4 else image
|
|
resized_mask = torch.nn.functional.interpolate(
|
|
selected_mask[None, None],
|
|
size=frame.shape[:2],
|
|
mode="bilinear",
|
|
align_corners=False,
|
|
)[0, 0].to(device=frame.device, dtype=frame.dtype)
|
|
highlighted_frame = frame.clone()
|
|
color_channels = min(int(frame.shape[-1]), 3)
|
|
if mode == "reconstruct":
|
|
# Fully hide the selected subject, including anti-aliased mask edges, so the
|
|
# vision model can reason from the surrounding scene instead of naming it.
|
|
binary_mask = (resized_mask > 0.01).to(dtype=frame.dtype)
|
|
expanded_mask = torch.nn.functional.max_pool2d(
|
|
binary_mask[None, None], kernel_size=9, stride=1, padding=4,
|
|
)[0, 0]
|
|
visible = 1.0 - expanded_mask
|
|
visible_weight = visible.sum()
|
|
if float(visible_weight.detach().cpu()) > 0:
|
|
fill = (
|
|
frame[..., :color_channels] * visible.unsqueeze(-1)
|
|
).sum(dim=(0, 1)) / visible_weight
|
|
else:
|
|
fill = torch.full(
|
|
(color_channels,), 0.5, device=frame.device, dtype=frame.dtype,
|
|
)
|
|
highlighted_frame[..., :color_channels] = (
|
|
frame[..., :color_channels] * visible.unsqueeze(-1)
|
|
+ fill.view(1, 1, -1) * expanded_mask.unsqueeze(-1)
|
|
)
|
|
else:
|
|
brightness = 0.2 + 0.8 * resized_mask
|
|
highlighted_frame[..., :color_channels] *= brightness.unsqueeze(-1)
|
|
highlighted = image.clone()
|
|
if image.ndim == 4:
|
|
highlighted[0] = highlighted_frame
|
|
else:
|
|
highlighted = highlighted_frame
|
|
|
|
digest = hashlib.md5()
|
|
digest.update(mode.encode("ascii"))
|
|
digest.update(str(tuple(mask.shape)).encode("ascii"))
|
|
digest.update(mask.detach().contiguous().cpu().numpy().tobytes())
|
|
return highlighted, digest.hexdigest()[:16]
|
|
|
|
def get_video_hash(video):
|
|
"""Generate hash for video tensor (same as image)"""
|
|
return get_image_hash(video)
|