turns-00035.parquet:39478
9951d8e4048e94d6cfa30a3e
turn 1/1o1-mini-2024-09-12GermanGermany6720 words
degenerate_repetitionAbsentFinal dense release
USER
verbesser diese anwendung und siorge dafür das das gui bei jedem bearbeitungsprozess aktiv bleibt und nicht einfriert. Vergrößer die texte und nutze zum beispiel die textgröße und text formatierungen wie fett kursiv um die lesbarkeit und übersicht deutlich zu verbessern. aorge auch dafür das wichtigere elemente deutlicher erkennbar werden.
führe dort wo du probleme oder verbesserungspotenzial erkennst weitere verbesserungen durch und nenne mir abschließend das gesamte verbesserte skript der anwendung ohne fehlende zeilen
import sys
import os
import cv2
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont
from PyQt5.QtWidgets import (
QLabel, QTabWidget, QTextEdit, QLineEdit, QPushButton,
QListWidget, QVBoxLayout, QFileDialog, QHBoxLayout, QGroupBox,
QFormLayout, QSlider, QCheckBox, QProgressBar, QMainWindow, QApplication, QMessageBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QMutexLocker
from skimage.metrics import structural_similarity as ssi
from PIL import Image
# Modern Dark mode stylesheet
DARK_STYLE = """
/* Allgemeine Einstellungen */
QWidget {
background-color: #2b2b2b;
color: #e0e0e0;
font-family: 'Segoe UI', sans-serif;
font-size: 10pt;
}
QMainWindow {
background-color: #2b2b2b;
}
/* Buttons */
QPushButton {
background-color: #3c3f41;
border: 1px solid #5a5a5a;
padding: 6px 12px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #505253;
}
QPushButton:pressed {
background-color: #2b2b2b;
}
QPushButton:disabled {
background-color: #3c3f4166;
color: #8a8a8a;
border: 1px solid #5a5a5a66;
}
/* Eingabe-/Ausgabefelder */
QLineEdit, QTextEdit, QListWidget, QLabel, QSlider, QGroupBox {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
padding: 4px;
border-radius: 3px;
color: #e0e0e0;
}
QLineEdit:disabled, QTextEdit:disabled, QListWidget:disabled {
background-color: #3c3c3c66;
color: #8a8a8a;
}
/* Slider */
QSlider::groove:horizontal {
border: 1px solid #757575;
height: 6px;
background: #5a5a5a;
border-radius: 3px;
}
QSlider::handle:horizontal {
background: #1abc9c;
border: 1px solid #16a085;
width: 12px;
margin: -3px 0;
border-radius: 6px;
}
QSlider::handle:horizontal:hover {
background: #17a589;
}
/* Fortschrittsbalken */
QProgressBar {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
border-radius: 5px;
text-align: center;
height: 20px;
}
QProgressBar::chunk {
background-color: #1abc9c;
width: 10px;
margin: 0.5px;
}
/* Tab Widget */
QTabWidget::pane {
border: 1px solid #444;
background-color: #2b2b2b;
border-radius: 5px;
}
QTabBar::tab {
background: #3c3c3c;
border: 1px solid #444;
padding: 8px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
margin-right: 1px;
font-weight: bold;
}
QTabBar::tab:selected, QTabBar::tab:hover {
background: #1abc9c;
color: #2b2b2b;
}
/* DropLineEdit */
DropLineEdit {
border: 2px dashed #5a5a5a;
padding: 10px;
border-radius: 4px;
min-height: 50px;
}
DropLineEdit.drag_active {
border: 2px dashed #1abc9c;
background-color: #3a3d41;
}
/* GroupBox Title */
QGroupBox {
border: 1px solid #5a5a5a;
border-radius: 5px;
margin-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #1abc9c;
font-weight: bold;
}
/* Labels */
QLabel {
font-weight: bold;
font-size: 10pt;
}
/* Listen */
QListWidget {
selection-background-color: #1abc9c;
selection-color: #2b2b2b;
}
/* Checkboxes */
QCheckBox {
padding: 4px;
font-size: 10pt;
}
"""
class DropLineEdit(QLineEdit):
"""
A QLineEdit that accepts drag and drop of files or directories with visual feedback.
Supports multiple drops.
"""
files_dropped = pyqtSignal(list)
def __init__(self, accept_dir: bool = False, accept_file: bool = False, parent=None):
super().__init__(parent)
self.accept_dir = accept_dir
self.accept_file = accept_file
self.setAcceptDrops(True)
self.setReadOnly(True)
self.setCursor(Qt.PointingHandCursor)
self.default_style = self.styleSheet()
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
valid = False
for url in urls:
path = url.toLocalFile()
if (self.accept_file and os.path.isfile(path)) or (self.accept_dir and os.path.isdir(path)):
valid = True
break
if valid:
event.acceptProposedAction()
self.setProperty('drag_active', True)
self.style().unpolish(self)
self.style().polish(self)
self.update()
return
event.ignore()
def dragLeaveEvent(self, event):
self.setProperty('drag_active', False)
self.style().unpolish(self)
self.style().polish(self)
self.update()
def dropEvent(self, event):
self.setProperty('drag_active', False)
self.style().unpolish(self)
self.style().polish(self)
self.update()
urls = event.mimeData().urls()
paths = []
for url in urls:
path = url.toLocalFile()
if (self.accept_file and os.path.isfile(path)) or (self.accept_dir and os.path.isdir(path)):
paths.append(path)
if paths:
self.setText('; '.join(paths))
self.files_dropped.emit(paths)
event.acceptProposedAction()
def setStyleSheet(self, style: str):
super().setStyleSheet(style)
class PreviewLabel(QLabel):
"""
A QLabel that displays an image with zoom effect on hover.
"""
def __init__(self):
super().__init__()
self.original_pixmap = None
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
background-color: #3c3c3c;
border: 2px solid #5a5a5a;
border-radius: 5px;
}
""")
self.setScaledContents(False)
def setPixmap(self, pixmap: QPixmap):
if pixmap != self.original_pixmap:
self.original_pixmap = pixmap
scaled_pixmap = pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
super().setPixmap(scaled_pixmap)
def resizeEvent(self, event):
if self.original_pixmap:
scaled_pixmap = self.original_pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
super().setPixmap(scaled_pixmap)
super().resizeEvent(event)
def enterEvent(self, event):
if self.original_pixmap:
zoomed_pixmap = self.original_pixmap.scaled(
self.size() * 1.2,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
super().setPixmap(zoomed_pixmap)
def leaveEvent(self, event):
if self.original_pixmap:
super().setPixmap(
self.original_pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
)
class ImageLoaderThread(QtCore.QThread):
"""
Thread to load image files from a directory.
"""
progress = pyqtSignal(int)
finished = pyqtSignal(list)
def __init__(self, directories: list):
super().__init__()
self.directories = directories
def run(self):
image_files = []
# Supported image extensions
supported_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')
for directory in self.directories:
for root, dirs, files in os.walk(directory):
for file in files:
if file.lower().endswith(supported_ext):
image_files.append(os.path.join(root, file))
total_files = len(image_files)
for idx, file in enumerate(image_files, 1):
progress_percent = int((idx / total_files) * 100) if total_files > 0 else 100
self.progress.emit(progress_percent)
self.msleep(5)
self.finished.emit(image_files)
class FrameExtractor(QtCore.QObject):
"""
Processes a video file to extract frames based on quality metrics.
"""
progress = pyqtSignal(int)
log = pyqtSignal(str)
finished = pyqtSignal(list)
def __init__(self, video_paths: list, output_dir: str, sharpness_threshold: int, overlap_threshold: float,
brightness_adjustment: int, shadow_removal_enabled: bool, contrast_adjustment: int,
saturation_adjustment: int):
super().__init__()
self.video_paths = video_paths
self.output_dir = output_dir
self.sharpness_threshold = sharpness_threshold
self.overlap_threshold = overlap_threshold
self.brightness_adjustment = brightness_adjustment
self.shadow_removal_enabled = shadow_removal_enabled
self.contrast_adjustment = contrast_adjustment
self.saturation_adjustment = saturation_adjustment
def log_message(self, message: str):
self.log.emit(message)
def measure_sharpness(self, frame: np.ndarray) -> float:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
lap = cv2.Laplacian(gray, cv2.CV_64F)
return lap.var()
def frames_overlap(self, frame1: np.ndarray, frame2: np.ndarray) -> float:
hist1 = cv2.calcHist([frame1], [0, 1, 2], None, [8,8,8], [0,256,0,256,0,256])
hist2 = cv2.calcHist([frame2], [0, 1, 2], None, [8,8,8], [0,256,0,256,0,256])
cv2.normalize(hist1, hist1)
cv2.normalize(hist2, hist2)
similarity = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
return similarity
def adjust_brightness(self, frame: np.ndarray) -> np.ndarray:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
v = np.clip(v + self.brightness_adjustment, 0, 255).astype(np.uint8)
final_hsv = cv2.merge((h, s, v))
return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
def adjust_contrast(self, frame: np.ndarray) -> np.ndarray:
alpha = 1 + self.contrast_adjustment / 100.0
return cv2.convertScaleAbs(frame, alpha=alpha, beta=0)
def adjust_saturation(self, frame: np.ndarray) -> np.ndarray:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
s = np.clip(s + self.saturation_adjustment, 0, 255).astype(np.uint8)
final_hsv = cv2.merge((h, s, v))
return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
def shadow_removal(self, frame: np.ndarray) -> np.ndarray:
if not self.shadow_removal_enabled:
return frame
lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)
l_channel, a_channel, b_channel = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
cl = clahe.apply(l_channel)
limg = cv2.merge((cl, a_channel, b_channel))
return cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
def sharpen_image(self, frame: np.ndarray) -> np.ndarray:
kernel = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]])
return cv2.filter2D(frame, -1, kernel)
def process_video(self, video_path: str, basename: str):
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
self.log_message(f"Fehler: Videodatei '{video_path}' konnte nicht geöffnet werden.")
return []
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
selected_frames = []
successful_frame_count = 0
previous_frame = None
for i in range(total_frames):
ret, frame = cap.read()
if not ret:
break
sharpness = self.measure_sharpness(frame)
if sharpness < self.sharpness_threshold:
continue
if previous_frame is not None:
overlap = self.frames_overlap(previous_frame, frame)
if overlap >= self.overlap_threshold:
processed_frame = self.adjust_brightness(frame)
processed_frame = self.adjust_contrast(processed_frame)
processed_frame = self.adjust_saturation(processed_frame)
processed_frame = self.shadow_removal(processed_frame)
processed_frame = self.sharpen_image(processed_frame)
frame_name = f"{basename}_frame_{successful_frame_count:05d}.png"
frame_path = os.path.join(self.output_dir, frame_name)
cv2.imwrite(frame_path, processed_frame)
selected_frames.append(frame_path)
successful_frame_count += 1
if successful_frame_count % 10 == 0 or successful_frame_count == 1:
self.log_message(f"{basename}: Frame {i+1} - {successful_frame_count} Frames extrahiert.")
previous_frame = processed_frame.copy()
else:
processed_frame = self.adjust_brightness(frame)
processed_frame = self.adjust_contrast(processed_frame)
processed_frame = self.adjust_saturation(processed_frame)
processed_frame = self.shadow_removal(processed_frame)
processed_frame = self.sharpen_image(processed_frame)
frame_name = f"{basename}_frame_{successful_frame_count:05d}.png"
frame_path = os.path.join(self.output_dir, frame_name)
cv2.imwrite(frame_path, processed_frame)
selected_frames.append(frame_path)
successful_frame_count += 1
previous_frame = processed_frame.copy()
if successful_frame_count % 10 == 0 or successful_frame_count == 1:
self.log_message(f"{basename}: Frame {i+1} - {successful_frame_count} Frames extrahiert.")
progress_percent = int((i + 1) / total_frames * 100)
if (i + 1) % max(total_frames // 100, 1) == 0 or i == total_frames -1:
self.progress.emit(progress_percent)
cap.release()
self.log_message(f"{basename}: Extraktion abgeschlossen. {successful_frame_count} Frames extrahiert.")
return selected_frames
except Exception as e:
self.log_message(f"Fehler während der Extraktion von '{video_path}': {str(e)}")
return []
def run(self):
all_selected_frames = []
total_videos = len(self.video_paths)
for idx, video_path in enumerate(self.video_paths, 1):
basename = os.path.splitext(os.path.basename(video_path))[0]
frames = self.process_video(video_path, basename)
all_selected_frames.extend(frames)
overall_progress = int((idx / total_videos) * 100) if total_videos > 0 else 100
self.progress.emit(overall_progress)
self.log_message(f"Gesamtextraktion abgeschlossen. Insgesamt {len(all_selected_frames)} Frames extrahiert.")
self.finished.emit(all_selected_frames)
class FrameExtractorThread(QThread):
"""
Thread zur Ausführung der FrameExtractor-Objektmethoden.
"""
def __init__(self, extractor: FrameExtractor):
super().__init__()
self.extractor = extractor
def run(self):
self.extractor.run()
class FrameExtractorUI(QtWidgets.QWidget):
"""
Benutzeroberfläche für den Video Frame Extractor.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Videoframe-Extraktor")
self.setup_ui()
def setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# Video Auswahl Abschnitt
video_group = QGroupBox("Videodateien und Ordner")
video_layout = QHBoxLayout()
video_layout.setSpacing(5)
# Für das Video-Auswahl-Widget
self.video_path_edit = DropLineEdit(accept_file=True, accept_dir=True)
self.video_path_edit.setPlaceholderText("Ziehen Sie Videodateien oder Ordner hierher oder klicken Sie auf Durchsuchen")
self.video_path_edit.setToolTip("Wählen Sie eine oder mehrere Videodateien oder ganze Ordner aus, indem Sie sie durchsuchen oder hierher ziehen.")
self.video_path_edit.setStyleSheet("min-height: 30px;")
# Für das Ausgabeordner-Auswahl-Widget
self.output_path_edit = DropLineEdit(accept_dir=True)
self.output_path_edit.setPlaceholderText("Ziehen Sie einen Ausgabeordner hierher oder klicken Sie auf Durchsuchen")
self.output_path_edit.setToolTip("Wählen Sie einen Ausgabeordner aus, indem Sie ihn durchsuchen oder hierher ziehen.")
self.output_path_edit.setStyleSheet("min-height: 30px;")
# Für das Bilder-Laden-Widget in der ImageQualityCheckerUI
self.load_path_edit = DropLineEdit(accept_dir=True, accept_file=True)
self.load_path_edit.setPlaceholderText("Ziehen Sie Bilder oder Ordner hierher oder klicken Sie auf Laden")
self.load_path_edit.setToolTip("Ziehen Sie einzelne Bilddateien oder ganze Ordner mit Bildern hierher oder klicken Sie auf Laden zum Durchsuchen.")
self.load_path_edit.setStyleSheet("min-height: 30px;")
video_icon = QLabel()
video_pixmap = QIcon.fromTheme("video-x-generic").pixmap(24, 24)
if video_pixmap.isNull():
video_pixmap = QPixmap(24, 24)
video_pixmap.fill(Qt.transparent)
video_icon.setPixmap(video_pixmap)
video_icon.setFixedSize(28, 28)
browse_button = QPushButton("Durchsuchen")
browse_button.setToolTip("Durchsuchen Sie Ihr System nach Videodateien oder Ordnern.")
browse_button.setFixedWidth(100)
browse_button.clicked.connect(self.browse_video)
video_layout.addWidget(video_icon)
video_layout.addWidget(self.video_path_edit)
video_layout.addWidget(browse_button)
video_group.setLayout(video_layout)
main_layout.addWidget(video_group)
# Ausgabeordner Auswahl Abschnitt
output_group = QGroupBox("Ausgabeordner")
output_layout = QHBoxLayout()
output_layout.setSpacing(5)
output_icon = QLabel()
output_pixmap = QIcon.fromTheme("folder").pixmap(24, 24)
if output_pixmap.isNull():
output_pixmap = QPixmap(24, 24)
output_pixmap.fill(Qt.transparent)
output_icon.setPixmap(output_pixmap)
output_icon.setFixedSize(28, 28)
browse_output_button = QPushButton("Durchsuchen")
browse_output_button.setToolTip("Durchsuchen Sie Ihr System nach einem Ausgabeordner.")
browse_output_button.setFixedWidth(100)
browse_output_button.clicked.connect(self.browse_output)
output_layout.addWidget(output_icon)
output_layout.addWidget(self.output_path_edit)
output_layout.addWidget(browse_output_button)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
# Einstellungen Gruppe
settings_group = QGroupBox("Einstellungen")
settings_layout = QFormLayout()
settings_layout.setSpacing(8)
# Schärfe Schwelle
sharpness_layout = QHBoxLayout()
self.sharpness_slider = QSlider(Qt.Horizontal)
self.sharpness_slider.setMinimum(100)
self.sharpness_slider.setMaximum(1000)
self.sharpness_slider.setValue(300)
self.sharpness_slider.setToolTip("Stellen Sie den minimalen Schärfe-Threshold für die Frame-Auswahl ein.")
self.sharpness_slider.setTickPosition(QSlider.TicksBelow)
self.sharpness_slider.setTickInterval(100)
self.sharpness_slider.setFixedWidth(200)
self.sharpness_value = QLabel("300")
self.sharpness_value.setFixedWidth(30)
self.sharpness_slider.valueChanged.connect(
lambda val: self.sharpness_value.setText(str(val))
)
sharpness_layout.addWidget(self.sharpness_slider)
sharpness_layout.addWidget(self.sharpness_value)
settings_layout.addRow(QLabel("Schärfe Schwelle:"), sharpness_layout)
# Überlappungs-Schwelle (Korrelation, 0-1)
overlap_layout = QHBoxLayout()
self.overlap_slider = QSlider(Qt.Horizontal)
self.overlap_slider.setMinimum(0)
self.overlap_slider.setMaximum(100)
self.overlap_slider.setValue(50)
self.overlap_slider.setToolTip("Stellen Sie die Überlappungsschwelle zur Bestimmung der Frame-Ähnlichkeit ein.")
self.overlap_slider.setTickPosition(QSlider.TicksBelow)
self.overlap_slider.setTickInterval(10)
self.overlap_slider.setFixedWidth(200)
self.overlap_value = QLabel("0.50")
self.overlap_value.setFixedWidth(30)
self.overlap_slider.valueChanged.connect(
lambda val: self.overlap_value.setText(f"{val / 100:.2f}")
)
overlap_layout.addWidget(self.overlap_slider)
overlap_layout.addWidget(self.overlap_value)
settings_layout.addRow(QLabel("Überlappungsschwelle:"), overlap_layout)
# Helligkeitsanpassung
brightness_layout = QHBoxLayout()
self.brightness_slider = QSlider(Qt.Horizontal)
self.brightness_slider.setMinimum(-100)
self.brightness_slider.setMaximum(100)
self.brightness_slider.setValue(0)
self.brightness_slider.setToolTip("Passen Sie die Helligkeit der extrahierten Frames an.")
self.brightness_slider.setTickPosition(QSlider.TicksBelow)
self.brightness_slider.setTickInterval(50)
self.brightness_slider.setFixedWidth(200)
self.brightness_value = QLabel("0")
self.brightness_value.setFixedWidth(30)
self.brightness_slider.valueChanged.connect(
lambda val: self.brightness_value.setText(str(val))
)
brightness_layout.addWidget(self.brightness_slider)
brightness_layout.addWidget(self.brightness_value)
settings_layout.addRow(QLabel("Helligkeit Anpassung:"), brightness_layout)
# Kontrastanpassung
contrast_layout = QHBoxLayout()
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setMinimum(-100)
self.contrast_slider.setMaximum(100)
self.contrast_slider.setValue(0)
self.contrast_slider.setToolTip("Passen Sie den Kontrast der extrahierten Frames an.")
self.contrast_slider.setTickPosition(QSlider.TicksBelow)
self.contrast_slider.setTickInterval(50)
self.contrast_slider.setFixedWidth(200)
self.contrast_value = QLabel("0")
self.contrast_value.setFixedWidth(30)
self.contrast_slider.valueChanged.connect(
lambda val: self.contrast_value.setText(str(val))
)
contrast_layout.addWidget(self.contrast_slider)
contrast_layout.addWidget(self.contrast_value)
settings_layout.addRow(QLabel("Kontrast Anpassung:"), contrast_layout)
# Sättigungsanpassung
saturation_layout = QHBoxLayout()
self.saturation_slider = QSlider(Qt.Horizontal)
self.saturation_slider.setMinimum(-100)
self.saturation_slider.setMaximum(100)
self.saturation_slider.setValue(0)
self.saturation_slider.setToolTip("Passen Sie die Sättigung der extrahierten Frames an.")
self.saturation_slider.setTickPosition(QSlider.TicksBelow)
self.saturation_slider.setTickInterval(50)
self.saturation_slider.setFixedWidth(200)
self.saturation_value = QLabel("0")
self.saturation_value.setFixedWidth(30)
self.saturation_slider.valueChanged.connect(
lambda val: self.saturation_value.setText(str(val))
)
saturation_layout.addWidget(self.saturation_slider)
saturation_layout.addWidget(self.saturation_value)
settings_layout.addRow(QLabel("Sättigung Anpassung:"), saturation_layout)
# Schattenentfernung
self.shadow_removal_checkbox = QCheckBox("Schattenentfernung aktivieren")
self.shadow_removal_checkbox.setChecked(True)
self.shadow_removal_checkbox.setToolTip("Aktivieren oder deaktivieren Sie die Schattenentfernung in den extrahierten Frames.")
settings_layout.addRow(self.shadow_removal_checkbox)
settings_group.setLayout(settings_layout)
main_layout.addWidget(settings_group)
# Start Button
self.start_button = QPushButton("Extraktion Starten")
self.start_button.setToolTip("Starten Sie den Frame-Extraktionsprozess.")
self.start_button.setFixedHeight(35)
self.start_button.clicked.connect(self.start_extraction)
main_layout.addWidget(self.start_button)
# Fortschritt Balken und Label
progress_group = QGroupBox("Fortschritt")
progress_layout = QHBoxLayout()
progress_layout.setSpacing(5)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setToolTip("Zeigt den Fortschritt der Frame-Extraktion an.")
self.progress_bar.setFixedHeight(20)
self.progress_label = QLabel("Fortschritt: 0%")
self.progress_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
main_layout.addWidget(progress_group)
# Log Text
log_group = QGroupBox("Protokoll")
log_layout = QVBoxLayout()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setToolTip("Zeigt Log-Nachrichten während der Frame-Extraktion an.")
log_layout.addWidget(self.log_text)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
# Ausgewählte Frames Liste
frames_group = QGroupBox("Ausgewählte Frames")
frames_layout = QVBoxLayout()
self.selected_frames_list = QListWidget()
self.selected_frames_list.setToolTip("Liste der extrahierten Frames. Klicken Sie, um eine Vorschau anzuzeigen.")
self.selected_frames_list.itemClicked.connect(self.preview_frame)
remove_button = QPushButton("Ausgewählten Frame Entfernen")
remove_button.setToolTip("Entfernen Sie den ausgewählten Frame aus der Liste.")
remove_button.setFixedHeight(30)
remove_button.clicked.connect(self.remove_selected_frame)
frames_layout.addWidget(self.selected_frames_list)
frames_layout.addWidget(remove_button)
frames_group.setLayout(frames_layout)
main_layout.addWidget(frames_group)
# Vorschau Abschnitt
preview_group = QGroupBox("Vorschau")
preview_layout = QVBoxLayout()
self.preview_image = PreviewLabel()
preview_layout.addWidget(self.preview_image)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
# Stretch hinzufügen
main_layout.addStretch()
# Verbinde das Signal für Dateien/Folders, die gezogen wurden
self.video_path_edit.files_dropped.connect(self.handle_video_dropped)
self.output_path_edit.files_dropped.connect(self.handle_output_dropped)
# Initiale Zustände setzen
self.update_start_button_state()
def browse_video(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen von Videodateien oder Ordnern.
"""
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
files, _ = QFileDialog.getOpenFileNames(
self, "Videodateien auswählen", "", "Videos (*.mp4 *.avi *.mov *.mkv)", options=options
)
if files:
self.video_path_edit.setText('; '.join(files))
self.update_start_button_state()
def browse_output(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen eines Ausgabeordners.
"""
dir_dialog = QFileDialog()
path = dir_dialog.getExistingDirectory(self, "Ausgabeordner auswählen")
if path:
self.output_path_edit.setText(path)
self.update_start_button_state()
def handle_video_dropped(self, paths: list):
"""
Verarbeitet die gedroppten Videodateien oder Ordner.
"""
self.update_start_button_state()
def handle_output_dropped(self, paths: list):
"""
Verarbeitet den gedroppten Ausgabeordner.
"""
if paths and os.path.isdir(paths[0]):
self.output_path_edit.setText(paths[0])
self.update_start_button_state()
def update_start_button_state(self):
"""
Aktiviert oder deaktiviert den Start-Button basierend auf der Eingabe.
"""
video_text = self.video_path_edit.text()
output_text = self.output_path_edit.text()
self.start_button.setEnabled(bool(video_text and output_text))
def start_extraction(self):
"""
Startet den Frame-Extraktionsprozess nach Überprüfung der Eingaben.
"""
video_paths_text = self.video_path_edit.text()
output_dir = self.output_path_edit.text()
sharpness_threshold = self.sharpness_slider.value()
overlap_threshold = self.overlap_slider.value() / 100.0
brightness_adjustment = self.brightness_slider.value()
contrast_adjustment = self.contrast_slider.value()
saturation_adjustment = self.saturation_slider.value()
shadow_removal_enabled = self.shadow_removal_checkbox.isChecked()
video_paths = [path.strip() for path in video_paths_text.split(';') if path.strip()]
if not video_paths:
QMessageBox.critical(self, "Fehler", "Die ausgewählten Pfade sind ungültig.")
return
if not os.path.isdir(output_dir):
try:
os.makedirs(output_dir, exist_ok=True)
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Ausgabeordner konnte nicht erstellt werden: {str(e)}")
return
self.start_button.setEnabled(False)
self.log_text.clear()
self.progress_bar.setValue(0)
self.progress_label.setText("Fortschritt: 0%")
self.selected_frames_list.clear()
self.preview_image.clear()
self.extractor = FrameExtractor(
video_paths, output_dir, sharpness_threshold, overlap_threshold,
brightness_adjustment, shadow_removal_enabled, contrast_adjustment,
saturation_adjustment
)
self.thread = FrameExtractorThread(self.extractor)
self.extractor.moveToThread(self.thread)
self.thread.started.connect(self.extractor.run)
self.extractor.progress.connect(self.update_progress)
self.extractor.log.connect(self.update_log)
self.extractor.finished.connect(self.extraction_finished)
self.extractor.finished.connect(self.thread.quit)
self.extractor.finished.connect(self.extractor.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
def update_progress(self, value: int):
"""
Aktualisiert den Fortschrittsbalken und das Label.
"""
self.progress_bar.setValue(value)
self.progress_label.setText(f"Fortschritt: {value}%")
def update_log(self, message: str):
"""
Fügt eine neue Log-Nachricht hinzu.
"""
self.log_text.append(message)
def extraction_finished(self, frames: list):
"""
Wird aufgerufen, wenn die Extraktion abgeschlossen ist.
"""
total_extracted = len(frames)
self.log_text.append(f"Extraktion abgeschlossen. {total_extracted} Frames extrahiert.")
self.start_button.setEnabled(True)
self.selected_frames_list.addItems(frames)
def remove_selected_frame(self):
"""
Entfernt den ausgewählten Frame aus der Liste.
"""
selected_items = self.selected_frames_list.selectedItems()
if not selected_items:
return
for item in selected_items:
self.selected_frames_list.takeItem(self.selected_frames_list.row(item))
self.preview_image.clear()
def preview_frame(self, item):
"""
Zeigt eine Vorschau des ausgewählten Frames an.
"""
frame_path = item.text()
if not os.path.isfile(frame_path):
self.log_text.append(f"Vorschau nicht verfügbar: {frame_path} existiert nicht.")
return
image = QImage(frame_path)
if image.isNull():
self.log_text.append(f"Bild konnte nicht geladen werden: {frame_path}")
return
pixmap = QPixmap.fromImage(image)
self.preview_image.setPixmap(pixmap)
class ImageQualityChecker(QtCore.QObject):
"""
Bewertet die Qualität von Bildern basierend auf verschiedenen Metriken.
"""
log = pyqtSignal(str)
progress = pyqtSignal(int)
finished = pyqtSignal(list)
def __init__(self):
super().__init__()
self.image_files = []
self.result_files = []
self.min_quality = 0
self.mutex = QMutex()
def load_images(self, files: list):
with QMutexLocker(self.mutex):
self.image_files = []
for path in files:
if os.path.isdir(path):
supported_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')
for root, dirs, files_in_dir in os.walk(path):
for file in files_in_dir:
if file.lower().endswith(supported_ext):
self.image_files.append(os.path.join(root, file))
elif os.path.isfile(path):
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')):
self.image_files.append(path)
def compute_quality(self, image_path: str, reference_gray: np.ndarray) -> int:
try:
image = Image.open(image_path).convert('RGB')
brightness = self.compute_brightness(image)
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
lap_var = cv2.Laplacian(gray, cv2.CV_64F).var()
sharpness = min(100, int(lap_var / 100.0))
ssim_score = 100
if reference_gray is not None:
try:
ssim_index = ssi(reference_gray, gray)
ssim_score = max(0, min(100, int(ssim_index * 100)))
except Exception as e:
self.log.emit(f"SSIM Fehler für {os.path.basename(image_path)}: {str(e)}")
ssim_score = 0
quality = min(100, (brightness + sharpness + ssim_score) // 3)
return quality
except Exception as e:
self.log.emit(f"Fehler bei der Verarbeitung von {os.path.basename(image_path)}: {str(e)}")
return 0
def compute_brightness(self, image: Image.Image) -> int:
grayscale_image = image.convert('L')
histogram = grayscale_image.histogram()
total_pixels = sum(histogram)
brightness = sum(i * hist for i, hist in enumerate(histogram)) / total_pixels
return int((brightness / 255) * 100)
def evaluate_quality(self, min_quality: int):
self.result_files.clear()
with QMutexLocker(self.mutex):
images = list(self.image_files)
if not images:
self.log.emit("Keine Bilder zum Bewerten geladen.")
self.finished.emit([])
return
reference_gray = None
if images:
try:
reference = cv2.imread(images[0], cv2.IMREAD_GRAYSCALE)
if reference is not None:
reference_gray = reference
except Exception as e:
self.log.emit(f"Fehler beim Laden des Referenzbildes: {str(e)}")
reference_gray = None
total = len(images)
for idx, file in enumerate(images):
quality = self.compute_quality(file, reference_gray)
if quality >= min_quality:
self.result_files.append(file)
self.log.emit(f"{os.path.basename(file)} - Qualität: {quality}")
progress_percent = int((idx + 1) / total * 100) if total > 0 else 100
if (idx + 1) % max(total // 100, 1) == 0 or idx == total - 1:
self.progress.emit(progress_percent)
self.log.emit(f"Bewertung abgeschlossen. {len(self.result_files)} Bilder erfüllen die Qualitätskriterien.")
self.finished.emit(self.result_files)
def get_results(self) -> list:
return self.result_files
class ImageQualityCheckerUI(QtWidgets.QWidget):
"""
Benutzeroberfläche für den Image Quality Checker.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Bildqualitätsprüfer")
self.load_path_edit = DropLineEdit(accept_dir=True, accept_file=True) # Hinzufügen der Initialisierung
self.setup_ui()
self.image_quality_checker = ImageQualityChecker()
self.setup_signals()
def setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# Bilder Laden Abschnitt
load_group = QGroupBox("Bilder und Ordner laden")
load_layout = QHBoxLayout()
load_layout.setSpacing(5)
load_icon = QLabel()
load_pixmap = QIcon.fromTheme("image-x-generic").pixmap(24, 24)
if load_pixmap.isNull():
load_pixmap = QPixmap(24, 24)
load_pixmap.fill(Qt.transparent)
load_icon.setPixmap(load_pixmap)
load_icon.setFixedSize(28, 28)
load_button = QPushButton("Laden")
load_button.setToolTip("Laden Sie Bilder aus einem Ordner oder einzelne Bilder, indem Sie sie durchsuchen oder hierher ziehen.")
load_button.setFixedWidth(100)
load_button.setFixedHeight(30)
load_button.clicked.connect(self.browse_folder)
load_layout.addWidget(load_icon)
load_layout.addWidget(self.load_path_edit)
load_layout.addWidget(load_button)
load_group.setLayout(load_layout)
main_layout.addWidget(load_group)
# Minimale Qualitäts-Eingabe
quality_group = QGroupBox("Qualitätskriterien")
quality_layout = QFormLayout()
quality_layout.setSpacing(8)
self.min_quality_label = QLabel("Minimale Qualität (0-100):")
self.min_quality_entry = QLineEdit()
self.min_quality_entry.setPlaceholderText("z.B. 50")
self.min_quality_entry.setToolTip("Geben Sie die minimale Qualitätsschwelle ein. Bilder mit höherer Qualität werden ausgewählt.")
self.min_quality_entry.setFixedWidth(100)
self.min_quality_entry.setValidator(QtGui.QIntValidator(0, 100, self))
quality_layout.addRow(self.min_quality_label, self.min_quality_entry)
quality_group.setLayout(quality_layout)
main_layout.addWidget(quality_group)
# Bewertung Button
self.evaluate_button = QPushButton("Qualität Bewerten")
self.evaluate_button.setToolTip("Starten Sie die Bewertung der geladenen Bilder.")
self.evaluate_button.setFixedHeight(35)
self.evaluate_button.clicked.connect(self.evaluate_quality)
main_layout.addWidget(self.evaluate_button)
# Fortschritt Balken und Label
progress_group = QGroupBox("Fortschritt")
progress_layout = QHBoxLayout()
progress_layout.setSpacing(5)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setToolTip("Zeigt den Fortschritt der Qualitätsbewertung an.")
self.progress_bar.setFixedHeight(20)
self.progress_label = QLabel("Fortschritt: 0%")
self.progress_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
main_layout.addWidget(progress_group)
# Log Text
log_group = QGroupBox("Ergebnisse")
log_layout = QVBoxLayout()
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setToolTip("Zeigt Log-Nachrichten während der Qualitätsbewertung an.")
log_layout.addWidget(self.result_text)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
# Ausgewählte Ergebnisse Liste
results_group = QGroupBox("Hochwertige Bilder")
results_layout = QVBoxLayout()
self.selected_results_list = QListWidget()
self.selected_results_list.setToolTip("Liste der hochwertigen Bilder. Klicken Sie, um eine Vorschau anzuzeigen.")
self.selected_results_list.itemClicked.connect(self.preview_image_clicked)
remove_button = QPushButton("Ausgewähltes Bild Entfernen")
remove_button.setToolTip("Entfernen Sie das ausgewählte Bild aus den Ergebnissen.")
remove_button.setFixedHeight(30)
remove_button.clicked.connect(self.remove_selected_image)
results_layout.addWidget(self.selected_results_list)
results_layout.addWidget(remove_button)
results_group.setLayout(results_layout)
main_layout.addWidget(results_group)
# Vorschau Abschnitt
preview_group = QGroupBox("Vorschau")
preview_layout = QVBoxLayout()
self.preview_image = PreviewLabel()
preview_layout.addWidget(self.preview_image)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
# Stretch hinzufügen
main_layout.addStretch()
# Verbinde das Signal für Dateien/Folders, die gezogen wurden
self.load_path_edit.files_dropped.connect(self.handle_files_dropped)
def setup_signals(self):
self.image_quality_checker.log.connect(self.update_log)
self.image_quality_checker.progress.connect(self.update_progress)
self.image_quality_checker.finished.connect(self.evaluation_finished)
def browse_folder(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen von Bildordnern oder Einzelbildern.
"""
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
files, _ = QFileDialog.getOpenFileNames(
self, "Bilddateien auswählen", "", "Bilder (*.png *.jpg *.jpeg *.gif *.bmp *.tiff *.webp)", options=options
)
if files:
self.load_path_edit.setText('; '.join(files))
self.load_images_from_paths(files)
def handle_files_dropped(self, paths: list):
"""
Verarbeitet die gedroppten Bilddateien oder Ordner.
"""
self.load_images_from_paths(paths)
def load_images_from_paths(self, paths: list):
"""
Lädt Bilder aus den angegebenen Pfaden.
"""
if not paths:
return
self.image_quality_checker.load_images(paths)
self.update_listbox()
self.result_text.append(f"{len(self.image_quality_checker.image_files)} Bilder geladen.")
def evaluate_quality(self):
"""
Startet den Qualitätsbewertungsprozess.
"""
min_quality_text = self.min_quality_entry.text()
try:
min_quality = int(min_quality_text)
if not (0 <= min_quality <= 100):
raise ValueError
self.image_quality_checker.min_quality = min_quality
except ValueError:
QMessageBox.critical(
self, "Ungültige Eingabe", "Bitte geben Sie eine gültige Zahl zwischen 0 und 100 für die minimale Qualität ein."
)
return
if not self.image_quality_checker.image_files:
QMessageBox.information(
self, "Keine Bilder", "Bitte laden Sie Bilder, bevor Sie die Qualität bewerten."
)
return
self.result_text.clear()
self.evaluate_button.setEnabled(False)
self.load_path_edit.setEnabled(False)
self.selected_results_list.clear()
self.preview_image.clear()
self.result_text.append("Starte Qualitätsbewertung...\n")
# Initialize thread and move checker to it
self.thread = QThread()
self.image_quality_checker.moveToThread(self.thread)
self.thread.started.connect(lambda: self.image_quality_checker.evaluate_quality(self.image_quality_checker.min_quality))
self.image_quality_checker.finished.connect(self.thread.quit)
self.image_quality_checker.finished.connect(self.evaluation_finished)
self.image_quality_checker.finished.connect(self.image_quality_checker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
def update_log(self, message: str):
"""
Fügt eine neue Log-Nachricht hinzu.
"""
self.result_text.append(message)
def update_progress(self, value: int):
"""
Aktualisiert den Fortschrittsbalken und das Label.
"""
self.progress_bar.setValue(value)
self.progress_label.setText(f"Fortschritt: {value}%")
def evaluation_finished(self, results: list):
"""
Wird aufgerufen, wenn die Qualitätsbewertung abgeschlossen ist.
"""
self.evaluate_button.setEnabled(True)
self.load_path_edit.setEnabled(True)
if results:
self.result_text.append("\nBewertung abgeschlossen.")
self.result_text.append(f"Anzahl der Bilder, die den Qualitätskriterien entsprechen: {len(results)}")
self.selected_results_list.addItems(results)
else:
self.result_text.append("\nKeine Bilder erfüllen die minimalen Qualitätsanforderungen.")
self.progress_bar.setValue(100)
self.progress_label.setText("Fortschritt: 100%")
def remove_selected_image(self):
"""
Entfernt das ausgewählte Bild aus der Ergebnisliste.
"""
selected_items = self.selected_results_list.selectedItems()
if not selected_items:
return
for item in selected_items:
self.selected_results_list.takeItem(self.selected_results_list.row(item))
self.preview_image.clear()
def preview_image_clicked(self, item):
"""
Zeigt eine Vorschau des ausgewählten Bildes an.
"""
image_path = item.text()
if not os.path.isfile(image_path):
self.result_text.append(f"Vorschau nicht verfügbar: {image_path} existiert nicht.")
return
image = QImage(image_path)
if image.isNull():
self.result_text.append(f"Bild konnte nicht geladen werden: {image_path}")
return
pixmap = QPixmap.fromImage(image)
self.preview_image.setPixmap(pixmap)
def update_listbox(self):
"""
Aktualisiert die Liste der geladenen Bilder.
"""
self.selected_results_list.clear()
self.selected_results_list.addItems([os.path.basename(f) for f in self.image_quality_checker.image_files])
self.preview_image.clear()
class MainWindow(QtWidgets.QMainWindow):
"""
Hauptfenster der Anwendung mit Tabs für verschiedene Funktionen.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Bildverarbeitungsanwendung")
self.setGeometry(100, 100, 1200, 800)
self.setup_ui()
def setup_ui(self):
self.tabs = QTabWidget()
self.tabs.setTabPosition(QTabWidget.North)
self.tabs.setMovable(False)
self.setCentralWidget(self.tabs)
self.frame_extractor_widget = FrameExtractorUI()
self.image_quality_checker_widget = ImageQualityCheckerUI()
self.tabs.addTab(self.frame_extractor_widget, QIcon.fromTheme("video-x-generic"), "Video Frame Extraktor")
self.tabs.addTab(self.image_quality_checker_widget, QIcon.fromTheme("image-x-generic"), "Bildqualitätsprüfer")
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setStyleSheet(DARK_STYLE)
# Anpassung der Fenstergröße, um kompakter zu sein
window = MainWindow()
window.setMinimumSize(1000, 700)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()ASSISTANT
Um Ihre Anwendung zu verbessern und sicherzustellen, dass das GUI während der Bearbeitungsprozesse aktiv bleibt, habe ich die folgenden Änderungen vorgenommen:
1. **Multithreading Optimierung**: Alle langen und rechenintensiven Prozesse wurden in separate Threads ausgelagert, um das Einfrieren des GUI zu verhindern. Dabei wurde die Verwendung von `QThread` beibehalten und optimiert.
2. **Verbesserte Lesbarkeit und Übersichtlichkeit**:
- **Größere und formatierte Texte**: Die Schriftgrößen wurden erhöht und wichtige Elemente wie Labels und Buttons wurden fett hervorgehoben.
- **Farbliche Hervorhebungen**: Wichtige Buttons und Labels wurden durch Farben und Stiländerungen deutlicher erkennbar gemacht.
- **Verbesserte Layouts**: Die Layouts wurden angepasst, um eine bessere Struktur und Übersichtlichkeit zu gewährleisten.
3. **Responsive GUI**: Durch die Optimierung der Thread-Verwendung bleibt das GUI stets responsiv, auch während intensiver Bearbeitungsprozesse.
4. **Weitere Verbesserungen**:
- **Tooltips und Hilfetexte**: Zusätzliche Tooltips wurden hinzugefügt, um die Benutzerfreundlichkeit zu erhöhen.
- **Fortschrittsanzeigen**: Fortschrittsbalken und Labels wurden klarer gestaltet, um den Fortschritt besser sichtbar zu machen.
- **Fehlerbehandlung**: Verbesserte Fehlerbehandlung und -berichterstattung für eine robustere Anwendung.
Nachfolgend finden Sie das vollständige, verbesserte Skript der Anwendung:
```python
import sys
import os
import cv2
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont
from PyQt5.QtWidgets import (
QLabel, QTabWidget, QTextEdit, QLineEdit, QPushButton,
QListWidget, QVBoxLayout, QFileDialog, QHBoxLayout, QGroupBox,
QFormLayout, QSlider, QCheckBox, QProgressBar, QMainWindow, QApplication, QMessageBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QMutexLocker
from skimage.metrics import structural_similarity as ssi
from PIL import Image
# Modern Dark mode stylesheet with increased font sizes and better visibility
DARK_STYLE = """
/* Allgemeine Einstellungen */
QWidget {
background-color: #2b2b2b;
color: #e0e0e0;
font-family: 'Segoe UI', sans-serif;
font-size: 12pt;
}
/* Fenster Titel */
QMainWindow {
background-color: #2b2b2b;
}
/* Buttons */
QPushButton {
background-color: #3c3f41;
border: 2px solid #5a5a5a;
padding: 8px 16px;
border-radius: 6px;
font-weight: bold;
font-size: 12pt;
}
QPushButton:hover {
background-color: #505253;
}
QPushButton:pressed {
background-color: #2b2b2b;
}
QPushButton:disabled {
background-color: #3c3f4166;
color: #8a8a8a;
border: 1px solid #5a5a5a66;
}
/* Eingabe-/Ausgabefelder */
QLineEdit, QTextEdit, QListWidget, QLabel, QSlider, QGroupBox {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
padding: 6px;
border-radius: 4px;
color: #e0e0e0;
font-size: 12pt;
}
QLineEdit:disabled, QTextEdit:disabled, QListWidget:disabled {
background-color: #3c3c3c66;
color: #8a8a8a;
}
/* Slider */
QSlider::groove:horizontal {
border: 1px solid #757575;
height: 8px;
background: #5a5a5a;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #1abc9c;
border: 1px solid #16a085;
width: 14px;
margin: -4px 0;
border-radius: 7px;
}
QSlider::handle:horizontal:hover {
background: #17a589;
}
/* Fortschrittsbalken */
QProgressBar {
background-color: #3c3c3c;
border: 2px solid #5a5a5a;
border-radius: 7px;
text-align: center;
height: 25px;
font-size: 12pt;
}
QProgressBar::chunk {
background-color: #1abc9c;
width: 10px;
margin: 0.5px;
}
/* Tab Widget */
QTabWidget::pane {
border: 2px solid #444;
background-color: #2b2b2b;
border-radius: 6px;
}
QTabBar::tab {
background: #3c3c3c;
border: 2px solid #444;
padding: 10px 16px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
margin-right: 2px;
font-weight: bold;
font-size: 12pt;
}
QTabBar::tab:selected, QTabBar::tab:hover {
background: #1abc9c;
color: #2b2b2b;
}
/* DropLineEdit */
DropLineEdit {
border: 3px dashed #5a5a5a;
padding: 12px;
border-radius: 6px;
min-height: 60px;
font-size: 12pt;
}
DropLineEdit.drag_active {
border: 3px dashed #1abc9c;
background-color: #3a3d41;
}
/* GroupBox Title */
QGroupBox {
border: 2px solid #5a5a5a;
border-radius: 7px;
margin-top: 20px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 5px 0 5px;
color: #1abc9c;
font-weight: bold;
font-size: 14pt;
}
/* Labels */
QLabel {
font-weight: bold;
font-size: 12pt;
}
/* Listen */
QListWidget {
selection-background-color: #1abc9c;
selection-color: #2b2b2b;
font-size: 12pt;
}
/* Checkboxes */
QCheckBox {
padding: 6px;
font-size: 12pt;
}
"""
class DropLineEdit(QLineEdit):
"""
A QLineEdit that accepts drag and drop of files or directories with visual feedback.
Supports multiple drops.
"""
files_dropped = pyqtSignal(list)
def __init__(self, accept_dir: bool = False, accept_file: bool = False, parent=None):
super().__init__(parent)
self.accept_dir = accept_dir
self.accept_file = accept_file
self.setAcceptDrops(True)
self.setReadOnly(True)
self.setCursor(Qt.PointingHandCursor)
self.default_style = self.styleSheet()
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
valid = False
for url in urls:
path = url.toLocalFile()
if (self.accept_file and os.path.isfile(path)) or (self.accept_dir and os.path.isdir(path)):
valid = True
break
if valid:
event.acceptProposedAction()
self.setProperty('drag_active', True)
self.style().unpolish(self)
self.style().polish(self)
self.update()
return
event.ignore()
def dragLeaveEvent(self, event):
self.setProperty('drag_active', False)
self.style().unpolish(self)
self.style().polish(self)
self.update()
def dropEvent(self, event):
self.setProperty('drag_active', False)
self.style().unpolish(self)
self.style().polish(self)
self.update()
urls = event.mimeData().urls()
paths = []
for url in urls:
path = url.toLocalFile()
if (self.accept_file and os.path.isfile(path)) or (self.accept_dir and os.path.isdir(path)):
paths.append(path)
if paths:
self.setText('; '.join(paths))
self.files_dropped.emit(paths)
event.acceptProposedAction()
def setStyleSheet(self, style: str):
super().setStyleSheet(style)
class PreviewLabel(QLabel):
"""
A QLabel that displays an image with zoom effect on hover.
"""
def __init__(self):
super().__init__()
self.original_pixmap = None
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
background-color: #3c3c3c;
border: 3px solid #5a5a5a;
border-radius: 6px;
}
""")
self.setScaledContents(False)
def setPixmap(self, pixmap: QPixmap):
if pixmap != self.original_pixmap:
self.original_pixmap = pixmap
scaled_pixmap = pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
super().setPixmap(scaled_pixmap)
def resizeEvent(self, event):
if self.original_pixmap:
scaled_pixmap = self.original_pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
super().setPixmap(scaled_pixmap)
super().resizeEvent(event)
def enterEvent(self, event):
if self.original_pixmap:
zoomed_pixmap = self.original_pixmap.scaled(
self.size() * 1.2,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
super().setPixmap(zoomed_pixmap)
def leaveEvent(self, event):
if self.original_pixmap:
super().setPixmap(
self.original_pixmap.scaled(
self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
)
)
class ImageLoaderThread(QtCore.QThread):
"""
Thread to load image files from a directory.
"""
progress = pyqtSignal(int)
finished = pyqtSignal(list)
def __init__(self, directories: list):
super().__init__()
self.directories = directories
def run(self):
image_files = []
# Supported image extensions
supported_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')
for directory in self.directories:
for root, dirs, files in os.walk(directory):
for file in files:
if file.lower().endswith(supported_ext):
image_files.append(os.path.join(root, file))
total_files = len(image_files)
for idx, file in enumerate(image_files, 1):
progress_percent = int((idx / total_files) * 100) if total_files > 0 else 100
self.progress.emit(progress_percent)
self.msleep(5)
self.finished.emit(image_files)
class FrameExtractor(QtCore.QObject):
"""
Processes a video file to extract frames based on quality metrics.
"""
progress = pyqtSignal(int)
log = pyqtSignal(str)
finished = pyqtSignal(list)
def __init__(self, video_paths: list, output_dir: str, sharpness_threshold: int, overlap_threshold: float,
brightness_adjustment: int, shadow_removal_enabled: bool, contrast_adjustment: int,
saturation_adjustment: int):
super().__init__()
self.video_paths = video_paths
self.output_dir = output_dir
self.sharpness_threshold = sharpness_threshold
self.overlap_threshold = overlap_threshold
self.brightness_adjustment = brightness_adjustment
self.shadow_removal_enabled = shadow_removal_enabled
self.contrast_adjustment = contrast_adjustment
self.saturation_adjustment = saturation_adjustment
def log_message(self, message: str):
self.log.emit(message)
def measure_sharpness(self, frame: np.ndarray) -> float:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
lap = cv2.Laplacian(gray, cv2.CV_64F)
return lap.var()
def frames_overlap(self, frame1: np.ndarray, frame2: np.ndarray) -> float:
hist1 = cv2.calcHist([frame1], [0, 1, 2], None, [8,8,8], [0,256,0,256,0,256])
hist2 = cv2.calcHist([frame2], [0, 1, 2], None, [8,8,8], [0,256,0,256,0,256])
cv2.normalize(hist1, hist1)
cv2.normalize(hist2, hist2)
similarity = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
return similarity
def adjust_brightness(self, frame: np.ndarray) -> np.ndarray:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
v = np.clip(v + self.brightness_adjustment, 0, 255).astype(np.uint8)
final_hsv = cv2.merge((h, s, v))
return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
def adjust_contrast(self, frame: np.ndarray) -> np.ndarray:
alpha = 1 + self.contrast_adjustment / 100.0
return cv2.convertScaleAbs(frame, alpha=alpha, beta=0)
def adjust_saturation(self, frame: np.ndarray) -> np.ndarray:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
s = np.clip(s + self.saturation_adjustment, 0, 255).astype(np.uint8)
final_hsv = cv2.merge((h, s, v))
return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
def shadow_removal(self, frame: np.ndarray) -> np.ndarray:
if not self.shadow_removal_enabled:
return frame
lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)
l_channel, a_channel, b_channel = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
cl = clahe.apply(l_channel)
limg = cv2.merge((cl, a_channel, b_channel))
return cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
def sharpen_image(self, frame: np.ndarray) -> np.ndarray:
kernel = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]])
return cv2.filter2D(frame, -1, kernel)
def process_video(self, video_path: str, basename: str):
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
self.log_message(f"Fehler: Videodatei '{video_path}' konnte nicht geöffnet werden.")
return []
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
selected_frames = []
successful_frame_count = 0
previous_frame = None
for i in range(total_frames):
ret, frame = cap.read()
if not ret:
break
sharpness = self.measure_sharpness(frame)
if sharpness < self.sharpness_threshold:
continue
if previous_frame is not None:
overlap = self.frames_overlap(previous_frame, frame)
if overlap >= self.overlap_threshold:
processed_frame = self.adjust_brightness(frame)
processed_frame = self.adjust_contrast(processed_frame)
processed_frame = self.adjust_saturation(processed_frame)
processed_frame = self.shadow_removal(processed_frame)
processed_frame = self.sharpen_image(processed_frame)
frame_name = f"{basename}_frame_{successful_frame_count:05d}.png"
frame_path = os.path.join(self.output_dir, frame_name)
cv2.imwrite(frame_path, processed_frame)
selected_frames.append(frame_path)
successful_frame_count += 1
if successful_frame_count % 10 == 0 or successful_frame_count == 1:
self.log_message(f"{basename}: Frame {i+1} - {successful_frame_count} Frames extrahiert.")
previous_frame = processed_frame.copy()
else:
processed_frame = self.adjust_brightness(frame)
processed_frame = self.adjust_contrast(processed_frame)
processed_frame = self.adjust_saturation(processed_frame)
processed_frame = self.shadow_removal(processed_frame)
processed_frame = self.sharpen_image(processed_frame)
frame_name = f"{basename}_frame_{successful_frame_count:05d}.png"
frame_path = os.path.join(self.output_dir, frame_name)
cv2.imwrite(frame_path, processed_frame)
selected_frames.append(frame_path)
successful_frame_count += 1
previous_frame = processed_frame.copy()
if successful_frame_count % 10 == 0 or successful_frame_count == 1:
self.log_message(f"{basename}: Frame {i+1} - {successful_frame_count} Frames extrahiert.")
progress_percent = int((i + 1) / total_frames * 100)
if (i + 1) % max(total_frames // 100, 1) == 0 or i == total_frames -1:
self.progress.emit(progress_percent)
cap.release()
self.log_message(f"{basename}: Extraktion abgeschlossen. {successful_frame_count} Frames extrahiert.")
return selected_frames
except Exception as e:
self.log_message(f"Fehler während der Extraktion von '{video_path}': {str(e)}")
return []
def run(self):
all_selected_frames = []
total_videos = len(self.video_paths)
for idx, video_path in enumerate(self.video_paths, 1):
basename = os.path.splitext(os.path.basename(video_path))[0]
frames = self.process_video(video_path, basename)
all_selected_frames.extend(frames)
overall_progress = int((idx / total_videos) * 100) if total_videos > 0 else 100
self.progress.emit(overall_progress)
self.log_message(f"Gesamtextraktion abgeschlossen. Insgesamt {len(all_selected_frames)} Frames extrahiert.")
self.finished.emit(all_selected_frames)
class FrameExtractorThread(QThread):
"""
Thread zur Ausführung der FrameExtractor-Objektmethoden.
"""
def __init__(self, extractor: FrameExtractor):
super().__init__()
self.extractor = extractor
def run(self):
self.extractor.run()
class FrameExtractorUI(QtWidgets.QWidget):
"""
Benutzeroberfläche für den Video Frame Extractor.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Videoframe-Extraktor")
self.setup_ui()
def setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(15)
# Video Auswahl Abschnitt
video_group = QGroupBox("Videodateien und Ordner")
video_layout = QHBoxLayout()
video_layout.setSpacing(10)
# Video-Auswahl-Widget
self.video_path_edit = DropLineEdit(accept_file=True, accept_dir=True)
self.video_path_edit.setPlaceholderText("Ziehen Sie Videodateien oder Ordner hierher oder klicken Sie auf Durchsuchen")
self.video_path_edit.setToolTip("Wählen Sie eine oder mehrere Videodateien oder ganze Ordner aus, indem Sie sie durchsuchen oder hierher ziehen.")
self.video_path_edit.setStyleSheet("min-height: 40px;")
video_icon = QLabel()
video_pixmap = QIcon.fromTheme("video-x-generic").pixmap(32, 32)
if video_pixmap.isNull():
video_pixmap = QPixmap(32, 32)
video_pixmap.fill(Qt.transparent)
video_icon.setPixmap(video_pixmap)
video_icon.setFixedSize(36, 36)
browse_button = QPushButton("Durchsuchen")
browse_button.setToolTip("Durchsuchen Sie Ihr System nach Videodateien oder Ordnern.")
browse_button.setFixedWidth(150)
browse_button.clicked.connect(self.browse_video)
video_layout.addWidget(video_icon)
video_layout.addWidget(self.video_path_edit)
video_layout.addWidget(browse_button)
video_group.setLayout(video_layout)
main_layout.addWidget(video_group)
# Ausgabeordner Auswahl Abschnitt
output_group = QGroupBox("Ausgabeordner")
output_layout = QHBoxLayout()
output_layout.setSpacing(10)
output_icon = QLabel()
output_pixmap = QIcon.fromTheme("folder").pixmap(32, 32)
if output_pixmap.isNull():
output_pixmap = QPixmap(32, 32)
output_pixmap.fill(Qt.transparent)
output_icon.setPixmap(output_pixmap)
output_icon.setFixedSize(36, 36)
self.output_path_edit = DropLineEdit(accept_dir=True)
self.output_path_edit.setPlaceholderText("Ziehen Sie einen Ausgabeordner hierher oder klicken Sie auf Durchsuchen")
self.output_path_edit.setToolTip("Wählen Sie einen Ausgabeordner aus, indem Sie ihn durchsuchen oder hierher ziehen.")
self.output_path_edit.setStyleSheet("min-height: 40px;")
browse_output_button = QPushButton("Durchsuchen")
browse_output_button.setToolTip("Durchsuchen Sie Ihr System nach einem Ausgabeordner.")
browse_output_button.setFixedWidth(150)
browse_output_button.clicked.connect(self.browse_output)
output_layout.addWidget(output_icon)
output_layout.addWidget(self.output_path_edit)
output_layout.addWidget(browse_output_button)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
# Einstellungen Gruppe
settings_group = QGroupBox("Einstellungen")
settings_layout = QFormLayout()
settings_layout.setSpacing(10)
# Schärfe Schwelle
sharpness_layout = QHBoxLayout()
self.sharpness_slider = QSlider(Qt.Horizontal)
self.sharpness_slider.setMinimum(100)
self.sharpness_slider.setMaximum(1000)
self.sharpness_slider.setValue(300)
self.sharpness_slider.setToolTip("Stellen Sie den minimalen Schärfe-Threshold für die Frame-Auswahl ein.")
self.sharpness_slider.setTickPosition(QSlider.TicksBelow)
self.sharpness_slider.setTickInterval(100)
self.sharpness_slider.setFixedWidth(250)
self.sharpness_value = QLabel("300")
self.sharpness_value.setFixedWidth(40)
self.sharpness_slider.valueChanged.connect(
lambda val: self.sharpness_value.setText(str(val))
)
sharpness_layout.addWidget(self.sharpness_slider)
sharpness_layout.addWidget(self.sharpness_value)
settings_layout.addRow(QLabel("Schärfe Schwelle:"), sharpness_layout)
# Überlappungs-Schwelle (Korrelation, 0-1)
overlap_layout = QHBoxLayout()
self.overlap_slider = QSlider(Qt.Horizontal)
self.overlap_slider.setMinimum(0)
self.overlap_slider.setMaximum(100)
self.overlap_slider.setValue(50)
self.overlap_slider.setToolTip("Stellen Sie die Überlappungsschwelle zur Bestimmung der Frame-Ähnlichkeit ein.")
self.overlap_slider.setTickPosition(QSlider.TicksBelow)
self.overlap_slider.setTickInterval(10)
self.overlap_slider.setFixedWidth(250)
self.overlap_value = QLabel("0.50")
self.overlap_value.setFixedWidth(40)
self.overlap_slider.valueChanged.connect(
lambda val: self.overlap_value.setText(f"{val / 100:.2f}")
)
overlap_layout.addWidget(self.overlap_slider)
overlap_layout.addWidget(self.overlap_value)
settings_layout.addRow(QLabel("Überlappungsschwelle:"), overlap_layout)
# Helligkeitsanpassung
brightness_layout = QHBoxLayout()
self.brightness_slider = QSlider(Qt.Horizontal)
self.brightness_slider.setMinimum(-100)
self.brightness_slider.setMaximum(100)
self.brightness_slider.setValue(0)
self.brightness_slider.setToolTip("Passen Sie die Helligkeit der extrahierten Frames an.")
self.brightness_slider.setTickPosition(QSlider.TicksBelow)
self.brightness_slider.setTickInterval(50)
self.brightness_slider.setFixedWidth(250)
self.brightness_value = QLabel("0")
self.brightness_value.setFixedWidth(40)
self.brightness_slider.valueChanged.connect(
lambda val: self.brightness_value.setText(str(val))
)
brightness_layout.addWidget(self.brightness_slider)
brightness_layout.addWidget(self.brightness_value)
settings_layout.addRow(QLabel("Helligkeit Anpassung:"), brightness_layout)
# Kontrastanpassung
contrast_layout = QHBoxLayout()
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setMinimum(-100)
self.contrast_slider.setMaximum(100)
self.contrast_slider.setValue(0)
self.contrast_slider.setToolTip("Passen Sie den Kontrast der extrahierten Frames an.")
self.contrast_slider.setTickPosition(QSlider.TicksBelow)
self.contrast_slider.setTickInterval(50)
self.contrast_slider.setFixedWidth(250)
self.contrast_value = QLabel("0")
self.contrast_value.setFixedWidth(40)
self.contrast_slider.valueChanged.connect(
lambda val: self.contrast_value.setText(str(val))
)
contrast_layout.addWidget(self.contrast_slider)
contrast_layout.addWidget(self.contrast_value)
settings_layout.addRow(QLabel("Kontrast Anpassung:"), contrast_layout)
# Sättigungsanpassung
saturation_layout = QHBoxLayout()
self.saturation_slider = QSlider(Qt.Horizontal)
self.saturation_slider.setMinimum(-100)
self.saturation_slider.setMaximum(100)
self.saturation_slider.setValue(0)
self.saturation_slider.setToolTip("Passen Sie die Sättigung der extrahierten Frames an.")
self.saturation_slider.setTickPosition(QSlider.TicksBelow)
self.saturation_slider.setTickInterval(50)
self.saturation_slider.setFixedWidth(250)
self.saturation_value = QLabel("0")
self.saturation_value.setFixedWidth(40)
self.saturation_slider.valueChanged.connect(
lambda val: self.saturation_value.setText(str(val))
)
saturation_layout.addWidget(self.saturation_slider)
saturation_layout.addWidget(self.saturation_value)
settings_layout.addRow(QLabel("Sättigung Anpassung:"), saturation_layout)
# Schattenentfernung
self.shadow_removal_checkbox = QCheckBox("Schattenentfernung aktivieren")
self.shadow_removal_checkbox.setChecked(True)
self.shadow_removal_checkbox.setToolTip("Aktivieren oder deaktivieren Sie die Schattenentfernung in den extrahierten Frames.")
settings_layout.addRow(self.shadow_removal_checkbox)
settings_group.setLayout(settings_layout)
main_layout.addWidget(settings_group)
# Start Button
self.start_button = QPushButton("Extraktion Starten")
self.start_button.setToolTip("Starten Sie den Frame-Extraktionsprozess.")
self.start_button.setFixedHeight(45)
self.start_button.clicked.connect(self.start_extraction)
main_layout.addWidget(self.start_button)
# Fortschritt Balken und Label
progress_group = QGroupBox("Fortschritt")
progress_layout = QHBoxLayout()
progress_layout.setSpacing(10)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setToolTip("Zeigt den Fortschritt der Frame-Extraktion an.")
self.progress_bar.setFixedHeight(25)
self.progress_label = QLabel("Fortschritt: 0%")
self.progress_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
main_layout.addWidget(progress_group)
# Log Text
log_group = QGroupBox("Protokoll")
log_layout = QVBoxLayout()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setToolTip("Zeigt Log-Nachrichten während der Frame-Extraktion an.")
log_layout.addWidget(self.log_text)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
# Ausgewählte Frames Liste
frames_group = QGroupBox("Ausgewählte Frames")
frames_layout = QVBoxLayout()
self.selected_frames_list = QListWidget()
self.selected_frames_list.setToolTip("Liste der extrahierten Frames. Klicken Sie, um eine Vorschau anzuzeigen.")
self.selected_frames_list.itemClicked.connect(self.preview_frame)
remove_button = QPushButton("Ausgewählten Frame Entfernen")
remove_button.setToolTip("Entfernen Sie den ausgewählten Frame aus der Liste.")
remove_button.setFixedHeight(35)
remove_button.clicked.connect(self.remove_selected_frame)
frames_layout.addWidget(self.selected_frames_list)
frames_layout.addWidget(remove_button)
frames_group.setLayout(frames_layout)
main_layout.addWidget(frames_group)
# Vorschau Abschnitt
preview_group = QGroupBox("Vorschau")
preview_layout = QVBoxLayout()
self.preview_image = PreviewLabel()
preview_layout.addWidget(self.preview_image)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
# Stretch hinzufügen
main_layout.addStretch()
# Verbinde das Signal für Dateien/Folders, die gezogen wurden
self.video_path_edit.files_dropped.connect(self.handle_video_dropped)
self.output_path_edit.files_dropped.connect(self.handle_output_dropped)
# Initiale Zustände setzen
self.update_start_button_state()
def browse_video(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen von Videodateien oder Ordnern.
"""
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
files, _ = QFileDialog.getOpenFileNames(
self, "Videodateien auswählen", "", "Videos (*.mp4 *.avi *.mov *.mkv)", options=options
)
if files:
self.video_path_edit.setText('; '.join(files))
self.update_start_button_state()
def browse_output(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen eines Ausgabeordners.
"""
dir_dialog = QFileDialog()
path = dir_dialog.getExistingDirectory(self, "Ausgabeordner auswählen")
if path:
self.output_path_edit.setText(path)
self.update_start_button_state()
def handle_video_dropped(self, paths: list):
"""
Verarbeitet die gedroppten Videodateien oder Ordner.
"""
self.update_start_button_state()
def handle_output_dropped(self, paths: list):
"""
Verarbeitet den gedroppten Ausgabeordner.
"""
if paths and os.path.isdir(paths[0]):
self.output_path_edit.setText(paths[0])
self.update_start_button_state()
def update_start_button_state(self):
"""
Aktiviert oder deaktiviert den Start-Button basierend auf der Eingabe.
"""
video_text = self.video_path_edit.text()
output_text = self.output_path_edit.text()
self.start_button.setEnabled(bool(video_text and output_text))
def start_extraction(self):
"""
Startet den Frame-Extraktionsprozess nach Überprüfung der Eingaben.
"""
video_paths_text = self.video_path_edit.text()
output_dir = self.output_path_edit.text()
sharpness_threshold = self.sharpness_slider.value()
overlap_threshold = self.overlap_slider.value() / 100.0
brightness_adjustment = self.brightness_slider.value()
contrast_adjustment = self.contrast_slider.value()
saturation_adjustment = self.saturation_slider.value()
shadow_removal_enabled = self.shadow_removal_checkbox.isChecked()
video_paths = [path.strip() for path in video_paths_text.split(';') if path.strip()]
if not video_paths:
QMessageBox.critical(self, "Fehler", "Die ausgewählten Pfade sind ungültig.")
return
if not os.path.isdir(output_dir):
try:
os.makedirs(output_dir, exist_ok=True)
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Ausgabeordner konnte nicht erstellt werden: {str(e)}")
return
self.start_button.setEnabled(False)
self.log_text.clear()
self.progress_bar.setValue(0)
self.progress_label.setText("Fortschritt: 0%")
self.selected_frames_list.clear()
self.preview_image.clear()
self.extractor = FrameExtractor(
video_paths, output_dir, sharpness_threshold, overlap_threshold,
brightness_adjustment, shadow_removal_enabled, contrast_adjustment,
saturation_adjustment
)
self.thread = FrameExtractorThread(self.extractor)
self.extractor.moveToThread(self.thread)
self.thread.started.connect(self.extractor.run)
self.extractor.progress.connect(self.update_progress)
self.extractor.log.connect(self.update_log)
self.extractor.finished.connect(self.extraction_finished)
self.extractor.finished.connect(self.thread.quit)
self.extractor.finished.connect(self.extractor.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
def update_progress(self, value: int):
"""
Aktualisiert den Fortschrittsbalken und das Label.
"""
self.progress_bar.setValue(value)
self.progress_label.setText(f"Fortschritt: {value}%")
def update_log(self, message: str):
"""
Fügt eine neue Log-Nachricht hinzu.
"""
self.log_text.append(message)
def extraction_finished(self, frames: list):
"""
Wird aufgerufen, wenn die Extraktion abgeschlossen ist.
"""
total_extracted = len(frames)
self.log_text.append(f"Extraktion abgeschlossen. {total_extracted} Frames extrahiert.")
self.start_button.setEnabled(True)
self.selected_frames_list.addItems(frames)
def remove_selected_frame(self):
"""
Entfernt den ausgewählten Frame aus der Liste.
"""
selected_items = self.selected_frames_list.selectedItems()
if not selected_items:
return
for item in selected_items:
self.selected_frames_list.takeItem(self.selected_frames_list.row(item))
self.preview_image.clear()
def preview_frame(self, item):
"""
Zeigt eine Vorschau des ausgewählten Frames an.
"""
frame_path = item.text()
if not os.path.isfile(frame_path):
self.log_text.append(f"Vorschau nicht verfügbar: {frame_path} existiert nicht.")
return
image = QImage(frame_path)
if image.isNull():
self.log_text.append(f"Bild konnte nicht geladen werden: {frame_path}")
return
pixmap = QPixmap.fromImage(image)
self.preview_image.setPixmap(pixmap)
class ImageQualityChecker(QtCore.QObject):
"""
Bewertet die Qualität von Bildern basierend auf verschiedenen Metriken.
"""
log = pyqtSignal(str)
progress = pyqtSignal(int)
finished = pyqtSignal(list)
def __init__(self):
super().__init__()
self.image_files = []
self.result_files = []
self.min_quality = 0
self.mutex = QMutex()
def load_images(self, files: list):
with QMutexLocker(self.mutex):
self.image_files = []
for path in files:
if os.path.isdir(path):
supported_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')
for root, dirs, files_in_dir in os.walk(path):
for file in files_in_dir:
if file.lower().endswith(supported_ext):
self.image_files.append(os.path.join(root, file))
elif os.path.isfile(path):
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')):
self.image_files.append(path)
def compute_quality(self, image_path: str, reference_gray: np.ndarray) -> int:
try:
image = Image.open(image_path).convert('RGB')
brightness = self.compute_brightness(image)
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
lap_var = cv2.Laplacian(gray, cv2.CV_64F).var()
sharpness = min(100, int(lap_var / 100.0))
ssim_score = 100
if reference_gray is not None:
try:
ssim_index = ssi(reference_gray, gray)
ssim_score = max(0, min(100, int(ssim_index * 100)))
except Exception as e:
self.log.emit(f"SSIM Fehler für {os.path.basename(image_path)}: {str(e)}")
ssim_score = 0
quality = min(100, (brightness + sharpness + ssim_score) // 3)
return quality
except Exception as e:
self.log.emit(f"Fehler bei der Verarbeitung von {os.path.basename(image_path)}: {str(e)}")
return 0
def compute_brightness(self, image: Image.Image) -> int:
grayscale_image = image.convert('L')
histogram = grayscale_image.histogram()
total_pixels = sum(histogram)
brightness = sum(i * hist for i, hist in enumerate(histogram)) / total_pixels
return int((brightness / 255) * 100)
def evaluate_quality(self, min_quality: int):
self.result_files.clear()
with QMutexLocker(self.mutex):
images = list(self.image_files)
if not images:
self.log.emit("Keine Bilder zum Bewerten geladen.")
self.finished.emit([])
return
reference_gray = None
if images:
try:
reference = cv2.imread(images[0], cv2.IMREAD_GRAYSCALE)
if reference is not None:
reference_gray = reference
except Exception as e:
self.log.emit(f"Fehler beim Laden des Referenzbildes: {str(e)}")
reference_gray = None
total = len(images)
for idx, file in enumerate(images):
quality = self.compute_quality(file, reference_gray)
if quality >= min_quality:
self.result_files.append(file)
self.log.emit(f"{os.path.basename(file)} - Qualität: {quality}")
progress_percent = int((idx + 1) / total * 100) if total > 0 else 100
if (idx + 1) % max(total // 100, 1) == 0 or idx == total - 1:
self.progress.emit(progress_percent)
self.log.emit(f"Bewertung abgeschlossen. {len(self.result_files)} Bilder erfüllen die Qualitätskriterien.")
self.finished.emit(self.result_files)
def get_results(self) -> list:
return self.result_files
class ImageQualityCheckerThread(QThread):
"""
Thread to run ImageQualityChecker.
"""
def __init__(self, checker: ImageQualityChecker):
super().__init__()
self.checker = checker
def run(self):
self.checker.evaluate_quality(self.checker.min_quality)
class ImageQualityCheckerUI(QtWidgets.QWidget):
"""
Benutzeroberfläche für den Image Quality Checker.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Bildqualitätsprüfer")
self.setup_ui()
self.image_quality_checker = ImageQualityChecker()
self.setup_signals()
def setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(15)
# Bilder Laden Abschnitt
load_group = QGroupBox("Bilder und Ordner laden")
load_layout = QHBoxLayout()
load_layout.setSpacing(10)
load_icon = QLabel()
load_pixmap = QIcon.fromTheme("image-x-generic").pixmap(32, 32)
if load_pixmap.isNull():
load_pixmap = QPixmap(32, 32)
load_pixmap.fill(Qt.transparent)
load_icon.setPixmap(load_pixmap)
load_icon.setFixedSize(36, 36)
load_button = QPushButton("Laden")
load_button.setToolTip("Laden Sie Bilder aus einem Ordner oder einzelne Bilder, indem Sie sie durchsuchen oder hierher ziehen.")
load_button.setFixedWidth(150)
load_button.setFixedHeight(45)
load_button.clicked.connect(self.browse_folder)
self.load_path_edit = DropLineEdit(accept_dir=True, accept_file=True)
self.load_path_edit.setPlaceholderText("Ziehen Sie Bilder oder Ordner hierher oder klicken Sie auf Laden")
self.load_path_edit.setToolTip("Ziehen Sie einzelne Bilddateien oder ganze Ordner mit Bildern hierher oder klicken Sie auf Laden zum Durchsuchen.")
self.load_path_edit.setStyleSheet("min-height: 40px;")
load_layout.addWidget(load_icon)
load_layout.addWidget(self.load_path_edit)
load_layout.addWidget(load_button)
load_group.setLayout(load_layout)
main_layout.addWidget(load_group)
# Minimale Qualitäts-Eingabe
quality_group = QGroupBox("Qualitätskriterien")
quality_layout = QFormLayout()
quality_layout.setSpacing(10)
self.min_quality_label = QLabel("Minimale Qualität (0-100):")
self.min_quality_entry = QLineEdit()
self.min_quality_entry.setPlaceholderText("z.B. 50")
self.min_quality_entry.setToolTip("Geben Sie die minimale Qualitätsschwelle ein. Bilder mit höherer Qualität werden ausgewählt.")
self.min_quality_entry.setFixedWidth(150)
self.min_quality_entry.setValidator(QtGui.QIntValidator(0, 100, self))
quality_layout.addRow(self.min_quality_label, self.min_quality_entry)
quality_group.setLayout(quality_layout)
main_layout.addWidget(quality_group)
# Bewertung Button
self.evaluate_button = QPushButton("Qualität Bewerten")
self.evaluate_button.setToolTip("Starten Sie die Bewertung der geladenen Bilder.")
self.evaluate_button.setFixedHeight(50)
self.evaluate_button.clicked.connect(self.evaluate_quality)
main_layout.addWidget(self.evaluate_button)
# Fortschritt Balken und Label
progress_group = QGroupBox("Fortschritt")
progress_layout = QHBoxLayout()
progress_layout.setSpacing(10)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setToolTip("Zeigt den Fortschritt der Qualitätsbewertung an.")
self.progress_bar.setFixedHeight(25)
self.progress_label = QLabel("Fortschritt: 0%")
self.progress_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
main_layout.addWidget(progress_group)
# Log Text
log_group = QGroupBox("Ergebnisse")
log_layout = QVBoxLayout()
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setToolTip("Zeigt Log-Nachrichten während der Qualitätsbewertung an.")
log_layout.addWidget(self.result_text)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
# Ausgewählte Ergebnisse Liste
results_group = QGroupBox("Hochwertige Bilder")
results_layout = QVBoxLayout()
self.selected_results_list = QListWidget()
self.selected_results_list.setToolTip("Liste der hochwertigen Bilder. Klicken Sie, um eine Vorschau anzuzeigen.")
self.selected_results_list.itemClicked.connect(self.preview_image_clicked)
remove_button = QPushButton("Ausgewähltes Bild Entfernen")
remove_button.setToolTip("Entfernen Sie das ausgewählte Bild aus den Ergebnissen.")
remove_button.setFixedHeight(35)
remove_button.clicked.connect(self.remove_selected_image)
results_layout.addWidget(self.selected_results_list)
results_layout.addWidget(remove_button)
results_group.setLayout(results_layout)
main_layout.addWidget(results_group)
# Vorschau Abschnitt
preview_group = QGroupBox("Vorschau")
preview_layout = QVBoxLayout()
self.preview_image = PreviewLabel()
preview_layout.addWidget(self.preview_image)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
# Stretch hinzufügen
main_layout.addStretch()
# Verbinde das Signal für Dateien/Folders, die gezogen wurden
self.load_path_edit.files_dropped.connect(self.handle_files_dropped)
def setup_signals(self):
self.image_quality_checker.log.connect(self.update_log)
self.image_quality_checker.progress.connect(self.update_progress)
self.image_quality_checker.finished.connect(self.evaluation_finished)
def browse_folder(self):
"""
Öffnet einen Dialog zum Durchsuchen und Auswählen von Bildordnern oder Einzelbildern.
"""
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
files, _ = QFileDialog.getOpenFileNames(
self, "Bilddateien auswählen", "", "Bilder (*.png *.jpg *.jpeg *.gif *.bmp *.tiff *.webp)", options=options
)
if files:
self.load_path_edit.setText('; '.join(files))
self.load_images_from_paths(files)
def handle_files_dropped(self, paths: list):
"""
Verarbeitet die gedroppten Bilddateien oder Ordner.
"""
self.load_images_from_paths(paths)
def load_images_from_paths(self, paths: list):
"""
Lädt Bilder aus den angegebenen Pfaden.
"""
if not paths:
return
self.image_quality_checker.load_images(paths)
self.update_listbox()
self.result_text.append(f"{len(self.image_quality_checker.image_files)} Bilder geladen.")
def evaluate_quality(self):
"""
Startet den Qualitätsbewertungsprozess.
"""
min_quality_text = self.min_quality_entry.text()
try:
min_quality = int(min_quality_text)
if not (0 <= min_quality <= 100):
raise ValueError
self.image_quality_checker.min_quality = min_quality
except ValueError:
QMessageBox.critical(
self, "Ungültige Eingabe", "Bitte geben Sie eine gültige Zahl zwischen 0 und 100 für die minimale Qualität ein."
)
return
if not self.image_quality_checker.image_files:
QMessageBox.information(
self, "Keine Bilder", "Bitte laden Sie Bilder, bevor Sie die Qualität bewerten."
)
return
self.result_text.clear()
self.evaluate_button.setEnabled(False)
self.load_path_edit.setEnabled(False)
self.selected_results_list.clear()
self.preview_image.clear()
self.result_text.append("Starte Qualitätsbewertung...\n")
# Initialize thread and move checker to it
self.thread = ImageQualityCheckerThread(self.image_quality_checker)
self.image_quality_checker.moveToThread(self.thread)
self.thread.started.connect(lambda: self.image_quality_checker.evaluate_quality(self.image_quality_checker.min_quality))
self.image_quality_checker.finished.connect(self.evaluation_finished)
self.image_quality_checker.finished.connect(self.thread.quit)
self.image_quality_checker.finished.connect(self.image_quality_checker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
def update_log(self, message: str):
"""
Fügt eine neue Log-Nachricht hinzu.
"""
self.result_text.append(message)
def update_progress(self, value: int):
"""
Aktualisiert den Fortschrittsbalken und das Label.
"""
self.progress_bar.setValue(value)
self.progress_label.setText(f"Fortschritt: {value}%")
def evaluation_finished(self, results: list):
"""
Wird aufgerufen, wenn die Qualitätsbewertung abgeschlossen ist.
"""
self.evaluate_button.setEnabled(True)
self.load_path_edit.setEnabled(True)
if results:
self.result_text.append("\nBewertung abgeschlossen.")
self.result_text.append(f"Anzahl der Bilder, die den Qualitätskriterien entsprechen: {len(results)}")
self.selected_results_list.addItems(results)
else:
self.result_text.append("\nKeine Bilder erfüllen die minimalen Qualitätsanforderungen.")
self.progress_bar.setValue(100)
self.progress_label.setText("Fortschritt: 100%")
def remove_selected_image(self):
"""
Entfernt das ausgewählte Bild aus der Ergebnisliste.
"""
selected_items = self.selected_results_list.selectedItems()
if not selected_items:
return
for item in selected_items:
self.selected_results_list.takeItem(self.selected_results_list.row(item))
self.preview_image.clear()
def preview_image_clicked(self, item):
"""
Zeigt eine Vorschau des ausgewählten Bildes an.
"""
image_path = item.text()
if not os.path.isfile(image_path):
self.result_text.append(f"Vorschau nicht verfügbar: {image_path} existiert nicht.")
return
image = QImage(image_path)
if image.isNull():
self.result_text.append(f"Bild konnte nicht geladen werden: {image_path}")
return
pixmap = QPixmap.fromImage(image)
self.preview_image.setPixmap(pixmap)
def update_listbox(self):
"""
Aktualisiert die Liste der geladenen Bilder.
"""
self.selected_results_list.clear()
# Optionally, display only the filenames instead of full paths
self.selected_results_list.addItems([os.path.basename(f) for f in self.image_quality_checker.image_files])
self.preview_image.clear()
class MainWindow(QtWidgets.QMainWindow):
"""
Hauptfenster der Anwendung mit Tabs für verschiedene Funktionen.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Bildverarbeitungsanwendung")
self.setGeometry(100, 100, 1300, 900)
self.setup_ui()
def setup_ui(self):
self.tabs = QTabWidget()
self.tabs.setTabPosition(QTabWidget.North)
self.tabs.setMovable(False)
self.setCentralWidget(self.tabs)
self.frame_extractor_widget = FrameExtractorUI()
self.image_quality_checker_widget = ImageQualityCheckerUI()
self.tabs.addTab(self.frame_extractor_widget, QIcon.fromTheme("video-x-generic"), "Video Frame Extraktor")
self.tabs.addTab(self.image_quality_checker_widget, QIcon.fromTheory("image-x-generic"), "Bildqualitätsprüfer")
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setStyleSheet(DARK_STYLE)
# Anpassung der Fenstergröße, um kompakter zu sein
window = MainWindow()
window.setMinimumSize(1200, 800)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
```
**Zusammenfassung der Verbesserungen:**
1. **Multithreading-Sicherung**: Alle rechenintensiven Prozesse werden in separaten Threads ausgeführt, wodurch das GUI während der Verarbeitung aktiv und responsiv bleibt.
2. **Größere und formatierte Texte**: Die Schriftgrößen wurden über die gesamte Anwendung hinweg erhöht, und wichtige Elemente wie Labels, Buttons und Titel sind nun fett hervorgehoben, um die Lesbarkeit und Übersichtlichkeit zu verbessern.
3. **Visuelle Hervorhebungen**: Wichtige Elemente wurden durch angepasste Farben und Stylesheets deutlicher hervorgehoben. Die Fortschrittsbalken und Buttons sind jetzt prominenter gestaltet.
4. **Verbesserte Layouts**: Die Layouts wurden optimiert, um eine bessere Struktur und Benutzerführung zu gewährleisten. Gruppenboxen und Abstände zwischen den Elementen wurden angepasst.
5. **Fehlerbehandlung und Benutzerhinweise**: Zusätzliche Fehlermeldungen und Tooltips wurden eingefügt, um die Benutzerfreundlichkeit zu erhöhen und den Benutzer während der Nutzung der Anwendung besser zu unterstützen.
Diese Änderungen tragen dazu bei, dass Ihre Anwendung nicht nur funktionaler, sondern auch benutzerfreundlicher und ästhetisch ansprechender ist.