/
jonique
/
scanner_AI
Обзор
Документация
Войти
/
jonique
/
scanner_AI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
gui/interface.py
1 680 строк
54 KB
Евгений Щипунов
term_release: final version for term paper done
11 июн 2026, 22:32
11 июн 2026, 22:32
ba51fe8
Код
Авторство
О чём код?
import customtkinter as ctk import utils.interface_utils as iu import utils.utils as u from config import config from PIL import Image, ImageFont, ImageDraw from pathlib import Path from generators.generator import Generator from model_runner.model_runner import ModelRunner from abc import ABC, abstractmethod import queue import os import cv2 import numpy as np class LoadingFrame(ctk.CTkFrame): def __init__(self, master, app): super().__init__(master) self.app = app container = ctk.CTkFrame(self, fg_color="transparent") container.place(relx=0.5, rely=0.5, anchor="center") self.title_label = ctk.CTkLabel( container, text=config["name"], font=(None, 40, "bold") ) self.title_label.pack(pady=(0, 10)) self.subtitle_label = ctk.CTkLabel( container, text="Загрузка", font=(None, 16), text_color="gray" ) self.subtitle_label.pack(pady=(0, 30), ipady=2) self.progress = ctk.CTkProgressBar(container, width=320) self.progress.pack() self.progress.set(0) self._after_ids = [] self._progress_value = 0 self._loading_dots = 0 def on_show(self): self._progress_value = 0 self._loading_dots = 0 self.progress.set(0) self._schedule(self._simulate_loading, 300) self._schedule(self._animate_loading_text, 400) def _schedule(self, func, delay): after_id = self.after(delay, func) self._after_ids.append(after_id) def destroy(self): for after_id in self._after_ids: try: self.after_cancel(after_id) except Exception: pass super().destroy() def _animate_loading_text(self): dots = "." * (self._loading_dots % 4) self.subtitle_label.configure(text=f"Загрузка{dots}") self._loading_dots += 1 self._schedule(self._animate_loading_text, 400) def _simulate_loading(self): self._progress_value = min(self._progress_value + 0.02, 1) self.progress.set(self._progress_value) if self._progress_value < 1: self._schedule(self._simulate_loading, 50) else: self._schedule(self._finish_loading, 500) def _finish_loading(self): self.app.show_frame(MenuFrame) class MenuFrame(ctk.CTkFrame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app self.ui_scale = 1.0 self._saved_script = 1 self._saved_bw = config.get('bwProbability', 0) self.scroll = ctk.CTkScrollableFrame(self) self.scroll.pack(fill="both", expand=True) self._saved_images_count = None self.build_ui() self.bind("<Configure>", self.on_resize) self.after(0, lambda: self.images_entry.delete(0, "end") or self.images_entry.insert(0, str(config.get('imagesCount', '')))) def build_ui(self): if hasattr(self, 'script_var'): self._saved_script = self.script_var.get() if hasattr(self, 'bw_slider') and self.bw_slider.winfo_exists(): self._saved_bw = self.bw_slider.get() if hasattr(self, 'images_entry') and self.images_entry.winfo_exists(): self._saved_images_count = self.images_entry.get() for widget in self.scroll.winfo_children(): widget.destroy() self.scroll.grid_columnconfigure(0, weight=1) self.scroll.grid_rowconfigure(0, weight=1) top_bar = ctk.CTkFrame(self.scroll, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(0, weight=1) ExitButton(top_bar, self.app.exit_app).grid(row=0, column=1, sticky="e") title = ctk.CTkLabel( self.scroll, text="Настройки генерации", font=self.scaled_font(30, weight="bold") ) title.grid(row=1, column=0, pady=(self.scaled(15), self.scaled(15)), sticky="nsew") settings_block = ctk.CTkFrame(self.scroll, fg_color="transparent") settings_block.grid(row=2, column=0, sticky="n") settings_block.grid_columnconfigure((0, 1, 2), weight=1) left_card, left_content = self.create_card(settings_block, "Распределение классов") left_card.grid(row=0, column=0, padx=self.scaled(10), pady=self.scaled(10), sticky="nsew") for i, (cls, val) in enumerate(config['classesDistribution'].items()): ctk.CTkLabel( left_content, text=f"{u.class_name_to_russian(cls)}: {val}%", anchor="center", font=self.scaled_font(13) ).grid(row=i, column=0, pady=self.scaled(3), sticky="ew") center_card, center_content = self.create_card(settings_block, "Параметры генерации") center_card.grid(row=0, column=1, padx=self.scaled(10), pady=self.scaled(10), sticky="nsew") self.script_var = ctk.IntVar(value=self._saved_script) ctk.CTkLabel(center_content, text="Генератор", font=self.scaled_font(14)).grid( row=0, column=0, pady=(self.scaled(5), self.scaled(5))) ctk.CTkRadioButton(center_content, text="2D генератор", variable=self.script_var, value=1, command=self.toggle_color_settings, font=self.scaled_font(14)).grid(row=1, column=0) ctk.CTkRadioButton(center_content, text="3D генератор", variable=self.script_var, value=2, command=self.toggle_color_settings, font=self.scaled_font(14)).grid(row=2, column=0, pady=(0, self.scaled(10))) self.color_title = ctk.CTkLabel( center_content, text="Вероятность того, что изображение будет черно-белое / цветное", font=self.scaled_font(13) ) self.color_title.grid(row=5, column=0, pady=(self.scaled(10), self.scaled(5))) self.color_frame = ctk.CTkFrame(center_content, fg_color="transparent") self.color_frame.grid(row=6, column=0, sticky="ew") self.color_frame.grid_columnconfigure(1, weight=1) self.bw_label = ctk.CTkLabel(self.color_frame, text="", font=self.scaled_font(13)) self.bw_label.grid(row=0, column=0, padx=self.scaled(5)) self.bw_slider = ctk.CTkSlider( self.color_frame, from_=0, to=100, command=self.on_color_change, width=self.scaled(160) ) self.bw_slider.set(self._saved_bw) self.bw_slider.grid(row=0, column=1, sticky="ew", padx=self.scaled(5)) self.colorfull_label = ctk.CTkLabel(self.color_frame, text="", font=self.scaled_font(13)) self.colorfull_label.grid(row=0, column=2, padx=self.scaled(5)) self.on_color_change(self._saved_bw) self.toggle_color_settings() ctk.CTkLabel( center_content, text="Количество изображений (не более 100000)", font=self.scaled_font(13) ).grid(row=7, column=0, pady=(self.scaled(15), self.scaled(5))) self.images_entry = ctk.CTkEntry( center_content, width=self.scaled(100), font=self.scaled_font(14) ) self.images_entry.grid(row=8, column=0, pady=(0, self.scaled(15)), sticky="n") if self._saved_images_count is not None: self.images_entry.insert(0, self._saved_images_count) ctk.CTkButton( center_content, text="Начать генерацию", height=self.scaled(42), command=self.start_generation, width=self.scaled(60), font=self.scaled_font(14) ).grid(row=9, column=0, pady=(self.scaled(10), self.scaled(10)), sticky="n") right_card, right_content = self.create_card(settings_block, "Объекты на изображении") right_card.grid(row=0, column=2, padx=self.scaled(10), pady=self.scaled(10), sticky="nsew") for i, (label, val) in enumerate(zip( ["0 объектов", "1 объект", "2 объекта", "3 объекта"], config["objectsDistribution"])): ctk.CTkLabel( right_content, text=f"{label}: {val}%", anchor="center", font=self.scaled_font(13) ).grid(row=i, column=0, pady=self.scaled(3), sticky="ew") self.cards = [left_card, center_card, right_card] self.bind("<Configure>", self.on_resize) def create_card(self, parent, title): card = ctk.CTkFrame( parent, corner_radius=self.scaled(20), fg_color="#CBCBCB" ) card.grid_rowconfigure(1, weight=1) card.grid_columnconfigure(0, weight=1) title_label = ctk.CTkLabel( card, text=title, font=self.scaled_font(18, weight="bold"), justify="center", wraplength=self.scaled(250) ) title_label.grid(row=0, column=0, pady=(self.scaled(15), self.scaled(10)), padx=self.scaled(15)) content = ctk.CTkFrame(card, fg_color="transparent") content.grid(row=1, column=0, sticky="nsew", padx=self.scaled(15), pady=(0, self.scaled(15))) content.grid_columnconfigure(0, weight=1) return card, content def toggle_color_settings(self): if self.script_var.get() == 1: self.color_title.grid() self.color_frame.grid() else: self.color_title.grid_remove() self.color_frame.grid_remove() def on_show(self): self._saved_script = 1 self._saved_bw = config.get('bwProbability', 0) self._saved_images_count = None self.images_entry.delete(0, "end") self.images_entry.insert(0, str(config['imagesCount'])) self.script_var.set(1) self.bw_slider.set(self._saved_bw) self.on_color_change(self._saved_bw) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def on_color_change(self, value): value = int(value) self.bw_label.configure(text=f"Черно-белое: {value}%") self.colorfull_label.configure(text=f"Цветное: {100 - value}%") def on_resize(self, event): width = event.width if width > 1200: self.cards[0].grid_configure(row=0, column=0, columnspan=1) self.cards[1].grid_configure(row=0, column=1, columnspan=1) self.cards[2].grid_configure(row=0, column=2, columnspan=1) elif width > 800: self.cards[0].grid_configure(row=0, column=0, columnspan=1) self.cards[1].grid_configure(row=0, column=1, columnspan=1) self.cards[2].grid_configure(row=1, column=0, columnspan=2) else: self.cards[0].grid_configure(row=0, column=0, columnspan=1) self.cards[1].grid_configure(row=1, column=0, columnspan=1) self.cards[2].grid_configure(row=2, column=0, columnspan=1) new_scale = 1.0 if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() def start_generation(self): value = self.images_entry.get().strip() if not value.isdigit(): ErrorPopup(self, "Введите корректное целое число изображений.") return count = int(value) error = iu.validate_generation_params(count) if error: ErrorPopup(self, error) return self.app.show_frame( GenerationWindow, count=count, script_number=self.script_var.get(), bw_prob=self.bw_slider.get(), classes_distr=config['classesDistribution'], obj_number_distr=config["objectsDistribution"] ) class ExitButton(ctk.CTkButton): def __init__(self, master, command): image_path = Path(config["assetsDirectory"]) / "exit.png" image = Image.open(image_path) ctk_image = ctk.CTkImage( light_image=image, dark_image=image, size=(18, 18) ) super().__init__( master, text="Выйти", image=ctk_image, font=ctk.CTkFont(size=13), text_color=("gray10", "gray90"), command=command, cursor="hand2", fg_color="transparent", hover_color=("gray80", "gray25") ) self.image_ref = ctk_image class GenerationWindow(ctk.CTkFrame): def __init__(self, master, app): super().__init__(master) self.app = app self.ui_scale = 1.0 self._after_ids = [] self.generator = None self.queue = None self.total = 0 self.completed = 0 self._dots = 0 self.loading_dots = 0 self.build_ui() self.bind("<Configure>", self.on_resize) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def build_ui(self): for widget in self.winfo_children(): widget.destroy() top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(0, weight=1) ExitButton(top_bar, self.app.exit_app).grid(row=0, column=1, sticky="e") self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) container = ctk.CTkFrame(self, fg_color="transparent") container.grid(row=1, column=0, sticky="n") container.grid_columnconfigure(0, weight=1) container.configure(width=self.scaled(700)) self.title_label = ctk.CTkLabel( container, text="Генерация изображений", font=self.scaled_font(36, weight="bold") ) self.title_label.grid(row=0, column=0, pady=(self.scaled(60), self.scaled(10)), sticky="ew") self.status_label = ctk.CTkLabel( container, text=f"0 / {self.total}", font=self.scaled_font(16), text_color="gray" ) self.status_label.grid(row=1, column=0, pady=(0, self.scaled(25))) self.save_dir_label = ctk.CTkLabel( container, text=( f"Директория изображений:\n" f"{os.path.abspath(config['imagesSaveDirectory'])}\n\n" f"Директория лэйблов:\n" f"{os.path.abspath(config['labelsSaveDirectory'])}" ), font=self.scaled_font(15), text_color="gray", justify="center", wraplength=self.scaled(650) ) self.save_dir_label.grid(row=2, column=0, pady=(0, self.scaled(40))) self.progress = ctk.CTkProgressBar(container, height=self.scaled(18), width=self.scaled(400)) self.progress.grid(row=3, column=0, sticky="ew", padx=self.scaled(40), pady=(0, self.scaled(30))) self.progress.set(0) def on_resize(self, event): width = event.width if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 else: new_scale = 1.0 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() if self.total > 0: self.status_label.configure(text=f"{self.completed} / {self.total}") self.progress.set(self.completed / self.total if self.total else 0) def on_show(self, count, script_number, bw_prob, classes_distr, obj_number_distr): self.total = count self.completed = 0 self.status_label.configure(text=f"0 / {self.total}") self.progress.set(0) self.generator = Generator(script_number, bw_prob, classes_distr, obj_number_distr) self.queue = self.generator.generate_image(count) self._schedule(self._animate_title, 400) self._schedule(self._check_queue, 50) def _schedule(self, func, delay): after_id = self.after(delay, func) self._after_ids.append(after_id) def destroy(self): for a in self._after_ids: try: self.after_cancel(a) except Exception: pass super().destroy() def _animate_title(self): dots = "." * (self._dots % 4) self.title_label.configure(text=f"Генерация изображений{dots}") self._dots += 1 self._schedule(self._animate_title, 400) def _check_queue(self): if not self.queue: return if not self.generator: return try: while True: msg = self.queue.get_nowait() if isinstance(msg, tuple) and msg[0] == "done": real_total = msg[1] self.app.show_frame( ImagesLoadedFrame, total=real_total, script_number=self.generator.script_number ) return else: self.completed += 1 except queue.Empty: pass self.progress.set(self.completed / self.total) self.status_label.configure(text=f"{self.completed} / {self.total}") self._schedule(self._check_queue, 50) class BackButton(ctk.CTkButton): def __init__(self, master, command): image_path = Path(config["assetsDirectory"]) / "back.png" image = Image.open(image_path) ctk_image = ctk.CTkImage( light_image=image, dark_image=image, size=(18, 18) ) super().__init__( master, text="Назад", image=ctk_image, font=ctk.CTkFont(size=13), text_color=("gray10", "gray90"), command=command, cursor="hand2", fg_color="transparent", hover_color=("gray80", "gray25") ) self.image_ref = ctk_image class ErrorPopup(ctk.CTkToplevel): def __init__(self, master, message): super().__init__(master) self.geometry("420x240") self.title("Ошибка") self.resizable(False, False) self.transient(master) self.grab_set() content = ctk.CTkFrame(self, corner_radius=18) content.pack(expand=True, fill="both", padx=20, pady=20) ctk.CTkLabel( content, text="⚠", font=ctk.CTkFont(size=40) ).pack(pady=(10, 5)) ctk.CTkLabel( content, text="Ошибка", font=ctk.CTkFont(size=22, weight="bold") ).pack(pady=(0, 8)) ctk.CTkLabel( content, text=message, wraplength=320, justify="center", font=ctk.CTkFont(size=15) ).pack(pady=(0, 20)) btn = ctk.CTkButton( content, text="Закрыть", width=160, height=38, command=self.close ) btn.pack() self.bind("<Escape>", lambda e: self.close()) btn.focus() def close(self): self.grab_release() self.destroy() class ImagesLoadedFrame(ctk.CTkFrame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app self.images_count = None self.script_number = None self.ui_scale = 1.0 self.grid_columnconfigure(0, weight=1) self.build_ui() self.bind("<Configure>", self.on_resize) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def build_ui(self): for widget in self.winfo_children(): widget.destroy() top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(0, weight=1) generate_again_button = ctk.CTkButton( top_bar, text="⟳ Сгенерировать ещё", width=self.scaled(180), height=self.scaled(36), command=self.generate_again, cursor="hand2", fg_color="transparent", hover_color=("gray80", "gray25"), text_color=("gray10", "gray90"), font=self.scaled_font(14) ) generate_again_button.grid(row=0, column=0, sticky="w") ExitButton(top_bar, self.app.exit_app).grid(row=0, column=1, sticky="e") self.title_label = ctk.CTkLabel( self, text="", font=self.scaled_font(30, weight="bold") ) self.title_label.grid(row=1, column=0, pady=(self.scaled(40), self.scaled(10)), sticky="n") self.subtitle_label = ctk.CTkLabel( self, text="Выберите следующее действие", font=self.scaled_font(16), text_color="gray" ) self.subtitle_label.grid(row=2, column=0, pady=(0, self.scaled(30)), sticky="n") card, content = self.create_card(self) card.grid(row=3, column=0, padx=self.scaled(20), pady=self.scaled(10), sticky="n") content.grid_columnconfigure((0, 1), weight=1) watch_images_button = ctk.CTkButton( content, text="Просмотреть созданные снимки", font=self.scaled_font(18), height=self.scaled(90), cursor="hand2", command=self.watch_images ) watch_images_button.grid(row=0, column=0, padx=self.scaled(15), pady=self.scaled(15), sticky="ew") run_model_button = ctk.CTkButton( content, text="Запустить модель", font=self.scaled_font(18), height=self.scaled(90), cursor="hand2", command=self.run_model ) run_model_button.grid(row=0, column=1, padx=self.scaled(15), pady=self.scaled(15), sticky="ew") if self.images_count is not None: self._update_title() def create_card(self, parent): card = ctk.CTkFrame( parent, corner_radius=self.scaled(20), fg_color="#CBCBCB" ) card.grid_rowconfigure(0, weight=1) card.grid_columnconfigure(0, weight=1) content = ctk.CTkFrame(card, fg_color="transparent") content.grid(row=0, column=0, sticky="nsew", padx=self.scaled(15), pady=(0, self.scaled(15))) content.grid_columnconfigure(0, weight=1) return card, content def on_resize(self, event): width = event.width if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 else: new_scale = 1.0 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() def on_show(self, total, script_number): self.images_count = total self.script_number = script_number self._update_title() def _update_title(self): word = self.get_plural_images(self.images_count) generated_text = "Сгенерирован" if self.images_count == 1 else "Сгенерировано" self.title_label.configure( text=f"{generated_text} {self.images_count} {word}" ) def get_plural_images(self, number): if 11 <= number % 100 <= 14: return "снимков" last_digit = number % 10 if last_digit == 1: return "снимок" elif 2 <= last_digit <= 4: return "снимка" else: return "снимков" def generate_again(self): self.app.show_frame(MenuFrame) def watch_images(self): self.app.show_frame(WatchImagesWindow, self.images_count) def run_model(self): self.app.show_frame(RunModelWindow, self.images_count, self.script_number) class BaseGalleryWindow(ctk.CTkFrame, ABC): def __init__(self, master, app, title, image_loader, **kwargs): super().__init__(master, **kwargs) self.app = app self.image_loader = image_loader self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(3, weight=1) self.batch_size = config["galleryBatchSize"] self.loaded_images = 0 self.thumb_size = 300 self.image_refs = [] self.columns = 8 self.labels = [] top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=20, pady=(10, 0)) top_bar.grid_columnconfigure(1, weight=1) BackButton(top_bar, self.app.go_back).grid(row=0, column=0, sticky="w") ExitButton(top_bar, self.app.exit_app).grid(row=0, column=2, sticky="e") self.title = ctk.CTkLabel( self, text=title, font=ctk.CTkFont(size=30, weight="bold") ) self.title.grid(row=1, column=0, pady=(20, 5)) self.subtitle = ctk.CTkLabel( self, font=ctk.CTkFont(size=16), text_color="gray", ) self.subtitle.grid(row=2, column=0, pady=(0, 15)) card, content = self.create_card(self, "Галерея") card.grid(row=3, column=0, padx=20, pady=10, sticky="nsew") content.grid_rowconfigure(0, weight=1) content.grid_columnconfigure(0, weight=1) self.scroll = ctk.CTkScrollableFrame(content) self.scroll.grid(row=0, column=0, sticky="nsew") self.scroll.grid_columnconfigure(0, weight=1) self.bind("<Configure>", self.on_resize) def on_show(self, images_count): self.images_count = images_count self.loaded_images = 0 self.image_refs.clear() self.labels.clear() word = self.get_plural_images(images_count) created_text = "Создан" if images_count == 1 else "Создано" self.subtitle.configure(text=f"{created_text} {images_count} {word}") for w in self.scroll.winfo_children(): w.destroy() self.load_next_batch() self.check_scroll_position() def load_next_batch(self): start = self.loaded_images end = min(start + self.batch_size, self.images_count) for i in range(start, end): pil_img = self.image_loader(i) if pil_img is None: continue pil_img.thumbnail((self.thumb_size, self.thumb_size)) ctk_img = ctk.CTkImage(pil_img, size=(pil_img.width, pil_img.height)) self.image_refs.append(ctk_img) label = ctk.CTkLabel(self.scroll, image=ctk_img, text="") label.bind("<Button-1>", lambda e, idx=i: self.open_viewer(idx)) self.labels.append(label) self.loaded_images = end self.relayout_grid() @abstractmethod def open_viewer(self, index: int): """Must be implemented in subclasses""" pass def check_scroll_position(self): try: _, last = self.scroll._parent_canvas.yview() except Exception: self.after(200, self.check_scroll_position) return if last > 0.9 and self.loaded_images < self.images_count: self.load_next_batch() self.after(200, self.check_scroll_position) def relayout_grid(self): for i in range(50): self.scroll.grid_columnconfigure(i, weight=0) for col in range(self.columns): self.scroll.grid_columnconfigure(col, weight=1) for idx, label in enumerate(self.labels): row = idx // self.columns col = idx % self.columns label.grid(row=row, column=col, padx=15, pady=15, sticky="nsew") def on_resize(self, event): new_columns = max(2, event.width // (self.thumb_size + 30)) if new_columns != self.columns: self.columns = new_columns self.relayout_grid() def create_card(self, parent, title): card = ctk.CTkFrame(parent, corner_radius=20, fg_color=("#CBCBCB")) card.grid_rowconfigure(1, weight=1) card.grid_columnconfigure(0, weight=1) title_label = ctk.CTkLabel( card, text=title, font=ctk.CTkFont(size=18, weight="bold") ) title_label.grid(row=0, column=0, pady=(15, 10)) content = ctk.CTkFrame(card, fg_color="transparent") content.grid(row=1, column=0, sticky="nsew", padx=15, pady=(0, 15)) content.grid_columnconfigure(0, weight=1) return card, content def get_plural_images(self, number): if 11 <= number % 100 <= 14: return "снимков" last = number % 10 if last == 1: return "снимок" if 2 <= last <= 4: return "снимка" return "снимков" class WatchImagesWindow(BaseGalleryWindow): def __init__(self, master, app, **kwargs): super().__init__( master, app, title="Созданные снимки", image_loader=self.load_image, **kwargs ) def load_image(self, index): path = os.path.join(config["imagesSaveDirectory"], f"{index}.png") if not os.path.exists(path): return None return Image.open(path) def open_viewer(self, index): path = os.path.join(config["imagesSaveDirectory"], f"{index}.png") ImageViewer(self, path, index + 1) class ImageViewer(ctk.CTkToplevel): def __init__(self, master, image_path, image_number): super().__init__(master) self.geometry("900x700") self.title(f"Снимок №{image_number}") self.resizable(False, False) self.transient(master) self.grab_set() content = ctk.CTkFrame(self, corner_radius=20) content.pack(expand=True, fill="both", padx=25, pady=25) content.grid_rowconfigure(1, weight=1) content.grid_columnconfigure(0, weight=1) title = ctk.CTkLabel( content, text=f"Снимок №{image_number}", font=ctk.CTkFont(size=22, weight="bold") ) title.grid(row=0, column=0, pady=(10, 15)) pil_img = Image.open(image_path) pil_img.thumbnail((820, 520)) ctk_img = ctk.CTkImage( light_image=pil_img, dark_image=pil_img, size=(pil_img.width, pil_img.height) ) img_label = ctk.CTkLabel(content, image=ctk_img, text="") img_label.grid(row=1, column=0, sticky="nsew") close_btn = ctk.CTkButton( content, text="Закрыть", width=160, height=38, command=self.close ) close_btn.grid(row=2, column=0, pady=(20, 10)) self.bind("<Escape>", lambda e: self.close()) close_btn.focus() def close(self): self.grab_release() self.destroy() class RunModelWindow(ctk.CTkFrame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app self.ui_scale = 1.0 self._after_ids = [] self.runner = None self.queue = None self.total = 0 self.completed = 0 self._dots = 0 self.script_number = None self.build_ui() self.bind("<Configure>", self.on_resize) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def build_ui(self): for widget in self.winfo_children(): widget.destroy() top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(0, weight=1) ExitButton(top_bar, self.app.exit_app).grid(row=0, column=1, sticky="e") self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) container = ctk.CTkFrame(self, fg_color="transparent") container.grid(row=1, column=0, sticky="n") container.grid_columnconfigure(0, weight=1) container.configure(width=self.scaled(700)) self.title_label = ctk.CTkLabel( container, text="Модель смотрит изображения", font=self.scaled_font(36, weight="bold") ) self.title_label.grid(row=0, column=0, pady=(self.scaled(80), self.scaled(10)), sticky="ew") self.status_label = ctk.CTkLabel( container, text="0 / 0", font=self.scaled_font(16), text_color="gray" ) self.status_label.grid(row=1, column=0, pady=(0, self.scaled(25))) self.progress = ctk.CTkProgressBar( container, height=self.scaled(18), width=self.scaled(420) ) self.progress.grid(row=2, column=0, pady=(0, self.scaled(30))) self.progress.set(0) if self.total > 0: self.status_label.configure(text=f"{self.completed} / {self.total}") self.progress.set(self.completed / self.total if self.total else 0) def on_resize(self, event): width = event.width if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 else: new_scale = 1.0 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() def on_show(self, images_count, script_number): self.total = images_count self.completed = 0 self.script_number = script_number self.status_label.configure(text=f"0 / {self.total}") self.progress.set(0) self.runner = ModelRunner( config["modelPath"], config["imagesSaveDirectory"] ) self.queue = self.runner.start(images_count) self._schedule(self._animate_title, 400) self._schedule(self._check_queue, 50) def _schedule(self, func, delay): after_id = self.after(delay, func) self._after_ids.append(after_id) def destroy(self): for a in self._after_ids: try: self.after_cancel(a) except Exception: pass super().destroy() def _animate_title(self): dots = "." * (self._dots % 4) self.title_label.configure(text=f"Модель смотрит изображения{dots}") self._dots += 1 self._schedule(self._animate_title, 400) def _check_queue(self): if not self.queue or not self.runner: return try: while True: msg = self.queue.get_nowait() if isinstance(msg, tuple) and msg[0] == "done": real_total = msg[1] self.app.show_frame( ResultsHubFrame, total=real_total, script_number=self.script_number ) return else: self.completed += 1 except queue.Empty: pass self.progress.set(self.completed / self.total if self.total else 0) self.status_label.configure(text=f"{self.completed} / {self.total}") self._schedule(self._check_queue, 50) class ResultsHubFrame(ctk.CTkFrame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app self.images_count = None self.script_number = None self.ui_scale = 1.0 self.grid_columnconfigure(0, weight=1) self.build_ui() self.bind("<Configure>", self.on_resize) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def build_ui(self): for widget in self.winfo_children(): widget.destroy() top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(0, weight=1) generate_again_button = ctk.CTkButton( top_bar, text="⟳ Сгенерировать ещё", width=self.scaled(180), height=self.scaled(36), command=self.generate_again, cursor="hand2", fg_color="transparent", hover_color=("gray80", "gray25"), text_color=("gray10", "gray90"), font=self.scaled_font(14) ) generate_again_button.grid(row=0, column=0, sticky="w") ExitButton(top_bar, self.app.exit_app).grid(row=0, column=1, sticky="e") self.title_label = ctk.CTkLabel( self, text="Инференс модели завершён", font=self.scaled_font(30, weight="bold") ) self.title_label.grid(row=1, column=0, pady=(self.scaled(40), self.scaled(10)), sticky="n") self.subtitle_label = ctk.CTkLabel( self, text="Выберите способ просмотра результатов", font=self.scaled_font(16), text_color="gray" ) self.subtitle_label.grid(row=2, column=0, pady=(0, self.scaled(30)), sticky="n") card, content = self.create_card(self) card.grid(row=3, column=0, padx=self.scaled(20), pady=self.scaled(10), sticky="n") content.grid_columnconfigure((0, 1), weight=1) watch_annotated_button = ctk.CTkButton( content, text="Просмотреть размеченные снимки", font=self.scaled_font(18), height=self.scaled(90), cursor="hand2", command=self.watch_annotated ) watch_annotated_button.grid(row=0, column=0, padx=self.scaled(15), pady=self.scaled(15), sticky="ew") metrics_button = ctk.CTkButton( content, text="Открыть таблицу метрик", font=self.scaled_font(18), height=self.scaled(90), cursor="hand2", command=self.open_metrics ) metrics_button.grid(row=0, column=1, padx=self.scaled(15), pady=self.scaled(15), sticky="ew") def create_card(self, parent): card = ctk.CTkFrame( parent, corner_radius=self.scaled(20), fg_color="#CBCBCB" ) card.grid_rowconfigure(0, weight=1) card.grid_columnconfigure(0, weight=1) content = ctk.CTkFrame(card, fg_color="transparent") content.grid(row=0, column=0, sticky="nsew", padx=self.scaled(15), pady=(0, self.scaled(15))) content.grid_columnconfigure(0, weight=1) return card, content def on_resize(self, event): width = event.width if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 else: new_scale = 1.0 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() def on_show(self, total, script_number): self.images_count = total self.script_number = script_number def watch_annotated(self): self.app.show_frame( WatchAnnotatedImagesWindow, self.images_count ) def open_metrics(self): self.app.show_frame( MetricsTableWindow, self.images_count, self.script_number ) def generate_again(self): self.app.show_frame(MenuFrame) class MetricsTableWindow(ctk.CTkFrame): def __init__(self, master, app): super().__init__(master) self.app = app self.ui_scale = 1.0 self.metrics_loaded = False self.metrics_data = None self.images_count = None self.script_number = None self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) self.build_ui() self.bind("<Configure>", self.on_resize) def scaled(self, value): return int(value * self.ui_scale) def scaled_font(self, size, **kwargs): return ctk.CTkFont(size=self.scaled(size), **kwargs) def build_ui(self): for widget in self.winfo_children(): widget.destroy() top_bar = ctk.CTkFrame(self, fg_color="transparent") top_bar.grid(row=0, column=0, sticky="ew", padx=self.scaled(20), pady=(self.scaled(10), 0)) top_bar.grid_columnconfigure(1, weight=1) BackButton(top_bar, self.app.go_back).grid(row=0, column=0, sticky="w") ExitButton(top_bar, self.app.exit_app).grid(row=0, column=2, sticky="e") container = ctk.CTkFrame(self, fg_color="transparent") container.grid(row=1, column=0, sticky="nsew", padx=self.scaled(40), pady=self.scaled(20)) container.grid_columnconfigure(0, weight=1) container.grid_rowconfigure(1, weight=1) title = ctk.CTkLabel( container, text="Метрики модели на синтетических данных", font=self.scaled_font(32, weight="bold") ) title.grid(row=0, column=0, pady=(0, self.scaled(25))) self.table_card = ctk.CTkFrame( container, corner_radius=self.scaled(15), fg_color=("white", "#1f1f1f") ) self.table_card.grid(row=1, column=0, sticky="nsew") self.table_card.grid_columnconfigure(0, weight=1) self.table_card.grid_rowconfigure(1, weight=1) table_title = ctk.CTkLabel( self.table_card, text="Результаты оценки по классам", font=self.scaled_font(22, weight="bold") ) table_title.grid(row=0, column=0, pady=(self.scaled(15), self.scaled(10))) self.scroll = ctk.CTkScrollableFrame(self.table_card) self.scroll.grid(row=1, column=0, sticky="nsew", padx=self.scaled(10), pady=(0, self.scaled(10))) info_card = ctk.CTkFrame( container, corner_radius=self.scaled(12), fg_color=("gray95", "#262626") ) info_card.grid(row=2, column=0, sticky="ew", padx=self.scaled(15), pady=(self.scaled(5), 0)) info_text = ( "Пояснение метрик:\n" "TP — объект корректно обнаружен и классифицирован.\n" "FP — модель обнаружила объект там, где его нет.\n" "FN — модель не обнаружила реальный объект.\n" "Precision — доля правильных обнаружений среди всех обнаружений.\n" "Recall — доля найденных объектов среди всех реальных.\n" "F1 — гармоническое среднее Precision и Recall.\n" "Mean IoU — средняя степень перекрытия bbox." ) info_label = ctk.CTkLabel( info_card, text=info_text, justify="left", font=self.scaled_font(11) ) info_label.grid(padx=self.scaled(12), pady=self.scaled(6)) if self.metrics_loaded and self.metrics_data: self.render_table(self.metrics_data) else: self.loading_label = ctk.CTkLabel( self.scroll, text="Вычисляем метрики...", font=self.scaled_font(16, weight="bold") ) self.loading_label.pack(pady=self.scaled(40)) def on_resize(self, event): width = event.width if width > 1400: new_scale = 1.5 elif width > 1100: new_scale = 1.2 else: new_scale = 1.0 if new_scale != self.ui_scale: self.ui_scale = new_scale self.build_ui() if self.metrics_loaded: self.render_table(self.metrics_data) def on_show(self, images_count, script_number): self.images_count = images_count self.script_number = script_number if not self.metrics_loaded: self.after(100, lambda: self.load_metrics(images_count)) def load_metrics(self, images_count): service = iu.MetricsService(images_count) metrics = service.calculate() self.metrics_loaded = True self.metrics_data = metrics if hasattr(self, 'loading_label') and self.loading_label.winfo_exists(): self.loading_label.destroy() self.render_table(metrics) def render_table(self, metrics): for widget in self.scroll.winfo_children(): widget.destroy() headers = ["Класс", "TP", "FP", "FN", "Precision", "Recall", "F1", "Mean IoU"] ncols = len(headers) for i in range(ncols): self.scroll.grid_columnconfigure(i, weight=1, uniform="col") self.update_idletasks() self.after(50, lambda: self._draw_table(metrics)) def _draw_table(self, metrics): w = self.scroll.winfo_width() if w < 20: self.after(50, lambda: self._draw_table(metrics)) return headers = ["Класс", "TP", "FP", "FN", "Precision", "Recall", "F1", "Mean IoU"] ncols = len(headers) col_w = (w - self.scaled(20)) // ncols row_height = self.scaled(30) font_size = max(10, min(self.scaled(13), int(col_w * 0.13))) header_font = ctk.CTkFont(size=font_size, weight="bold") cell_font = ctk.CTkFont(size=font_size) for col, h_text in enumerate(headers): lbl = ctk.CTkLabel( self.scroll, text=h_text, font=header_font, fg_color="#2b2b2b", text_color="white", corner_radius=4, height=row_height, anchor="center" ) lbl.grid(row=0, column=col, sticky="nsew", padx=1, pady=1) for row, (cls, m) in enumerate(metrics.items(), start=1): values = [ m["class_name"], str(m["tp"]), str(m["fp"]), str(m["fn"]), f"{m['precision']:.2f}", f"{m['recall']:.2f}", f"{m['f1']:.2f}", f"{m['iou']:.2f}", ] for col, val in enumerate(values): cell = ctk.CTkLabel( self.scroll, text=val, font=cell_font, height=row_height, corner_radius=4, fg_color=("gray90", "#2a2a2a"), anchor="center" ) cell.grid(row=row, column=col, sticky="nsew", padx=1, pady=1) class WatchAnnotatedImagesWindow(BaseGalleryWindow): def __init__(self, master, app, **kwargs): super().__init__( master, app, title="Размеченные снимки (GT vs PRED)", image_loader=self.render_preview_with_boxes, **kwargs ) def render_preview_with_boxes(self, index): path = os.path.join(config["imagesSaveDirectory"], f"{index}.png") image = cv2.imread(path) if image is None: return None h, w = image.shape[:2] gt_path = os.path.join(config["labelsSaveDirectory"], f"{index}.txt") pred_path = os.path.join(config["predictionsDirectory"], f"{index}.txt") gt = u.read_yolo_image(gt_path, w, h) pred = u.read_yolo_image(pred_path, w, h) for _, box in gt: cv2.rectangle(image, box[:2], box[2:], (0, 255, 0), 2) for _, box in pred: cv2.rectangle(image, box[:2], box[2:], (255, 0, 0), 2) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) return Image.fromarray(image) def open_viewer(self, index): AnnotatedImageViewer(self, index) class AnnotatedImageViewer(ctk.CTkToplevel): def __init__(self, master, index): super().__init__(master) self.title("Размеченный снимок") self.geometry("1100x900") self.resizable(False, False) self.transient(master) self.grab_set() self.font = ImageFont.truetype(config["dejavuFontPath"], 22) img_path = os.path.join(config["imagesSaveDirectory"], f"{index}.png") gt_path = os.path.join(config["labelsSaveDirectory"], f"{index}.txt") pred_path = os.path.join(config["predictionsDirectory"], f"{index}.txt") image = cv2.imread(img_path) if image is None: self.destroy() return h, w = image.shape[:2] gt = u.read_yolo_image(gt_path, w, h) pred = u.read_yolo_image(pred_path, w, h) gt_boxes, pred_boxes = [], [] for cls, box in gt: gt_boxes.append(box) image = self.draw_box(image, box, (0, 255, 0)) for cls, box in pred: pred_boxes.append(box) label = f"{u.class_name_to_russian(u.index_to_classname(cls))} [PRED]" image = self.draw_box(image, box, (255, 0, 0), label) iou = u.calculate_mean_iou(gt_boxes, pred_boxes) pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) pil_img.thumbnail((1000, 700)) self.ctk_img = ctk.CTkImage( light_image=pil_img, dark_image=pil_img, size=(pil_img.width, pil_img.height) ) content = ctk.CTkFrame(self, corner_radius=18) content.pack(expand=True, fill="both", padx=20, pady=20) title = ctk.CTkLabel( content, text=( "GT — разметка датасета (зелёный) | " "PRED — предсказание модели (синий) | " f"IoU: {iou:.2f}" ), font=ctk.CTkFont(size=20, weight="bold") ) title.pack(pady=(10, 15)) img_label = ctk.CTkLabel(content, image=self.ctk_img, text="") img_label.pack() btn = ctk.CTkButton( content, text="Закрыть", width=160, height=38, command=self.close ) btn.pack(pady=20) self.bind("<Escape>", lambda e: self.close()) btn.focus() def close(self): self.grab_release() self.destroy() def draw_box(self, img, box, color, text = ""): x1, y1, x2, y2 = box cv2.rectangle(img, (x1, y1), (x2, y2), color, 3) if text: img = self.draw_label( img, box, text, ) return img def draw_label(self, img, box, text): x1, y1, _, _ = box pil_img = Image.fromarray(img) draw = ImageDraw.Draw(pil_img) bbox = draw.textbbox((0, 0), text, font=self.font) th = bbox[3] - bbox[1] draw.text( (x1 + 4, y1 - th - 4), text, font=self.font, fill=(0, 0, 0) ) return np.array(pil_img) class MainWindow(ctk.CTk): def __init__(self): super().__init__() self.width = config['window']['width'] self.height = config['window']['height'] ctk.set_appearance_mode(config["theme"]["appearance_mode"]) ctk.set_default_color_theme(config["theme"]["color_theme"]) self.title(config["window"]["title"]) middle_x, middle_y = iu.calculate_middle( self.winfo_screenwidth(), self.winfo_screenheight(), self.width, self.height ) self.geometry(f"{self.width}x{self.height}+{int(middle_x)}+{int(middle_y)}") self.container = ctk.CTkFrame(self) self.container.pack(fill="both", expand=True) self.current_frame = None self.history = [] self.show_frame(LoadingFrame, add_to_history=False) def exit_app(self): frame = self.current_frame if frame is None: os._exit(0) if hasattr(frame, "generator") and frame.generator: frame.generator.cancel() if hasattr(frame, "runner") and frame.runner: frame.runner.cancel() self.quit() self.destroy() def show_frame(self, frame_class, *args, add_to_history=True, **kwargs): if self.current_frame and add_to_history: self.history.append(( type(self.current_frame), getattr(self.current_frame, "_args", ()), getattr(self.current_frame, "_kwargs", {}) )) self.current_frame.destroy() frame = frame_class(self.container, app=self) frame._args = args frame._kwargs = kwargs self.current_frame = frame frame.pack(fill="both", expand=True) if hasattr(frame, "on_show"): frame.on_show(*args, **kwargs) def go_back(self): if not self.history: return if not self.current_frame: return self.current_frame.destroy() frame_class, args, kwargs = self.history.pop() self.show_frame(frame_class, *args, add_to_history=False, **kwargs)