SummerAIse / KeyFrameSelection /Similarties.py
Israaabdelghany's picture
update
0d9760b
Raw
History Blame Contribute Delete
4.31 kB
# KeyFrameSelection/Similarties.py
import cv2
from PIL import Image
import imagehash
from skimage.metrics import structural_similarity as ssim
from sklearn.metrics.pairwise import cosine_similarity
import torch
from transformers import CLIPProcessor, CLIPModel
import numpy as np
from concurrent.futures import ThreadPoolExecutor
clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
clip_model.eval()
def _resize_gray(frame):
return cv2.resize(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), (128, 128))
def hash_filter(records, hash_threshold=5, ssim_threshold=0.90, ssim_compare_window=3):
"""
Filters out visually similar frames using perceptual hashing and SSIM.
Args:
records (list): List of tuples (frame, frame_idx) representing sampled video frames.
hash_threshold (int, optional): Maximum Hamming distance between perceptual hashes to consider frames as duplicates. Defaults to 5.
ssim_threshold (float, optional): Maximum SSIM score to consider frames as distinct. Defaults to 0.90.
ssim_compare_window (int, optional): Number of most recent accepted frames to compare against using SSIM. Defaults to 3.
Returns:
list: List of tuples (frame, frame_idx) representing filtered, distinct keyframes.
"""
resized_cache = {idx: _resize_gray(frame) for frame, idx in records}
def compute_hash(frame):
return imagehash.phash(Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)))
with ThreadPoolExecutor() as executor:
hashes = list(executor.map(lambda x: compute_hash(x[0]), records))
seen_hashes = []
distinct = []
for i, (frame, frame_idx) in enumerate(records):
img_hash = hashes[i]
if any(abs(img_hash - h) <= hash_threshold for h in seen_hashes):
continue
seen_hashes.append(img_hash)
is_distinct = True
resized_gray = resized_cache[frame_idx]
for _, prev_idx in distinct[-ssim_compare_window:]:
prev_gray = resized_cache[prev_idx]
if ssim(resized_gray, prev_gray) > ssim_threshold:
is_distinct = False
break
if is_distinct:
distinct.append((frame, frame_idx))
return distinct
def _get_clip_embeddings(frames):
"""
Computes the CLIP image embedding for a given video frame.
Args:
frame (np.ndarray): A single video frame in BGR format (as returned by OpenCV).
Returns:
np.ndarray: A normalized 1D NumPy array representing the CLIP image embedding.
"""
images = [Image.fromarray(cv2.cvtColor(f, cv2.COLOR_BGR2RGB)) for f in frames]
inputs = clip_processor(images=images, return_tensors="pt", padding=True)
with torch.no_grad():
features = clip_model.get_image_features(**inputs)
normed = torch.nn.functional.normalize(features, p=2, dim=1)
return normed.cpu().numpy()
def clip_filter(records, similarity_threshold=0.85, compare_window=5, batch_size=8):
"""
Filters frames using CLIP embeddings and cosine similarity in batch mode (CPU-optimized).
Args:
records (list): List of (frame, frame_idx) tuples.
similarity_threshold (float): Max cosine similarity to keep frame distinct.
compare_window (int): How many past frames to compare against.
batch_size (int): Number of frames to embed per batch (for speed).
Returns:
list: Filtered list of (frame, frame_idx) tuples with distinct content.
"""
frames, frame_idxs = zip(*records)
embeddings = []
for i in range(0, len(frames), batch_size):
batch_frames = frames[i:i+batch_size]
batch_embs = _get_clip_embeddings(batch_frames)
embeddings.extend(batch_embs)
distinct = []
past_embeddings = []
for i, emb in enumerate(embeddings):
is_distinct = True
for prev_emb in past_embeddings[-compare_window:]:
sim = cosine_similarity([emb], [prev_emb])[0][0]
if sim > similarity_threshold:
is_distinct = False
break
if is_distinct:
distinct.append((frames[i], frame_idxs[i]))
past_embeddings.append(emb)
return distinct