Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions modules/ui/BaseConceptWindowView.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def __init__(self, components):
self.bucket_ax = None
self.text_color = None
self.canvas = None
#{aspect_ratio_string: [relative file paths]} for the currently displayed smallest buckets
self._smallest_bucket_files = {}

def build_general_tab(self, frame, controller, ui_state, text_ui_state):
# name
Expand Down Expand Up @@ -305,8 +307,10 @@ def build_concept_stats_tab(self, frame, controller):
tooltip="Graph of all possible buckets and the number of images in each one, defined as height/width. Buckets range from 0.25 (4:1 extremely wide) to 4 (1:4 extremely tall). \
Images which don't match a bucket exactly are cropped to the nearest one.", underline=True)
self.small_bucket_label = self.components.label(frame, 17, 1, "\nSmallest Buckets", pad=0,
tooltip="Image buckets with the least nonzero total images - if 'batch size' is larger than this, these images will be ignored during training! See the wiki for more details.", underline=True)
tooltip="Image buckets with the least nonzero total images - if 'batch size' is larger than this, these images will be ignored during training! See the wiki for more details.\n\nClick the list below to see the files in these buckets.", underline=True)
self.small_bucket_preview = self.components.label(frame, 18, 1, pad=0, text="-")
#clicking the preview opens a popup listing the files in the smallest buckets; populated by _update_concept_stats
self.components.bind_clickable(self.small_bucket_preview, self._show_smallest_bucket_files)

#refresh stats - must be after all labels are defined or will give error
self.refresh_basic_stats_button = self.components.button(master=frame, row=0, column=0, text="Refresh Basic", command=lambda: controller.get_concept_stats_threaded(self, False, 9999),
Expand Down Expand Up @@ -403,6 +407,8 @@ def _update_concept_stats(self, controller):

#aspect bucketing
aspect_buckets = controller.concept.concept_stats["aspect_buckets"]
aspect_bucket_files = controller.concept.concept_stats["aspect_bucket_files"]
self._smallest_bucket_files = {}
if len(aspect_buckets) != 0 and max(val for val in aspect_buckets.values()) > 0: #check aspect_bucket data exists and is not all zero
min_val = min(val for val in aspect_buckets.values() if val > 0) #smallest nonzero values
if max(val for val in aspect_buckets.values()) > min_val: #check if any buckets larger than min_val exist - if all images are same aspect then there won't be
Expand All @@ -412,7 +418,10 @@ def _update_concept_stats(self, controller):
min_aspect_buckets = {key: val for key,val in aspect_buckets.items() if val in (min_val, min_val2)}
min_bucket_str = ""
for key, val in min_aspect_buckets.items():
min_bucket_str += f'aspect {self.decimal_to_aspect_ratio(key)} : {val} img\n'
aspect_str = self.decimal_to_aspect_ratio(key)
min_bucket_str += f'aspect {aspect_str} : {val} img\n'
#remember the files behind each displayed bucket so the click handler can list them
self._smallest_bucket_files[aspect_str] = aspect_bucket_files.get(key, [])
min_bucket_str.strip()
self.components.set_label_text(self.small_bucket_preview, min_bucket_str)

Expand All @@ -434,6 +443,17 @@ def decimal_to_aspect_ratio(self, value : float):
aspect_string = f'{aspect_fraction.denominator}:{aspect_fraction.numerator}'
return aspect_string

def _show_smallest_bucket_files(self):
#popup listing the files in each of the smallest buckets, grouped by aspect ratio
if not any(self._smallest_bucket_files.values()):
return #no advanced scan data yet
sections = []
for aspect_str, files in self._smallest_bucket_files.items():
header = f'aspect {aspect_str} ({len(files)} img):'
sections.append(header + "\n" + "\n".join(files))
text = "\n\n".join(sections)
self.components.show_text_popup(self, "Smallest bucket files", text)

def _disable_scan_buttons(self):
self.components.set_widget_enabled(self.refresh_basic_stats_button, False)
self.components.set_widget_enabled(self.refresh_advanced_stats_button, False)
Expand Down
7 changes: 7 additions & 0 deletions modules/util/concept_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def init_concept_stats(advanced_checks : bool):
"min_caption_length" : "-",
"avg_caption_length" : "-",
"aspect_buckets" : {},
"aspect_bucket_files" : {}, #relative file paths per bucket, populated only in advanced scan
"force_cancelled" : False
}

Expand Down Expand Up @@ -80,6 +81,7 @@ def init_concept_stats(advanced_checks : bool):
#initialize counts for all buckets to 0
for aspect in aspect_ratio_list:
stats_dict["aspect_buckets"][aspect] = 0
stats_dict["aspect_bucket_files"][aspect] = []

