ברוכים הבאים למבוא של SysSeder לחיפוש חזותי בווידאו.
תרגיל תכנות מועיל הוא למצוא סצנות חזותיות בווידאו בלי לתייג ידנית כל פריים. המאמר משתמש בבקשות חיפוש לדוגמה, ולא טוען לגילוי מסוים ב־Hacker News או לתוצאות של קטלוג סרט.
תגיות ידניות וחיפוש מילות מפתח עדיין מועילים, אך שאילתות חזותיות תיאוריות עשויות להפיק תועלת גם מאחזור תמונה–טקסט. השתמשו רק במדיה שאתם מורשים לעבד; הדוגמה אינה מערכת לזיהוי פנים או למעקב.
דוגמת Python שלא הורצה משלבת דגימת פריימים ב־OpenCV, מודל CLIP תואם לתמונה ולטקסט ואחזור וקטורי ב־Qdrant. זהו חיפוש דמיון חזותי, לא שחזור מרחבי גאומטרי, והוא אינו מבטיח איתור של כל סצנה רלוונטית.
הארכיטקטורה: מווידאו גולמי לחיפוש סמנטי
הצינור מדרג דמיון נלמד בין תמונה לטקסט. הוא אינו מדגים הבנה אנושית, ו־OCR או מטא־נתונים עדיין עשויים לעזור לפי המשימה:
- דגימת פריימים: מפענחים וידאו מקומי ובוחרים פריימים במרווחים. קצבי הפריימים משתנים, ודגימה במרווחים אינה זיהוי גבולות צילום; צילומים קצרים עלולים להיות מוחמצים.
- שיכונים: CLIP לומד ייצוג משותף לתמונה ולטקסט. תיאורים קשורים עשויים לקבל דירוג גבוה מול תמונה, אך איכות ההתאמה תלויה באימון, בניסוח ובתוכן; ציונים אינם הסתברויות מכוילות.
- אינדוקס וקטורי: שומרים את הווקטורים עתירי הממדים — 512 ממדים ב־CLIP-ViT-B-32 — ב־Qdrant, מסד נתונים וקטורי בקוד פתוח שמיועד לחיפוש דמיון מהיר.
- שאילתה: כשהמשתמש מקליד חיפוש, יוצרים שיכון לטקסט באמצעות אותו מודל CLIP ומבצעים חיפוש דמיון קוסינוס מול פריימי הווידאו שאונדקסו.
הנה ייצוג טקסטואלי פשוט של זרימת הנתונים:
[ Raw Video ] ──> (OpenCV Frame Extraction) ──> [ Keyframes ]
│
▼
[ User Search Query ] ──> (CLIP Text Encoder) [ CLIP Image Encoder ]
│ │
▼ ▼
[ Query Vector ] ───( Cosine Similarity )───> [ Vector Database (Qdrant) ]
│
▼
[ Best Matching Frames ]
הגדרת הסביבה
יש לסקור סביבת Python ייעודית ולקבע גרסאות תלויות תואמות לפני שימוש. פונקציות התכונות המוצגות תואמות ל־API המתועד של Transformers 4.x שמחזיר טנזורים; אין זו הבטחת תאימות לכל גרסה ראשית מאוחרת. לא הותקנו חבילות או משקלי מודל לצורך המאמר.
# Illustrative dependency families; review and lock exact versions before use.
pip install opencv-python pillow "transformers>=4.42,<5" torch qdrant-client
בהדגמה קטנה זו נשתמש בלקוח המקומי של Qdrant בזיכרון במקום בשרת רשת. אין לו יציאת שירות, והנתונים נמחקים כשהתהליך מסתיים. פריסת שרת בהמשך תדרוש בקרות גישה והגנה על נתיבי מדיה ועל מטעני הנתונים.
אין צורך בשירות Docker להדגמה זו.
קטע בניית האינדקס משתמש ב־QdrantClient(":memory:").
שלב 1: דגימת פריימים במרווחים קבועים עם OpenCV
דגימה אחת בשנייה היא פשרה לדוגמה בין עלות לכיסוי, ולא מספיקה ללכידת כל צילום. הקוד להלן מניח קובץ מקומי שאומת כבעל קצב פריימים קבוע; חותמות זמן לפי מספר פריים חלקי FPS הן אומדן ואינן תחליף לזמני תצוגה מדויקים בווידאו בקצב משתנה.
import cv2
import os
import math
import uuid
from PIL import Image
def extract_keyframes(video_path, output_dir, interval_seconds=1.0):
# Educational CFR-only sampler; not scene detection or exact VFR timing.
if not math.isfinite(interval_seconds) or interval_seconds <= 0:
raise ValueError("interval_seconds must be finite and positive")
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
extracted_frames = []
try:
if not cap.isOpened():
raise ValueError("Cannot open the authorized local video")
fps = cap.get(cv2.CAP_PROP_FPS)
if not math.isfinite(fps) or fps <= 0:
raise ValueError("A valid constant frame rate must be established")
frame_interval = max(1, round(fps * interval_seconds))
video_id = str(uuid.uuid4()) # Identifies this import, not video identity.
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break # End-of-file and decode failure need separate production handling.
if frame_count % frame_interval == 0:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
path = os.path.join(output_dir, f"{video_id}_{frame_count}.jpg")
Image.fromarray(rgb).save(path)
extracted_frames.append({
"filepath": path, "timestamp": frame_count / fps,
"frame_index": frame_count, "video_id": video_id
})
frame_count += 1
finally:
cap.release()
return extracted_frames
שלב 2: הפיכת פריימים לווקטורים עם CLIP
לאחר שיש לנו פריימים, צריך להמיר אותם לייצוגים מתמטיים שלוכדים משמעות סמנטית. נשתמש במימוש של Hugging Face למודל clip-vit-base-patch32.
import torch
from transformers import CLIPProcessor, CLIPModel
device = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_ID = "openai/clip-vit-base-patch32"
# Review/pin model revision and dependency versions before local use.
model = CLIPModel.from_pretrained(MODEL_ID).to(device).eval()
processor = CLIPProcessor.from_pretrained(MODEL_ID)
def normalized_vector(features):
norm = features.norm(dim=-1, keepdim=True)
if not torch.isfinite(features).all() or not torch.isfinite(norm).all() or (norm <= 0).any():
raise ValueError("Embedding must be finite and nonzero")
vector = (features / norm).cpu().numpy()[0].tolist()
if len(vector) != 512:
raise ValueError("This collection expects 512-dimensional CLIP features")
return vector
def generate_image_embedding(pil_image):
inputs = processor(images=pil_image, return_tensors="pt").to(device)
with torch.no_grad():
return normalized_vector(model.get_image_features(**inputs))
שלב 3: אינדוקס וקטורים ב־Qdrant
ניצור אוסף חדש בזיכרון ונשמור כל וקטור עם מזהה הייבוא, מספר הפריים וחותמת זמן מוערכת. הדגמה זו אינה דורסת אוסף קיים בשרת ואינה מממשת התמדה, בקרת גישה, מחיקה או תור קליטה לייצור.
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
client = QdrantClient(":memory:")
COLLECTION_NAME = "authorized_video_demo"
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=512, distance=Distance.COSINE),
)
def index_video_frames(frames):
batch = []
for frame in frames:
with Image.open(frame["filepath"]) as image:
vector = generate_image_embedding(image.convert("RGB"))
point_id = str(uuid.uuid5(uuid.UUID(frame["video_id"]), str(frame["frame_index"])))
batch.append(PointStruct(id=point_id, vector=vector, payload=dict(frame)))
if len(batch) >= 32:
client.upsert(collection_name=COLLECTION_NAME, points=batch, wait=True)
batch = []
if batch:
client.upsert(collection_name=COLLECTION_NAME, points=batch, wait=True)
שלב 4: מימוש חיפוש סמנטי רב־אופני
הקסם של CLIP הוא שאפשר לחפש במאגר תמונות באמצעות מחרוזת טקסט, משום שהמודל ממקם מושגים דומים סמנטית בקרבה. נכתוב פונקציה לשיכון שאילתת הטקסט ולשליחת השאילתה ל־Qdrant.
def generate_text_embedding(text_query):
if not text_query.strip():
raise ValueError("Query must not be empty")
# Truncation respects the model token limit, but can discard query detail.
inputs = processor(text=[text_query], return_tensors="pt", padding=True,
truncation=True).to(device)
with torch.no_grad():
return normalized_vector(model.get_text_features(**inputs))
def search_video(query, limit=3):
query_vector = generate_text_embedding(query)
result = client.query_points(
collection_name=COLLECTION_NAME,
query=query_vector,
limit=limit,
with_payload=True,
)
for hit in result.points:
payload = hit.payload or {}
print(f"Score: {hit.score:.4f} | Import: {payload.get('video_id')} | "
f"Approx. time: {payload.get('timestamp')}s | Path: {payload.get('filepath')}")
חיבור הכול יחד
לקטע מקומי שמורשים לעבד, למשל authorized_demo.mp4, נקודת הכניסה הבאה, שלא הורצה, מחברת את החלקים. היא מניחה שהקטעים הקודמים נמצאים באותו מודול; איכות החיפוש וזמן הריצה לא נבדקו.
if __name__ == "__main__":
# 1. Extract frames from our video file
video_file = "authorized_demo.mp4" # Use only media you are authorized to process
output_directory = "./extracted_frames"
# Extract frame every 1 second of video
frames = extract_keyframes(video_file, output_directory, interval_seconds=1.0)
# 2. Index frames into Qdrant Vector DB
index_video_frames(frames)
# 3. Perform natural language queries
search_video("a retro subway platform with passengers")
search_video("a police officer wearing a hat")
search_video("an aerial view of a city street")
מדוע זה חשוב למפתחים מודרניים
CLIP מציע גישת אחזור גמישה אחת. זיהוי עצמים, OCR ומטא־נתונים שנערכו ידנית פותרים בעיות שונות ויכולים להשלים אותו; צינור של ארבעה מודלים או שבירות אינם הכרחיים.
שיכונים נלמדים יכולים לצמצם תיוג ייעודי, אך ל־CLIP מגבלות מתועדות בזיהוי עדין ובספירה. כרטיס המודל מגביל את השימוש הלשוני המיועד לאנגלית, ולכן גרסת המודל הזו אינה מבססת איכות שאילתות בעברית. זו המחשה למחקר בלבד: כרטיס המודל מחריג שימוש בפריסה ודורש בדיקות מעמיקות וייעודיות לתחום גם בניסויי חיפוש תמונות מוגבלים שאינם פרוסים, עם טקסונומיה קבועה. כיסוי החיפוש, ההשהיה ועלויות האחסון תלויים במאגר, באינדקס ובחומרה; לא מוצגת כאן תוצאה של מיליארד וקטורים או פחות מאלפית שנייה.
לפני ששוקלים שימוש בייצור
מעבר לכוונון ביצועים, יש להעריך איכות אחזור, הרשאות, שמירה ומחיקה, התאוששות משגיאות ומגבלות משאבים. שמרו רק מדיה ומטא־נתונים נחוצים; אל תחשפו נתיבים מקומיים או תוצאות למשתמשים שאינם מורשים.
- אצוות: מדדו גודלי אצווה מתאימים לזיכרון המעבד הגרפי הזמין. אצוות גדולות עשויות לשפר תפוקה אך אינן מיטביות תמיד; הדגמה זו מגבילה את גודל ההעלאות ומחשבת שיכון לתמונה אחת בכל פעם.
- זיהוי צילומים:
PySceneDetectמתעד גלאי תוכן, סף, הסתגלות, היסטוגרמה וגיבוב; הוא אינו פשוט גלאי למידה עמוקה. העריכו ספים ומעברים שהוחמצו; זיהוי צילומים אינו מבטיח שכל פריים שימושי יישמר. - חיפוש משולב: שלבו דמיון וקטורי עם חיפוש מטא־נתונים מסורתי, למשל סינון תוצאות לפי ז׳אנר הסרט, סוג המצלמה או טווח חותמות זמן, באמצעות יכולות סינון המטען של Qdrant.
לסיום
הדוגמה מציגה את רכיבי אחזור הווידאו בתמונה–טקסט, לא מערכת ייצור. התאמת המקודדים, מטא־נתונים מפורשים של פריים וזמן, הערכה ובקרות גישה חשובים לא פחות מצינור Python קצר.
אילו שיטות לדגימת פריימים ולהערכת אחזור עזרו לכם באוספי מדיה שאתם מורשים לעבד? שתפו בתגובות מגבלות ומדידות.
עד הפעם הבאה, תכנות מהנה!