USER
behebe alle fehler und probleme in dem skript und verbessere sie. nennemir dann das gesamte skript der anwendung
Log:
(.venv) D:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1>d:/_Projekte/_MyCodes/FrameExtractor_with_ImageQualityCheck_o1/.venv/Scripts/python.exe d:/_Projekte/_MyCodes/FrameExtractor_with_ImageQualityCheck_o1/FrameExtractor_with_ImageQualityCheck_o1.py
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:180: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class DropLineEdit(QLineEdit):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:240: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class PreviewLabel(QLabel):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:290: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class ImageLoaderThread(QtCore.QThread):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:317: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class FrameExtractor(QtCore.QObject):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:471: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class FrameExtractorThread(QThread):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:482: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class FrameExtractorUI(QtWidgets.QWidget):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:876: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class ImageQualityChecker(QtCore.QObject):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:970: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class ImageQualityCheckerThread(QThread):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:981: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class ImageQualityCheckerUI(QtWidgets.QWidget):
d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py:1246: DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead
class MainWindow(QtWidgets.QMainWindow):
Traceback (most recent call last):
File "d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py", line 1280, in <module>
main()
File "d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py", line 1274, in main
window = MainWindow()
File "d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py", line 1254, in __init__
self.setup_ui()
File "d:\_Projekte\_MyCodes\FrameExtractor_with_ImageQualityCheck_o1\FrameExtractor_with_ImageQualityCheck_o1.py", line 1266, in setup_ui
self.tabs.addTab(self.image_quality_checker_widget, QIcon.fromTheory("image-x-generic"), "Bildqualitätsprüfer")
AttributeError: type object 'QIcon' has no attribute 'fromTheory'
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()ASSISTANT
Es scheint, dass Ihr Skript zwei Hauptprobleme aufweist:
1. **Typo in der Verwendung von `QIcon.fromTheme`**: Der Fehler `AttributeError: type object 'QIcon' has no attribute 'fromTheory'` entsteht durch einen Tippfehler. `QIcon` verfügt über die Methode `fromTheme`, nicht `fromTheory`.
2. **DeprecationWarnings bezüglich `sipPyTypeDict`**: Diese Warnungen stammen aus der internen Verwendung von `sip` in PyQt5. Obwohl Ihr Code diese direkt nicht verwendet, können solche Warnungen durch die Aktualisierung von PyQt5 oder durch das Unterdrücken der Warnungen in Ihrem Code behoben werden.
Im Folgenden finden Sie die korrigierte Version Ihres Skripts mit den erforderlichen Anpassungen:
1. **Korrektur des Tippfehlers**: Ersetzen Sie `QIcon.fromTheory` durch `QIcon.fromTheme`.
2. **Unterdrückung der Deprecation-Warnungen**: Fügen Sie am Anfang des Skripts Code hinzu, um die spezifischen Deprecation-Warnungen zu unterdrücken.
3. **Zusätzliche Verbesserungen**:
- **Bereinigung des `selected_results_list`**: Stellen Sie sicher, dass in der Bildqualitätsprüfung die vollständigen Pfade gespeichert werden, damit die Vorschau korrekt funktioniert.
- **Optimierung der Fortschrittsaktualisierung**: Vermeiden Sie unnötige Aktualisierungen des Fortschrittsbalkens, um die Leistung zu verbessern.
Hier ist das vollständig korrigierte und verbesserte Skript:
```python
import sys
import os
import cv2
import numpy as np
import warnings
# Unterdrücken von spezifischen Deprecation-Warnungen
warnings.filterwarnings("ignore", category=DeprecationWarning, module="PyQt5")
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)
# Aktualisiere den Fortschritt nur bei signifikanten Änderungen
if progress_percent != self.last_progress:
self.progress.emit(progress_percent)
self.last_progress = 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)
self.last_progress = 0 # Initialer Fortschritt
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
if overall_progress != self.last_progress:
self.progress.emit(overall_progress)
self.last_progress = 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)
last_progress = 0
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
# Aktualisiere den Fortschritt nur bei signifikanten Änderungen
if progress_percent != last_progress:
self.progress.emit(progress_percent)
last_progress = 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()
self.selected_results_list.addItems(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")
# Korrigierter Methodenaufruf von 'fromTheory' zu 'fromTheme'
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(1200, 800)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
```
### Zusammenfassung der Änderungen:
1. **Korrektur des Tippfehlers:**
- **Vorher:**
```python
self.tabs.addTab(self.image_quality_checker_widget, QIcon.fromTheory("image-x-generic"), "Bildqualitätsprüfer")
```
- **Nachher:**
```python
self.tabs.addTab(self.image_quality_checker_widget, QIcon.fromTheme("image-x-generic"), "Bildqualitätsprüfer")
```
2. **Unterdrückung der Deprecation-Warnungen:**
- Am Anfang des Skripts wurde hinzugefügt:
```python
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning, module="PyQt5")
```
Dies unterdrückt spezifisch die Deprecation-Warnungen, die von PyQt5 intern ausgelöst werden.
3. **Optimierung der Fortschrittsaktualisierung:**
- In der `FrameExtractor`-Klasse wurde eine `self.last_progress`-Variable eingeführt, um unnötige Aktualisierungen des Fortschrittsbalkens zu vermeiden.
4. **Konsistente Pfadangabe in den Ergebnislisten:**
- In der `ImageQualityCheckerUI.update_listbox`-Methode wurde sichergestellt, dass die vollständigen Pfade der Bilder in der `selected_results_list` angezeigt werden, um die Vorschau korrekt zu ermöglichen.
### Weitere Empfehlungen:
- **Aktualisieren Sie PyQt5:** Stellen Sie sicher, dass Sie die neueste Version von PyQt5 verwenden, um mögliche Inkompatibilitäten und veraltete Methoden zu vermeiden.
- **Überprüfen Sie die Icons:** Wenn `QIcon.fromTheme` keine passenden Icons findet, können Sie alternative Icons bereitstellen oder sicherstellen, dass die entsprechenden Icon-Themen auf dem System verfügbar sind.
- **Fehlerbehandlung erweitern:** Obwohl einige Fehler bereits behandelt werden, könnten weitere Prüfungen und spezifischere Fehlermeldungen die Benutzererfahrung verbessern.
Mit diesen Anpassungen sollte Ihr Skript sowohl die genannten Fehler beheben als auch effizienter und stabiler laufen.