4.6. đ Automatische Erkennung von Einstellungen mit TransNetV2#
Dieses Notebook annotiert die Abfolge und die LĂ€nge der Einstellungen innerhalb einer oder mehrerer Videodateien mithilfe der Shot Boundary Detection, also der automatisierten Erkennung von ĂbergĂ€ngen zwischen Einstellungen. Als Ergebnis entstehen:
eine Tabelle im
.csv-Format mit Start, Ende, Mittelpunkt und Dauer jeder erkannten Einstellung,eine weitere Tabelle im
.csv-Format zu den Metadaten der Analyseeine ELAN-Datei (
.eaf) zum Import und zur Sichtung der Annotationsdaten in VIAN oder anderen Programmen zur Videoannotation.
Beim erneuten Export werden weitere Versionen der Ergebnisdateien angelegt. Bereits vorhandene Dateien bleiben erhalten.
Der Arbeitsablauf ist in vier Abschnitte gegliedert: Festlegen der Parameter, technische Vorbereitung, Analyse der EinstellungsĂŒbergĂ€nge und Export der Ergebnisse. Die Texte zwischen den Codeblöcken erlĂ€utern vor allem den Zweck dieser einzelnen Schritte und die Bedeutung der erzeugten Dateien, wĂ€hrend die Kommentare innerhalb des Codes auf Englisch dessen Funktionsweise vermitteln.
Weitere Informationen zur Funktionsweise dieses Notebooks bietet das EinfĂŒhrungskapitel zur Einstellungserkennung innerhalb der OER.
4.6.1. AusfĂŒhrung des Notebooks#
Im Jupyter Book, also als Seite innerhalb der OER, ist das Ergebnis der AusfĂŒhrung gespeichert und kann nachvollzogen werden, ohne den Code selbst auszufĂŒhren. Wir empfehlen allerdings fĂŒr ein tieferes VerstĂ€ndnis und die Bearbeitung der spĂ€teren Ăbungen, das Notebook in einer Cloud Umgebung oder lokal auszufĂŒhren.
Cloud-Umgebung: Klicken Sie oben rechts in der MenĂŒleiste auf das Raketen-Symbol đ und wĂ€hlen Sie âColabâ, um das Notebook auszufĂŒhren. Denken Sie daran, den Laufzeittyp zu Ă€ndern, um das Notebook ggf. mit einer GPU auszufĂŒhren.
Lokale Umgebung: Klicken Sie oben rechts in der MenĂŒleiste auf das Download-Symbol â und wĂ€hlen Sie â.ipynbâ, um das Notebook herunterzuladen und lokal auszufĂŒhren. AbhĂ€ngig von Browser und Betriebssystem mĂŒssen Sie die Datei mit Rechtsklick auf â.ipynbâ ggf. manuell herunterladen.
Wir empfehlen die Codeblöcke einzeln nacheinander auszufĂŒhren und nicht mit Funktionen wie âAlle ausfĂŒhrenâ in Colab oder âRun All Cellsâ in anderen Anwendungen zur AusfĂŒhrung von Jupyter Notebooks. Die separate AusfĂŒhrung der Codeblöcke erleichtert das VerstĂ€ndnis der mit ihnen verbundenen Funktionen und gewĂ€hrleistet, dass alle vorbereitenden Schritte fĂŒr nachfolgende Codeblöcke bereits durchlaufen wurden.
4.6.2. Festlegen der Parameter#
In diesem Abschnitt werden die zentralen Parameter fĂŒr die spĂ€tere Analyse festgelegt. Sie können an dieser Stelle angepasst werden, ohne Ănderungen im weiteren Code vornehmen zu mĂŒssen. Zum Erlernen der Funktionen des Codes in diesem Notebook und fĂŒr einen ersten Probelauf empfiehlt es sich, die voreingestellten Werte beizubehalten.
Ordnerpfade#
Die Variable VIDEO_PATH benennt den Ordner, innerhalb dessen die zu analysierenden Videodateien abgelegt werden. Falls dieser noch nicht existieren sollte, wird ein Ordner mit entsprechender Benennung angelegt. Die einzelnen Videos können dabei sowohl in Unterordnern, als auch auf der höchsten Ebene des Ordners abgelegt werden:
Option 1: Videodateien direkt im Videoordner#
VIDEO_PATH
âââ video_01.mp4
âââ video_02.mp4
âââ video_03.mp4
Alle zu analysierenden Videodateien befinden sich direkt auf der höchsten Ebene des durch VIDEO_PATH festgelegten Ordners.
Option 2: Videodateien in Unterordnern#
VIDEO_PATH
âââ film_01/
â âââ video_01.mp4
âââ film_02/
â âââ video_02.mp4
âââ film_03/
âââ video_03.mp4
Die zu analysierenden Videodateien befinden sich in einzelnen Unterordnern innerhalb des durch VIDEO_PATH festgelegten Ordners.
Eine Kombination beider Ablageoptionen ist ebenfalls möglich.
Wichtig ist, dass jede Videodatei einen einzigartigen Namen tragen muss. Dies gilt auch fĂŒr Videodateien, die sich in unterschiedlichen Unterordnern befinden. Da das Notebook die Dateinamen ohne ihre Endungen zur Identifikation verwendet, dĂŒrfen beispielsweise nicht gleichzeitig film.mp4 und film.mov vorhanden sein. In diesem Fall gibt das Notebook eine Fehlermeldung aus und unterbricht die Erfassung, bis die Dateinamen entsprechend geĂ€ndert wurden.
In diesen Ordner werden auch die fĂŒr diese OER vorbereiteten Testdateien im .zip-Format abgelegt. Diese sind fĂŒr die AusfĂŒhrung dieses Jupyter Notebooks zu Lernzwecken vorgesehen und wir empfehlen, diese hierfĂŒr auch zu verwenden. Ist deren Download nicht erwĂŒnscht, kann die Variable DOWNLOAD_TESTFILES auf False gesetzt werden. In diesem Fall mĂŒssen eigene Videodateien in den Ordner kopiert werden, der unter dem Parameter VIDEO_PATHangegeben ist.
ZusĂ€tzlich können auch eigene .zip-Archive von Online-SpeicherplĂ€tzen heruntergeladen werden. Hierzu muss ein entsprechender Download-Link innerhalb von AnfĂŒhrungszeichen dem Parameter CUSTOM_ZIP_LINK zugeordnet werden, z.B. CUSTOM_ZIP_LINK = "https://example.org/video_archive.zip". Wichtig ist dabei, dass es sich um einen Direktlink zur .zip-Datei handelt, nicht um einen Link zu einer Website. AuĂerdem sollte beim Download mehrerer Archive nacheinander jeweils die Variable CUSTOM_ARCHIVE_NAME geĂ€ndert werden, da Archive mit demselben Namen nicht erneut heruntergeladen werden. Die Option zum Download eigener Dateien ist besonders bei der Analyse von Langfilmen in Google Colab hilfreich, da die Videodateien dadurch nicht manuell und unter erheblichem Zeitaufwand in die Colab-Umgebung hochgeladen werden mĂŒssen. Der Download von einem Online-Speicher wie z.B. einem Cloud-Dienst benötigt in der Regel weniger Zeit.
Der Parameter EXPORT_PATH bezeichnet den Pfad zum Exportordner.
Festlegung des Schwellenwerts#
Besonders wichtig ist der Schwellenwert THRESHOLD. Er bestimmt, ab welcher vom Modell berechneten Wahrscheinlichkeit ein Bildwechsel als Einstellungsgrenze gewertet und als Annotation in die Ausgabe der Ergebnisse aufgenommen wird. Ein höherer Wert fĂŒhrt tendenziell zu weniger, aber sichereren Treffern. Ein niedrigerer Wert fĂŒhrt zur Erkennung von mehr ĂbergĂ€ngen, fĂŒhrt allerdings auch zu einer höheren Wahrscheinlichkeit falsch-positiver Ergebnisse (âfalse positivesâ). Der Werteumfang fĂŒr die Wahrscheinlichkeit liegt dabei zwischen 0.0 fĂŒr 0% und 1.0 fĂŒr 100%. Eine ausfĂŒhrlichere EinfĂŒhrung zu Schwellenwerten finden Sie im OER-Abschnitt zu zentralen Begriffen und Konzepten.
# Path to the directory containing the video files to be analysed.
# The default relative path is resolved from the location where the notebook is executed.
VIDEO_PATH = "videos_shots"
# The video directory is created automatically if it does not exist yet.
# `exist_ok=True` prevents an error if the directory already exists.
import os
os.makedirs(VIDEO_PATH, exist_ok=True)
# Path to the directory for the export of the analysis results
EXPORT_PATH = "export_shots"
os.makedirs(EXPORT_PATH, exist_ok=True)
# Minimum probability at which TransNetV2 treats a frame as a confirmed shot boundary.
# The value ranges from 0 to 1.
THRESHOLD = 0.5
# If True, a provided test video is downloaded and extracted.
# Set this option to False when analyzing your own files.
DOWNLOAD_TESTFILES = True
# Add your own custom link inside quotes to download additional files into the video folder.
CUSTOM_ZIP_LINK = "insert link here"
# Change the name of the custom archives. A new archive with the same name will not be downloaded.
CUSTOM_ARCHIVE_NAME = "custom_archive"
# Separator line for visually clearer text output.
DIVIDER = "â" * 100
4.6.3. Technische Vorbereitung#
Bevor ein Video analysiert werden kann, mĂŒssen die benötigten Softwarepakete installiert, in das Notebook eingebunden und das vortrainierte TransNetV2-Modell geladen werden.
Installation der Programmbibliotheken#
Der folgende Codeblock installiert externe Programmbibliotheken, die nicht in der Standardinstallation von Python enthalten sind. Dazu zĂ€hlen neben dem Modell TransNetV2 zur Erkennung der EinstellungsĂŒbergĂ€nge
pandaszum Bearbeiten und Exportieren von tabellarischen Daten,opencv-pythonundffmpegzur Videobearbeitung,und
pympi-lingzum Schreiben von ELAN-Dateien (.eaf).
FĂŒr die wichtigsten Bibliotheken wird eine spezifische Version installiert, um deren interdependente Funktionsweise zu garantieren.
ZunĂ€chst wird geprĂŒft, ob eine CUDA-fĂ€hige NVIDIA-GPU vorhanden ist. Die Installation von PyTorch wird entsprechend angepasst. Ist bereits eine funktionsfĂ€hige PyTorch-Installation vorhanden, bleibt sie erhalten. Andernfalls wird bei einer NVIDIA-GPU die CUDA-Version und ohne GPU eine CPU-kompatible Version installiert. Falls ein bereits installiertes PyTorch die vorhandene GPU nicht verwenden kann, wird es durch eine CUDA-Version ersetzt. AnschlieĂend ist ein Neustart des Kernels erforderlich. Weitere Informationen zu CPU und GPU finden Sie im OER-Abschnitt zu den technischen Grundlagen.
In einer lokalen und bei einer erneuten AusfĂŒhrung des Notebooks identischen Arbeitsumgebung muss diese Zelle nicht bei jeder Sitzung erneut ausgefĂŒhrt werden. In Google Colab ist eine erneute Installation nach dem Neustart der Laufzeit hingegen notwendig, da alle speziell installierten Programmbibliotheken beim Beenden der Laufzeit gelöscht werden.
print("Installing dependencies.")
import importlib.util
import os
import sys
# Check for an NVIDIA GPU.
if sys.platform.startswith("win"):
cuda = os.system("nvidia-smi > NUL 2>&1") == 0
else:
cuda = os.system("nvidia-smi > /dev/null 2>&1") == 0
# Select a compatible PyTorch wheel only if PyTorch is not already installed.
if importlib.util.find_spec("torch") is None:
if cuda:
!pip install -q torch --index-url https://download.pytorch.org/whl/cu126
elif sys.platform == "darwin":
!pip install -q torch
else:
!pip install -q torch --index-url https://download.pytorch.org/whl/cpu
# Install the model, video-processing, and annotation dependencies.
!pip install -q transnetv2-pytorch==1.0.5
!pip install -q pandas opencv-python==5.0.0.93 static-ffmpeg==3.0 pympi-ling
# Replace a CPU-only PyTorch build if an NVIDIA GPU is available.
import torch
if cuda and not torch.cuda.is_available():
print("NVIDIA GPU detected, but PyTorch cannot use CUDA. Installing CUDA-enabled PyTorch.")
!pip install -q --upgrade --force-reinstall torch --index-url https://download.pytorch.org/whl/cu126
raise RuntimeError("Restart the kernel after replacing PyTorch, then run the notebook again.")
print("Dependencies installed.")
Installing dependencies.
Dependencies installed.
Import der Programmbibliotheken#
Durch den Import werden die installierten Funktionen nun in der aktuellen Notebook-Sitzung verfĂŒgbar. Die Bibliotheken ĂŒbernehmen unterschiedliche Aufgaben, die im Codeblock jeweils kurz erlĂ€utert werden.
# Pretrained neural network for shot-boundary detection.
from transnetv2_pytorch import TransNetV2
# Check which device PyTorch can use.
import torch
print(f"PyTorch: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
# Handling file and directory paths.
from pathlib import Path
# Editing and exporting tabular data.
import pandas as pd
# Writing ELAN files.
import pympi
# Representing and calculating time values.
import datetime
import time
# Reading videos and their technical properties.
import cv2
import static_ffmpeg
# Downloading a file from a URL and extracting ZIP archives.
from urllib.request import urlretrieve
import zipfile
print("Dependencies imported.")
PyTorch: 2.14.0
CUDA available: False
Dependencies imported.
Download des Analysematerials#
Zum Download des Testmaterials und eines eigenen .zip-Archivs wird eine wiederverwendbare Funktion definiert und direkt angewendet.
Falls bei der Festlegung der Parameter DOWNLOAD_TESTFILES = True gesetzt ist, lĂ€dt das Notebook das .zip-Archiv shot_detection_testfiles.zip herunter, das den Trailer zu Konrad Wolfs Film Der geteilte Himmel enthĂ€lt. Liegt shot_detection_testfiles.zip aufgrund einer vorherigen AusfĂŒhrung bereits auf derselben Ordnerebene wie das Notebook, wird die Datei nicht erneut heruntergeladen. AnschlieĂend wird ihr Inhalt in den zuvor festgelegten Videoordner entpackt. Zu Lernzwecken im Rahmen der OER empfehlen wir den Download und die Arbeit mit den Testdateien im Notebook. Herkunft und Zusammensetzung des Testarchivs erlĂ€utert der OER-Abschnitt zum Analysematerial.
Dieser Schritt ist fĂŒr die Arbeit mit eigenen Video- und Film-Korpora nicht erforderlich. Eigene Videodateien können direkt im VIDEO_PATH Ordner oder in dessen Unterordnern abgelegt werden. In diesem Fall kann DOWNLOAD_TESTFILES = False gesetzt werden. AuĂerdem kann mittels des Parameters CUSTOM_ZIP_LINK ein eigenes .zip-Archiv heruntergeladen werden, wie bereits eingangs im Kontext der Festlegung der Parameter erlĂ€utert. In beiden FĂ€llen sollte der Codeblock zur Festlegung der Parameter bereits ausgefĂŒhrt worden sein, um falls notwendig den Videoordner erst zu erstellen.
# Define a reusable function for downloading .zip archives.
# This function takes three arguments: The download link, the name for the downloaded archive, and the directory to unzip the archive into.
def download_zip(url: str, archive_name: str, target_directory: str):
# Add .zip to the filename and turn it into a relative path in the working directory.
archive_path = Path(f"{archive_name}.zip")
# Download only if the archive is not already present in the working directory.
if not archive_path.is_file():
# Download the file.
print(f"Downloading {archive_path}...")
urlretrieve(url, archive_path)
print(f"{archive_name} downloaded.")
else:
print(f"{archive_name} already downloaded.")
# Unzip the file to the target_directory.
target_directory = Path(target_directory)
with zipfile.ZipFile(archive_path,"r") as zip_ref:
zip_ref.extractall(target_directory)
print(f"Extracted {archive_name} to {target_directory}.")
# Download and extract the testfiles if the setting is enabled.
if DOWNLOAD_TESTFILES == True:
print("Testfile download requested.")
# Download the testfiles
url = ("https://owncloud.gwdg.de/index.php/s/FXBHduaTJcavU7l/download")
download_zip(url, "shot_detection_testfiles", VIDEO_PATH)
else:
print("No testfile download requested.")
# Download and extract the custom archive if a link is provided.
if CUSTOM_ZIP_LINK != "insert link here":
print("Custom file download requested.")
# Download the custom video archive.
download_zip(CUSTOM_ZIP_LINK, CUSTOM_ARCHIVE_NAME, VIDEO_PATH)
Testfile download requested.
Downloading shot_detection_testfiles.zip...
shot_detection_testfiles downloaded.
Extracted shot_detection_testfiles to videos_shots.
Vorbereitung des TransNetV2-Modells#
In diesem Schritt wird das Machine-Learning-Modell TransNetV2 zur Erkennung der EinstellungsĂŒbergĂ€nge vorbereitet. Es handelt sich um ein bereits trainiertes neuronales Netz. Es berechnet fĂŒr jeden Frame eines Videos einen Wahrscheinlichkeitswert, der angibt, wie wahrscheinlich es ist, dass an dieser Stelle ein Ăbergang zwischen zwei Einstellungen stattfindet.
Mit device="auto" nutzt TransNetV2 eine einsatzbereite NVIDIA-GPU (CUDA) und andernfalls die CPU. Damit lĂ€sst sich dasselbe Notebook auf beiden Systemen ausfĂŒhren. Auf Macs mit Apple Silicon verwendet die automatische Auswahl aus GrĂŒnden der ErgebnisstabilitĂ€t ebenfalls die CPU.
Mit model.eval() wird das neuronale Netz in den Auswertungsmodus versetzt. Dieser Modus ist fĂŒr die Anwendung eines trainierten Modells vorgesehen, nicht fĂŒr dessen weiteres Training.
# Select CUDA automatically when it works, otherwise use the CPU.
model = TransNetV2(device="auto")
# Disable training-specific behaviour for reliable inference.
model.eval()
print(f"Model loaded on: {next(model.parameters()).device}")
# Make the bundled ffmpeg and ffprobe executables available for video decoding.
static_ffmpeg.add_paths()
âčïž MPS device detected but not used due to numerical inconsistency issues.
Use --device mps to explicitly enable MPS (faster but inconsistent results).
Model loaded on: cpu
True
4.6.4. Analyse der EinstellungsĂŒbergĂ€nge#
Hier beginnt die eigentliche Analyse. ZunĂ€chst werden die Videodateien erfasst und deren Metadaten gesammelt. AnschlieĂend fĂŒhrt das Notebook mithilfe des Modells die eigentliche Einstellungserkennung durch und bereitet deren Ergebnis tabellarisch auf.
Erfassung der technischen Videometadaten#
FĂŒr jede Videodatei werden deren
Dateiname (
video_name) und Ablageort (path),Filmtitel (
title) und gegebenenfalls das aus dem Dateinamen ermittelte Erscheinungsjahr (release_year),Auflösung (
frame_heightundframe_width),Anzahl der Frames (
frame_count),Bildrate (
frame_rate) undberechnete Laufzeit (
duration_hh_mm_ss)
bestimmt. Diese Angaben sind sowohl fĂŒr die Interpretation des Analyseergebnisses als auch fĂŒr die spĂ€tere Umrechnung von Framepositionen in Sekunden notwendig.
Die Informationen aller Videos werden im Dictionary videos gesammelt. Ein Dictionary ist eine Python-Datenstruktur, in der jedem SchlĂŒssel (key) ein Wert zugeordnet wird. Im vorliegenden Fall dient der Name eines Videos als SchlĂŒssel. Der zugehörige Wert ist ein weiteres Dictionary, das den Dateipfad, technische Metadaten und spĂ€ter auch die Analyseergebnisse enthĂ€lt. Weitere Informationen und Beispiele zur Funktionsweise eines Dictionaries finden Sie etwa im Dictionary-Kapitel des hier verlinkten Python Kurses.
# Supported video file extensions.
VIDEO_EXTENSIONS = {
".mp4", ".avi", ".mov", ".mkv", ".wmv",
".flv", ".webm", ".mpeg", ".mpg", ".m4v", ".3gp"
}
# Store each video's path and technical metadata.
# Video filenames must be unique across all subdirectories.
videos = {}
# Search the input directory for video files.
for video_path in Path(VIDEO_PATH).rglob("*"):
# Ignore directories and other non-file entries.
if not video_path.is_file():
continue
# Ignore files inside Jupyter checkpoint directories.
if ".ipynb_checkpoints" in video_path.parts:
continue
# Ignore unsupported file formats.
if video_path.suffix.lower() not in VIDEO_EXTENSIONS:
continue
# Use the filename without its extension as the dictionary key.
video_name = video_path.stem
# Extract the release year from the filename and check if it is an integer
try:
video_title, release_year = video_name.rsplit("_", 1)
release_year = int(release_year)
print(f"Successfully extracted video name {video_title} and release year {release_year}.")
except:
release_year = None
video_title = video_name
print(f"Filename {video_name} does not provide a release year. The correct formatting is 'title_year'.")
if video_name in videos:
raise ValueError(
f"Duplicate video name: {video_name}. "
"Please use unique filenames."
)
# Open the video without loading the complete file into memory.
capture = cv2.VideoCapture(str(video_path))
if not capture.isOpened():
print(f"Video could not be opened: {video_path}")
capture.release()
continue
# Read the video's technical metadata.
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
frame_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
frame_width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_rate = capture.get(cv2.CAP_PROP_FPS)
# Release the video file after reading its metadata.
capture.release()
# Skip files with invalid metadata.
if frame_rate <= 0 or frame_count <= 0:
print(f"Video contains invalid metadata: {video_path}")
continue
# Calculate the video duration and convert it to HH:MM:SS.
duration_hh_mm_ss = str(
datetime.timedelta(seconds=round(frame_count / frame_rate))
)
print(f"Video file: {video_name}")
print(
f"Resolution: {frame_width}x{frame_height} | "
f"Frames: {frame_count} | "
f"Frame Rate: {frame_rate:.3f} | "
f"Duration: {duration_hh_mm_ss}"
)
print(DIVIDER)
# Initialise the video dictionary.
videos[video_name] = {
"path" : video_path,
}
# Store the metadata for the subsequent analysis steps.
videos[video_name]["metadata"]= {
"video_name" : video_path.name,
"title" : video_title,
"release_year" : release_year,
"frame_count" : frame_count,
"frame_height" : frame_height,
"frame_width" : frame_width,
"frame_rate" : frame_rate,
"duration_hh_mm_ss" : duration_hh_mm_ss,
"model" : "transnetv2",
"device" : next(model.parameters()).device.type,
"analysis_timestamp" : None, # Updated when analysis begins.
"processing_time_hh_mm_ss": None # Updated after the analysis.
}
Successfully extracted video name Der geteilte Himmel_Trailer and release year 1964.
Video file: Der geteilte Himmel_Trailer_1964
Resolution: 1920x1080 | Frames: 5225 | Frame Rate: 25.000 | Duration: 0:03:29
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Anwendung des Modells#
Im folgenden Codeblock werden nacheinander die zuvor gesammelten Videos analysiert.
FĂŒr jedes Video berechnet TransNetV2 pro Frame die Wahrscheinlichkeit fĂŒr einen EinstellungsĂŒbergang. Dabei entstehen drei Ergebnisse:
video_framesist eine Liste der extrahierten Frames, die etwa zur Erstellung einer Visualisierung des Ergebnisses genutzt werden können.all_frame_predictionsenthĂ€lt die vom Modell berechneten Wahrscheinlichkeiten dafĂŒr, dass ein Frame Teil eines EinstellungsĂŒbergangs ist. Bei schrittweisen ĂbergĂ€ngen, beispielsweise Ăberblendungen oder Auf- und Abblenden, können somit mehrere aufeinanderfolgende Frames hohe Wahrscheinlichkeiten erhalten.single_frame_predictionsenthĂ€lt hingegen die Wahrscheinlichkeiten dafĂŒr, dass ein Frame die Grenze zwischen zwei Einstellungen markiert. Das Modell wurde darauf trainiert, auch bei lĂ€ngeren ĂbergĂ€ngen einen einzelnen Frame, in der Regel in der Mitte des Ăbergangs, als Einstellungsgrenze zu identifizieren. Die folgenden Schritte verwenden deshalbsingle_frame_predictionszur Bestimmung der Einstellungsgrenzen.
Die Funktion predictions_to_scenes ĂŒbersetzt die Wahrscheinlichkeiten anschlieĂend mithilfe des gewĂ€hlten Schwellenwerts THRESHOLD in konkrete Einstellungssegmente (shots). Jedes Segment wird als Paar der Framepositionen startframe und endframe gespeichert. Der Schwellenwert THRESHOLD wird im Codeblock zur Festlegung der Parameter eingestellt.
ZusĂ€tzlich misst das Notebook die benötigte Rechenzeit fĂŒr die DurchfĂŒhrung der Analyse (elapsed_time_hh_mm_ss).
Dieser Schritt ist sehr rechenintensiv! Je nach Videomaterial kann er von einer halben Stunde bis zu mehreren Stunden in Anspruch nehmen.
# Destination directory for later export files.
export_directory = Path(EXPORT_PATH)
# Analyze each previously registered video in sequence.
for video_name, video in videos.items():
# Record the start of this analysis in UTC and start the duration timer.
video["metadata"]["analysis_timestamp"] = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
start_time = time.perf_counter()
# Calculate predictions without calculating gradients.
# Gradients are only required when training models.
# This reduces memory usage and speeds up computation.
with torch.no_grad():
video_frames, single_frame_predictions, all_frame_predictions = model.predict_video(video["path"])
# Convert the PyTorch result to a NumPy array for further processing.
prediction_scores = single_frame_predictions.cpu().detach().numpy()
# Convert the probability scores into concrete shot segments.
shots = model.predictions_to_scenes(prediction_scores, THRESHOLD)
# Calculate the elapsed time from when the timer was started.
elapsed_time_seconds = time.perf_counter() - start_time
# Convert the elapsed time into the hhmmss format
elapsed_time_hh_mm_ss = str(
datetime.timedelta(seconds=round(elapsed_time_seconds))
)
print(f"Shot Boundary Detection on {video_name} took {elapsed_time_hh_mm_ss} of processing time.")
print(DIVIDER)
# Record the compute device used by the model.
video["metadata"]["device"] = next(model.parameters()).device.type
# Add the analysis result to the video dictionary
video.update({
"shots" : shots,
"elapsed_time_hh_mm_ss" : elapsed_time_hh_mm_ss,
"prediction_scores": prediction_scores
})
Extracting frames from videos_shots/Der geteilte Himmel_Trailer_1964.mp4
Processing frames: 100%|ââââââââââ| 5225/5225 [02:11<00:00, 39.85frame/s]
Shot Boundary Detection on Der geteilte Himmel_Trailer_1964 took 0:02:20 of processing time.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tabellarische Aufbereitung des Ergebnisses#
Die vom Modell gelieferten Einstellungen (shots) werden in einen pandas-DataFrame (df_shots) ĂŒberfĂŒhrt, mittels dessen sich DatensĂ€tze einfacher bearbeiten und exportieren lassen. Ein Dataframe ist dabei ein spezielles Tabellenformat. Jede Tabellenzeile entspricht einer erkannten Einstellung. Mehr Informationen zu Dataframes finden Sie in diesem Kapitel des zuvor verlinkten Python Kurses.
Neben Startframe (start) und Endframe (end) werden der zeitliche Mittelpunkt (middle) und die Dauer (duration) jeder Einstellung berechnet. Die ZĂ€hlung der Frames beginnt dabei bei 0. Da der Endframe noch zur jeweiligen Einstellung gehört, wird bei der Berechnung der Dauer ein zusĂ€tzlicher Frame berĂŒcksichtigt (end - start + 1). AnschlieĂend werden alle Frameangaben mithilfe der Bildrate (frame_rate) in Sekunden umgerechnet und auf zwei Nachkommastellen gerundet. Die Sekundenwerte erleichtern die weitere Auswertung der erkannten Einstellungen.
# Process the analysis results for each previously registered video in sequence.
for video_name, video in videos.items():
# Build dataframe. Each row contains the start and end frame of a shot.
df_shots = pd.DataFrame(video["shots"], columns=["start", "end"])
# Calculate the middle frame of a shot.
df_shots["middle"] = df_shots["start"] + ((df_shots["end"] - df_shots["start"]) / 2).astype(int)
# Calculate the duration of the shot in number of frames
df_shots["duration"] = df_shots["end"] - df_shots["start"] + 1
# Convert frames to seconds.
# Time in seconds = frame number divided by frames per second ("frame_rate") of the respective video.
for col in ["start", "end", "middle", "duration"]:
df_shots[f"{col}_s"] = round(df_shots[col] / video["metadata"]["frame_rate"], 2)
# Preview the first five detected shots.
print(f"First five shots in {video_name}:")
print(df_shots.head(5))
print(DIVIDER)
# Add the complete table to the corresponding video dictionary
video.update({
"df_shots" : df_shots
})
First five shots in Der geteilte Himmel_Trailer_1964:
start end middle duration start_s end_s middle_s duration_s
0 0 179 89 180 0.00 7.16 3.56 7.20
1 180 447 313 268 7.20 17.88 12.52 10.72
2 448 788 618 341 17.92 31.52 24.72 13.64
3 789 971 880 183 31.56 38.84 35.20 7.32
4 972 1186 1079 215 38.88 47.44 43.16 8.60
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Berechnung der Analysemetadaten#
Neben den Ergebnissen der Einstellungserkennung erzeugt das Notebook pro Video eine weitere Tabelle metadata, in der die Metadaten zur Analyse eingetragen werden:
der angewendete Schwellenwert,
die Anzahl der Einstellungen,
der Mittelwert der EinstellungslÀngen,
der Median der EinstellungslÀngen,
die Dauer der lĂ€ngsten und der kĂŒrzesten Einstellung,
die Gesamtdauer des Videos, die fĂŒr die Analyse benötigte Rechenzeit,
der minimale und der maximale Wahrscheinlichkeitswert bei der Erkennung,
die Auflösung und die Bildrate der Videodatei,
das verwendete Modell (
model) und das RechengerÀt (device),sowie den Zeitpunkt des Analysebeginns in UTC (
analysis_timestamp).
Eine vollstĂ€ndige Ăbersicht der exportierten Felder bietet die Metadatentabelle im OER-Abschnitt zur Ausgabe des Notebooks.
Diese Daten sind nĂŒtzlich, um eine kompakte Ăbersicht ĂŒber das Ergebnis zu erhalten oder um etwa die Auswirkungen unterschiedlicher THRESHOLD-Werte auf die Erkennungsergebnisse zu vergleichen. AuĂerdem werden diese Daten fĂŒr die Visualisierung der Einstellungsmetadaten im nĂ€chsten Kapitel verwendet.
Mittelwert (mean) und Median (median) sind unterschiedliche Methoden zur Berechnung von Durchschnittswerten. Der Mittelwert berechnet sich aus der Summe der erkannten Einstellungsdauern geteilt durch deren Anzahl. Der Mittelwert reagiert stĂ€rker auf sehr lange Einstellungen. Der Median beschreibt hingegen die Dauer der Einstellung die genau in der Mitte liegt, wenn alle Einstellungen ihrer LĂ€nge nach nacheinander angeordnet werden (von der lĂ€ngsten bis zur kĂŒrzesten) und ist gegenĂŒber AusreiĂern robuster.
Die minimalen und maximalen Vorhersagewerte (min_prediction_score und max_prediction_score) geben den niedrigsten und höchsten vom Modell berechneten Wahrscheinlichkeitswert fĂŒr einen EinstellungsĂŒbergang im untersuchten Video an. Diese Werte sind allerdings kein MaĂ fĂŒr die Genauigkeit der Erkennung. Auch ein hoher Wahrscheinlichkeitswert bedeutet nicht zwangslĂ€ufig, dass ein EinstellungsĂŒbergang korrekt erkannt wurde. DafĂŒr mĂŒssten die Ergebnisse mit manuell ĂŒberprĂŒften Annotationen verglichen werden.
# Calculate the metadata for each previously registered video in sequence.
for video_name, video in videos.items():
video["metadata"].update({
# Record the threshold used to ensure reproducibility.
"threshold" : THRESHOLD,
# Number of shots detected by the model.
"shot_count" : len(video["df_shots"]),
# Descriptive statistics for shot duration in seconds.
"mean_duration_s" : round(video["df_shots"]["duration_s"].mean(), 2),
"median_duration_s" : round(video["df_shots"]["duration_s"].median(), 2),
"max_duration_s" : round(video["df_shots"]["duration_s"].max(), 2),
"min_duration_s" : round(video["df_shots"]["duration_s"].min(), 2),
# Duration of the source video and time required for automatic processing.
"duration_hh_mm_ss" : video["metadata"]["duration_hh_mm_ss"],
"processing_time_hh_mm_ss" : video["elapsed_time_hh_mm_ss"],
# Range of probability values produced by the model.
"min_prediction_score" : round(float(video["prediction_scores"].min()),2),
"max_prediction_score" : round(float(video["prediction_scores"].max()),2),
})
# Transform the metadata dictionary into a dataframe.
video["metadata"] = pd.DataFrame.from_dict(
video["metadata"], orient="index", columns=["Value"]
)
print(f"Metadata for {video_name}:")
display(video["metadata"])
print(DIVIDER)
Metadata for Der geteilte Himmel_Trailer_1964:
| Value | |
|---|---|
| video_name | Der geteilte Himmel_Trailer_1964.mp4 |
| title | Der geteilte Himmel_Trailer |
| release_year | 1964 |
| frame_count | 5225 |
| frame_height | 1080 |
| frame_width | 1920 |
| frame_rate | 25.0 |
| duration_hh_mm_ss | 0:03:29 |
| model | transnetv2 |
| device | cpu |
| analysis_timestamp | 2026-10-29T15:19:12+00:00 |
| processing_time_hh_mm_ss | 0:02:20 |
| threshold | 0.5 |
| shot_count | 23 |
| mean_duration_s | 9.09 |
| median_duration_s | 7.76 |
| max_duration_s | 22.04 |
| min_duration_s | 1.96 |
| min_prediction_score | 0.0 |
| max_prediction_score | 0.99 |
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
4.6.5. Export der Ergebnisse#
Die Analyseergebnisse werden als nĂ€chstes in mehreren Formaten gespeichert. .csv-Dateien eignen sich fĂŒr Tabellenkalkulation, Statistikprogramme und weitere Python-Auswertungen, etwa zur Visualisierung. Zudem können sie auch in OpenRefine eingelesen werden. Die Verwendung von OpenRefine wird in der OER zu OER zu Studentischen Filmen an der FilmuniversitĂ€t genauer erklĂ€rt, ebenso das csv-Dateiformat.
Das .eaf-Format kann direkt in VIAN oder anderen Programmen zur Videoannotation geöffnet oder importiert werden, die Daten können dann dort manuell ĂŒberprĂŒft, bearbeitet und ergĂ€nzt werden.
Beim erneuten Export werden zusÀtzliche Versionen der Ergebnisdateien erstellt. Vorhandene Dateien bleiben erhalten.
Export der Tabellen als .csv-Dateien#
Die Ergebnisse der Einstellungserkennung werden fĂŒr jedes analysierte Video in zwei .csv-Dateien exportiert:
Die Datei
âŠ_shots_THRESHOLD.csventhĂ€lt die einzelnen erkannten Einstellungen. Jede Zeile dokumentiert Beginn, Ende, Mittelpunkt und Dauer einer Einstellung, jeweils in Frames und Sekunden.Die Datei
âŠ_shots_THRESHOLD_metadata.csventhĂ€lt die technischen Metadaten des Videos, den verwendeten AnalyseparameterTHRESHOLDsowie die berechneten statistischen Ergebnisse der Einstellungserkennung. Die Angaben werden als SchlĂŒssel-Wert-Paare in zwei Spalten gespeichert.
Weitere Informationen finden Sie im OER-Abschnitt zur Ausgabe des Notebooks.
Beide Dateien werden direkt im zuvor festgelegten Exportordner (EXPORT_PATH) gespeichert. Anders als beim Notebook zur Figurenerkennung wird kein eigener Unterordner pro Video angelegt. Ihre Namen setzen sich aus dem Namen der Videodatei ohne Endung, shots und dem gewÀhlten Schwellenwert (THRESHOLD) zusammen.
Die folgende Hilfsfunktion next_export_paths() prĂŒft fĂŒr jedes Video die beiden vorgesehenen CSV-Dateien und die zugehörige EAF-Datei gemeinsam. Beim ersten Export werden die ursprĂŒnglichen Dateinamen verwendet, beispielsweise Filmtitel_shots_0.5.csv und Filmtitel_shots_0.5_metadata.csv. Existiert bereits mindestens eine Datei dieses Satzes, wird fĂŒr alle Dateien des nĂ€chsten Exports derselbe freie Versionszusatz vergeben (_v2, _v3 usw.). So können bei einem zweiten Export Filmtitel_shots_0.5_v2.csv, Filmtitel_shots_0.5_metadata_v2.csv und Filmtitel_shots_0.5_v2.eaf entstehen. Auch eine unvollstĂ€ndige frĂŒhere Ausgabe reserviert ihre Versionsnummer.
Die ausgewĂ€hlten Pfade werden unter video["export_paths"] gespeichert und beim EAF-Export wiederverwendet. FĂŒr eine neue vollstĂ€ndige Version werden zuerst die CSV-Exportzelle und danach die EAF-Exportzelle ausgefĂŒhrt. Wird nur die EAF-Exportzelle erneut ausgefĂŒhrt, bleibt eine bereits vorhandene EAF-Datei erhalten.
# Select one version for all export files belonging to the same video.
# Find a free version for the complete group of CSV and EAF files.
def next_export_paths(paths):
version = 1
while True:
# The first export has no suffix; later versions use _v2, _v3, etc.
suffix = "" if version == 1 else f"_v{version}"
candidates = {
kind: path.with_name(f"{path.stem}{suffix}{path.suffix}")
for kind, path in paths.items()
}
# Even one existing file reserves this version for the whole group.
if not any(path.exists() for path in candidates.values()):
return candidates
version += 1
# Export the tables for each previously registered video.
export_directory = Path(EXPORT_PATH)
export_directory.mkdir(parents=True, exist_ok=True)
for video_name, video in videos.items():
# The threshold in the filename distinguishes different model settings.
stem = f"{video_name}_shots_{THRESHOLD}"
# Reserve one shared version for both CSV files and the later EAF file.
video["export_paths"] = next_export_paths({
"shots": export_directory / f"{stem}.csv",
"metadata": export_directory / f"{stem}_metadata.csv",
"eaf": export_directory / f"{stem}.eaf",
})
paths = video["export_paths"]
# mode="x" refuses to overwrite a CSV file that already exists.
video["df_shots"].to_csv(paths["shots"], index=False, mode="x")
# Store the metadata as key-value pairs without a column heading.
video["metadata"].to_csv(paths["metadata"], header=False, mode="x")
print(f"Successfully exported {paths['shots']} & {paths['metadata']}.")
print(DIVIDER)
Successfully exported export_shots/Der geteilte Himmel_Trailer_1964_shots_0.5.csv & export_shots/Der geteilte Himmel_Trailer_1964_shots_0.5_metadata.csv.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Export der Einstellungsannotationen als .eaf-Datei#
Die Datei âŠ_shots_THRESHOLD.eaf enthĂ€lt die erkannten Einstellungen als zeitliche Annotationen und wird ebenfalls direkt im Exportordner (EXPORT_PATH) gespeichert. Bei wiederholtem Export bleiben frĂŒhere Versionen der EAF-Datei erhalten.
Das .eaf-Format speichert Annotationen als Zeitangaben in Millisekunden. Deshalb werden die Start- und Endframes jeder Einstellung mithilfe der individuellen Bildrate des Videos direkt in Millisekunden umgerechnet. Der Endzeitpunkt liegt unmittelbar nach dem letzten zur Einstellung gehörenden Frame.
FĂŒr jedes Video wird eine Annotationsebene angelegt, die sich aus shots und dem gewĂ€hlten Schwellenwert THRESHOLD zusammensetzt. Jede erkannte Einstellung erscheint dort als eigenes Zeitintervall. Die Intervalle enthalten anders als gewöhnlich keinen Text, sondern bilden eine zeitliche Segmentierung, die nach dem Import in VIAN dort auf ihre korrekte Erkennung hin ĂŒberprĂŒft und inhaltlich ergĂ€nzt werden kann.
# Export the .eaf annotations for each previously registered video in sequence.
for video_name, video in videos.items():
# Reuse the version selected when exporting this video's CSV files.
if "export_paths" not in video:
raise RuntimeError("Run the CSV export cell before exporting EAF files.")
eaf_document_path = video["export_paths"]["eaf"]
# Prevent overwriting when the EAF export cell is run a second time.
if eaf_document_path.exists():
print(f"Skipping {eaf_document_path.name}: file already exists. Re-run the CSV export cell for a new version.")
print(DIVIDER)
continue
frame_rate = float(video["metadata"].loc["frame_rate", "Value"])
# Create a new, initially empty .eaf document.
eaf_document = pympi.Eaf()
# Add the annotation tier for the automatically detected shots.
eaf_document.add_tier(f"shots_{THRESHOLD}")
# Convert each table row into a time interval in milliseconds.
for _, row in video["df_shots"].iterrows():
eaf_document.add_annotation(
f"shots_{THRESHOLD}",
round(row["start"] / frame_rate * 1000),
round((row["end"] + 1) / frame_rate * 1000)
)
# Write the EAF using the version already assigned to the CSV files.
eaf_document.to_file(eaf_document_path)
print(f"Successfully exported {eaf_document_path}.")
print(DIVIDER)
Successfully exported export_shots/Der geteilte Himmel_Trailer_1964_shots_0.5.eaf.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Herunterladen des Google Colab Export-Ordners#
Dieser optionale Schritt ist nur fĂŒr eine AusfĂŒhrung in Google Colab vorgesehen. Der gesamte Exportordner in Colab wird zu einer .zip-Datei komprimiert und anschlieĂend ĂŒber die BrowseroberflĂ€che heruntergeladen. Der Download ist notwendig, da ansonsten die Erkennungsdaten verloren gehen, da diese nach Beenden der Laufzeit bzw. von Colab immer gelöscht werden, ebenso wie alle in Colab zur Analyse hochgeladenen Videodateien.
In einer lokalen Jupyter Notebook-Umgebung bleiben die Dateien im anfangs im Codeblock zur Festlegung der Parameter definierten Export-Ordner erhalten und können von dort entnommen werden. Es ist daher kein zusÀtzlicher Download-Schritt erforderlich.
try:
# The following import works only within a Google Colab runtime.
import google.colab
from google.colab import files
# Colab uses the `/content` directory by default.
colab_export_directory = f"/content/{EXPORT_PATH}"
zip_path = f"{EXPORT_PATH}.zip"
# Package the entire results directory as a ZIP archive.
!zip -r "{zip_path}" "{colab_export_directory}"
# Start downloading the ZIP archive in the browser.
files.download(f"/content/{EXPORT_PATH}.zip")
print(f"Downloaded {EXPORT_PATH}.zip.")
except ImportError:
# Skip this step when running locally.
print("Download only works in Google Colab.")
Download only works in Google Colab.