return stats_dict

Expand Down Expand Up @@ -140,6 +142,7 @@ def folder_scan(dir, stats_dict : dict, advanced_checks : bool, conceptconfig :
true_aspect = height/width
nearest_aspect = min(aspect_ratio_list, key=lambda x:abs(x-true_aspect)) #try to match math used in aspect bucketing
stats_dict["aspect_buckets"][nearest_aspect] += 1
stats_dict["aspect_bucket_files"][nearest_aspect].append(os.path.relpath(path, conceptconfig.path))

if pixels > stats_dict["max_pixels"][0]:
stats_dict["max_pixels"] = [pixels, os.path.relpath(path, conceptconfig.path), f'{width}w x {height}h']
Expand Down Expand Up @@ -183,6 +186,7 @@ def folder_scan(dir, stats_dict : dict, advanced_checks : bool, conceptconfig :
true_aspect = height/width
nearest_aspect = min(aspect_ratio_list, key=lambda x:abs(x-true_aspect))
stats_dict["aspect_buckets"][nearest_aspect] += 1
stats_dict["aspect_bucket_files"][nearest_aspect].append(os.path.relpath(path, conceptconfig.path))

if pixels > stats_dict["max_pixels"][0]:
stats_dict["max_pixels"] = [pixels, os.path.relpath(path, conceptconfig.path), f'{width}w x {height}h']
Expand Down Expand Up @@ -238,6 +242,9 @@ def combine_stats_dicts(input_dicts : list[dict], advanced_checks : bool):
elif advanced_checks and key in ["aspect_buckets"]:
for subkey in dict[key]:
final_dict[key][subkey] += dict[key][subkey]
elif advanced_checks and key in ["aspect_bucket_files"]:
for subkey in dict[key]:
final_dict[key][subkey].extend(dict[key][subkey])
elif advanced_checks and key in ["max_pixels", "max_length", "max_fps", "max_caption_length"]:
if dict[key][0] > final_dict[key][0]:
final_dict[key] = dict[key]
Expand Down
27 changes: 27 additions & 0 deletions modules/util/ui/ctk_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,30 @@ def set_label_text(label, text: str) -> None:

def call_after(widget, delay_ms: int, func) -> None:
widget.after(delay_ms, func)


def bind_clickable(widget, command: Callable[[], None]) -> None:
widget.configure(cursor="hand2")
widget.bind("<Button-1>", lambda _event: command())


def show_text_popup(parent, title: str, text: str) -> None:
from modules.util.ui.ui_utils import set_window_icon

window = ctk.CTkToplevel(parent)
window.title(title)
window.geometry("500x500")
window.grid_rowconfigure(0, weight=1)
window.grid_columnconfigure(0, weight=1)

textbox = ctk.CTkTextbox(window, wrap="none")
textbox.grid(row=0, column=0, sticky="nsew", padx=PAD, pady=PAD)
textbox.insert("1.0", text)
textbox.configure(state="disabled")

ctk.CTkButton(window, text="ok", command=window.destroy).grid(row=1, column=0, padx=PAD, pady=PAD)

window.wait_visibility()
window.grab_set()
window.focus_set()
window.after(200, lambda: set_window_icon(window))
27 changes: 27 additions & 0 deletions modules/util/ui/pyside6_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QFileDialog,
QFrame,
QGridLayout,
Expand All @@ -25,6 +26,7 @@
QPushButton,
QScrollArea,
QSizePolicy,
QTextEdit,
QToolButton,
QVBoxLayout,
QWidget,
Expand Down Expand Up @@ -810,3 +812,28 @@ def set_label_text(label: QLabel, text: str) -> None:

def call_after(widget: QWidget, delay_ms: int, func) -> None:
QTimer.singleShot(delay_ms, widget, func)


def bind_clickable(widget: QWidget, command: Callable[[], None]) -> None:
widget.setCursor(Qt.PointingHandCursor)
# QLabel has no clicked signal; route the raw press to the callback
widget.mousePressEvent = lambda _event: command()


def show_text_popup(parent: QWidget, title: str, text: str) -> None:
dialog = QDialog(parent)
dialog.setWindowTitle(title)
dialog.resize(500, 500)

lo = QVBoxLayout(dialog)
box = QTextEdit(dialog)
box.setReadOnly(True)
box.setLineWrapMode(QTextEdit.NoWrap)
box.setPlainText(text)
lo.addWidget(box)

ok = QPushButton("ok", dialog)
ok.clicked.connect(dialog.accept)
lo.addWidget(ok)

dialog.exec()