diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7a99c70 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "imdataset_creator.gui", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/imdataset_creator/__init__.py b/imdataset_creator/__init__.py index 4f36383..f981f4e 100644 --- a/imdataset_creator/__init__.py +++ b/imdataset_creator/__init__.py @@ -2,5 +2,15 @@ from .alphanumeric_sort import alphanumeric_sort from .config_handler import ConfigHandler from .configs import FilterData, MainConfig -from .datarules import DatasetBuilder, ExprDict, File, Filter, Input, Output, Producer, Rule, chunk_split +from .datarules import ( + DatasetBuilder, + ExprDict, + File, + Filter, + Input, + Output, + Producer, + Rule, + chunk_split, +) from .scenarios import FileScenario, OutputScenario diff --git a/imdataset_creator/__main__.py b/imdataset_creator/__main__.py index d290609..715bffa 100644 --- a/imdataset_creator/__main__.py +++ b/imdataset_creator/__main__.py @@ -25,19 +25,31 @@ ) CPU_COUNT = int(cpu_count()) -logging.basicConfig(level=logging.INFO, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()]) +logging.basicConfig( + level=logging.INFO, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()] +) app = typer.Typer(pretty_exceptions_show_locals=True, pretty_exceptions_short=True) log = logging.getLogger() @app.command() def main( - config_path: Annotated[Path, Option(help="Where the dataset config is placed")] = Path("config.json"), - database_path: Annotated[Path, Option(help="Where the database is placed")] = Path("filedb.arrow"), - threads: Annotated[int, Option(help="multiprocessing threads")] = CPU_COUNT * 3 // 4, + config_path: Annotated[ + Path, Option(help="Where the dataset config is placed") + ] = Path("config.json"), + database_path: Annotated[Path, Option(help="Where the database is placed")] = Path( + "filedb.arrow" + ), + threads: Annotated[int, Option(help="multiprocessing threads")] = CPU_COUNT + * 3 + // 4, chunksize: Annotated[int, Option(help="imap chunksize")] = 5, - population_chunksize: Annotated[int, Option(help="chunksize when populating the df")] = 100, - population_interval: Annotated[int, Option(help="save interval in secs when populating the df")] = 60, + population_chunksize: Annotated[ + int, Option(help="chunksize when populating the df") + ] = 100, + population_interval: Annotated[ + int, Option(help="save interval in secs when populating the df") + ] = 60, simulate: Annotated[bool, Option(help="stops before conversion")] = False, verbose: Annotated[bool, Option(help="prints converted files")] = False, sort_by: Annotated[str, Option(help="Which database column to sort by")] = "path", @@ -147,12 +159,16 @@ def main( files: list[File] if db_cfg.rules: filter_t = p.add_task("filtering", total=0) - files = [resolved[file] for file in db.filter(set(resolved)).get_column("path")] + files = [ + resolved[file] for file in db.filter(set(resolved)).get_column("path") + ] p.update(filter_t, total=len(files), completed=len(files)) else: files = list(resolved.values()) - scenarios = list(db_cfg.parse_files(p.track(files, description="parsing files"))) + scenarios = list( + db_cfg.parse_files(p.track(files, description="parsing files")) + ) if len(scenarios) != len(files): p.log(f"{len(files) - len(scenarios)} files are completed") diff --git a/imdataset_creator/config_handler.py b/imdataset_creator/config_handler.py index 074fffa..0cf4dde 100644 --- a/imdataset_creator/config_handler.py +++ b/imdataset_creator/config_handler.py @@ -13,23 +13,34 @@ class ConfigHandler: def __init__(self, cfg: MainConfig): # generate `Input`s - self.inputs: list[Input] = [Input.from_cfg(folder["data"]) for folder in cfg["inputs"]] + self.inputs: list[Input] = [ + Input.from_cfg(folder["data"]) for folder in cfg["inputs"] + ] # generate `Output`s - self.outputs: list[Output] = [Output.from_cfg(folder["data"]) for folder in cfg["output"]] + self.outputs: list[Output] = [ + Output.from_cfg(folder["data"]) for folder in cfg["output"] + ] # generate `Producer`s self.producers: list[Producer] = [ - Producer.all_producers[p["name"]].from_cfg(p["data"]) for p in cfg["producers"] + Producer.all_producers[p["name"]].from_cfg(p["data"]) + for p in cfg["producers"] ] # generate `Rule`s - self.rules: list[Rule] = [Rule.all_rules[r["name"]].from_cfg(r["data"]) for r in cfg["rules"]] + self.rules: list[Rule] = [ + Rule.all_rules[r["name"]].from_cfg(r["data"]) for r in cfg["rules"] + ] @overload - def gather_images(self, sort=True, reverse=False) -> Generator[tuple[Path, list[Path]], None, None]: + def gather_images( + self, sort=True, reverse=False + ) -> Generator[tuple[Path, list[Path]], None, None]: ... @overload - def gather_images(self, sort=False, reverse=False) -> Generator[tuple[Path, PathGenerator], None, None]: + def gather_images( + self, sort=False, reverse=False + ) -> Generator[tuple[Path, PathGenerator], None, None]: ... def gather_images( @@ -38,7 +49,12 @@ def gather_images( for input_ in self.inputs: gen = input_.run() if sort: - yield input_.folder, list(map(Path, sorted(map(str, gen), key=alphanumeric_sort, reverse=reverse))) + yield input_.folder, list( + map( + Path, + sorted(map(str, gen), key=alphanumeric_sort, reverse=reverse), + ) + ) else: yield input_.folder, gen @@ -46,7 +62,8 @@ def get_outputs(self, file: File) -> list[OutputScenario]: return [ OutputScenario(str(pth), output.filters) for output in self.outputs - if not (pth := output.folder / Path(output.format_file(file))).exists() or output.overwrite + if not (pth := output.folder / Path(output.format_file(file))).exists() + or output.overwrite ] def parse_files(self, files: Iterable[File]) -> Generator[FileScenario, None, None]: diff --git a/imdataset_creator/datarules/dataset_builder.py b/imdataset_creator/datarules/dataset_builder.py index de9bfd4..c9973c7 100644 --- a/imdataset_creator/datarules/dataset_builder.py +++ b/imdataset_creator/datarules/dataset_builder.py @@ -277,7 +277,8 @@ def get_unfinished_existing(self) -> LazyFrame: def filter(self, lst) -> DataFrame: # noqa: A003 if len(self.unready_rules): warnings.warn( - f"{len(self.unready_rules)} filters are not initialized and will not be populated", stacklevel=2 + f"{len(self.unready_rules)} filters are not initialized and will not be populated", + stacklevel=2, ) vdf: DataFrame = self.__df.filter(pl.col("path").is_in(lst)) @@ -330,6 +331,10 @@ def __repr__(self) -> str: def comply_to_schema(self, schema: SchemaDefinition) -> DataFrame: ... + @overload + def comply_to_schema(self, schema: SchemaDefinition, in_place=False) -> DataFrame: + ... + @overload def comply_to_schema(self, schema: SchemaDefinition, in_place=True) -> None: ... @@ -338,4 +343,5 @@ def comply_to_schema(self, schema: SchemaDefinition, in_place: bool = False) -> new_df: DataFrame = pl.concat((self.__df, DataFrame(schema=schema)), how="diagonal") if in_place: self.__df = new_df + return None return new_df diff --git a/imdataset_creator/datarules/image_rules.py b/imdataset_creator/datarules/image_rules.py index 7b154b2..d705ea7 100644 --- a/imdataset_creator/datarules/image_rules.py +++ b/imdataset_creator/datarules/image_rules.py @@ -2,7 +2,7 @@ import os from collections.abc import Callable -from enum import Enum +from enum import Enum, StrEnum from functools import cache from types import MappingProxyType from typing import Literal, Self @@ -129,7 +129,7 @@ def get_size(pth): } -class HASHERS(str, Enum): +class HASHERS(StrEnum): """ Available hashers. """ diff --git a/imdataset_creator/enum_helpers.py b/imdataset_creator/enum_helpers.py index 53a4a40..c95fdc7 100644 --- a/imdataset_creator/enum_helpers.py +++ b/imdataset_creator/enum_helpers.py @@ -5,4 +5,4 @@ def listostr2listoenum(lst: list[str], enum: type[T]) -> list[T]: - return [enum._member_map_[k] for k in lst] # type: ignore + return [enum[k] for k in lst] # type: ignore diff --git a/imdataset_creator/gui/config_inputs.py b/imdataset_creator/gui/config_inputs.py new file mode 100644 index 0000000..696a5d6 --- /dev/null +++ b/imdataset_creator/gui/config_inputs.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +import contextlib +import functools +from copy import deepcopy as objcopy + +from PySide6.QtCore import QRect, QSize, Qt, Signal, Slot +from PySide6.QtGui import QAction, QDrag, QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent, QMouseEvent +from PySide6.QtWidgets import ( + QCheckBox, + QFrame, + QGridLayout, + QGroupBox, + QLabel, + QMenu, + QProgressBar, + QScrollArea, + QSizePolicy, + QToolButton, + QVBoxLayout, + QWidget, +) + +from ..configs.configtypes import ItemConfig, ItemData +from ..configs.keyworded import Keyworded, fancy_repr +from .frames import apply_tooltip +from .settings_inputs import BaseInput, ItemSettings, SettingsBox, SettingsItem + +JSON_SERIALIZABLE = dict | list | tuple | str | int | float | bool | None + + +def copy_before_exec(f): + @functools.wraps(f) + def func(self, *args, **kwargs): + return f(objcopy(self), *args, **kwargs) + + return func + + +class ProceduralConfigItem(QFrame): + movable: bool = True + + move_down = Signal() + move_up = Signal() + position_changed = Signal() + closed = Signal() + duplicate = Signal() + + n_changed = Signal(int) + total_changed = Signal(int) + + bound_item: type[Keyworded] | Keyworded + + reverted = Signal() + + def __init__(self, item: ItemDeclaration, parent: QWidget | None = None): + super().__init__(parent) + self.declaration = item + self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised) + self.setLineWidth(2) + + self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + (collapse_action := QAction("collapse", self)).triggered.connect(self.toggle_group) + (duplicate_action := QAction("duplicate", self)).triggered.connect(self.duplicate.emit) + (revert_action := QAction("revert to defaults", self)).triggered.connect(self.reverted.emit) + self.addActions([collapse_action, duplicate_action, revert_action]) + + # self._minimum_size = self.size() + self.previous_position = None + + self._n = 0 + self._total = 0 + + self._layout = QVBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + self._layout.setSpacing(0) + self._layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetMinimumSize) + # setup top bar + + topbarwidget = QFrame(self) + topbarwidget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Maximum) + self._top_section_layout = QGridLayout(topbarwidget) + self._top_section_layout.setContentsMargins(6, 6, 6, 6) + top_bar = self._top_bar() + for idx, widget in enumerate(top_bar): + self._top_section_layout.addWidget(widget, 0, idx) + + self._layout.addWidget(topbarwidget) + self.settings_box: SettingsBox | None + if self.declaration.settings is not None: + self.settings_box = self.declaration.create_settings_widget(self) + for row in self.settings_box.rows.values(): + self.reverted.connect(row.reset) + + self._layout.addWidget(self.settings_box) + else: + self.settings_box = None + self.opened = True + + def _top_bar(self) -> list[QWidget]: + widgets: list[QWidget] = [] + self.checkbox = QCheckBox() + self.checkbox.setChecked(True) + if self.declaration.title: + self.checkbox.setText(self.declaration.title) + if self.declaration.description: + apply_tooltip(self.checkbox, self.declaration.description) + + widgets.append(self.checkbox) + + if self.movable: + self.up_arrow = QToolButton() + self.down_arrow = QToolButton() + self.up_arrow.setText("↑") + self.down_arrow.setText("↓") + self.up_arrow.clicked.connect(self.position_changed) + self.up_arrow.clicked.connect(self.move_up) + self.down_arrow.clicked.connect(self.position_changed) + self.down_arrow.clicked.connect(self.move_down) + widgets.append(self.up_arrow) + widgets.append(self.down_arrow) + + self.close_button = QToolButton() + self.close_button.setText("X") + self.close_button.clicked.connect(self.closed) + widgets.append(self.close_button) + + return widgets + + def get(self): + """produces something the item represents""" + if self.settings_box is not None: + return self.declaration.get(self.settings_box) + return self.declaration.get() + + @Slot() + def toggle_group(self): + self.opened = not self.opened + + def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: + self.toggle_group() + event.accept() + + @property + def n(self) -> int: + return self._n + + @n.setter + def n(self, val): + self._n = val + self.n_changed.emit(self._n) + + @property + def total(self): + return self._total + + @total.setter + def total(self, val): + self._total = val + self.total_changed.emit(self._total) + + @property + def enabled(self): + return self.checkbox.isChecked() + + @enabled.setter + def enabled(self, b: bool): + self.checkbox.setChecked(b) + + @property + def opened(self): + return self.__opened + + @opened.setter + def opened(self, b: bool): + if self.settings_box is not None: + self.settings_box.setVisible(b) + # if not b: + # self._minimum_size = self.minimumSize() + # self.setMinimumSize(0, 0) + # else: + # self.setMinimumSize(self._minimum_size) + self.__opened = b + + def cfg_name(self): + return self.declaration.bound_item.cfg_kwd() + + def get_cfg(self): + if self.settings_box is None: + return {} + return self.settings_box.get_cfg() + + def from_cfg(self, dct): + if self.settings_box is not None: + self.settings_box.from_cfg(dct) + + def __repr__(self): + return f"{self.__class__.__name__}({self.cfg_name()})" + + def copy(self): + cfg = self.get_cfg() + new = self.__class__(self.declaration) + new.from_cfg(cfg) + return new + + +class ProceduralConfigList(QGroupBox): # TODO: Better name lmao + n = Signal(int) + total = Signal(int) + + changed = Signal() + + def __init__(self, *items: ItemDeclaration, unique=False, parent=None): + super().__init__(parent) + self._layout = QGridLayout() + self.all_are_unique = unique + # self._layout.setContentsMargins(0, 0, 0, 0) + self.bound_item = ProceduralConfigItem + self.items: list[ProceduralConfigItem] = [] + self.registered_items: dict[str, ItemDeclaration] = {} + self.created_items: set[ItemDeclaration] = set() + + self.setLayout(self._layout) + self.scroll_area = QScrollArea(self) + self.scroll_area.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) + self.scroll_widget = QWidget(self) + + self.name_text = QLabel(self) + self.box = QVBoxLayout(self.scroll_widget) + self.box.setContentsMargins(8, 8, 8, 8) + self.box.setSpacing(5) + self.scroll_widget.setLayout(self.box) + + self.scroll_widget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Maximum) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setWidget(self.scroll_widget) + self.add_box = QToolButton(self) + self.add_box.setText("+") + self.add_box.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + self.add_box.setDisabled(True) + + self.progressbar = QProgressBar(self) + self.progressbar.setFormat("%p% %v/%m") + self.progressbar.hide() + + self.total.connect(self.progressbar.setMaximum) + self.n.connect(self.progressbar.setValue) + + self.add_menu = QMenu(self) + + self.add_box.setMenu(self.add_menu) + + self._layout.addWidget(self.add_box, 0, 0) + self._layout.addWidget(self.name_text, 0, 1) + self._layout.addWidget(self.progressbar, 0, 2) + self._layout.addWidget(self.scroll_area, 1, 0, 1, 3) + + self.register_item(*items) + + def label(self, s: str): + self.name_text.setText(s) + return self + + @Slot() + def update_n(self): + n = 0 + for item in self.items: + n += item.n + self.n.emit(n) + + @Slot() + def update_total(self): + total = 0 + for item in self.items: + total += item.total + + if total > 0 and not self.progressbar.isVisible(): + self.progressbar.setVisible(True) + elif total == 0 and self.progressbar.isVisible(): + self.progressbar.setVisible(False) + self.total.emit(total) + + def update_menu(self): + menu_items = {a.text(): a for a in self.add_menu.actions()} + available_items = { + item.title: item + for item in self.registered_items.values() + if not (item in self.created_items and self.all_are_unique) + } + + for item in menu_items.keys() - available_items.keys(): + self.add_menu.removeAction(menu_items[item]) + + for item in available_items.keys() - menu_items.keys(): + self.add_item_to_menu(available_items[item]) + + if any(actions := self.add_menu.actions()): + if len(actions) != 1: + self.add_box.setMenu(self.add_menu) + else: + self.add_box.setMenu(None) # type: ignore + with contextlib.suppress(RuntimeError): + self.add_box.clicked.disconnect() + self.add_box.clicked.connect(actions[0].trigger) + + self.add_box.setEnabled(True) + elif self.add_box.isVisible(): + self.add_box.setEnabled(False) + return + + def add_item_to_menu(self, item: ItemDeclaration): + self.add_menu.addAction(item.title, lambda: self.initialize_item(item)) + + def initialize_item(self, item: ItemDeclaration): + self.add_item(self.bound_item(item, self)) + + def register_item(self, *items: ItemDeclaration): + for item in items: + self._register_item(item) + self.update_menu() + + def _register_item(self, item: ItemDeclaration): + self.registered_items[item.bound_item.cfg_kwd()] = item + if not self.add_box.isEnabled(): + self.add_box.setEnabled(True) + + def add_item(self, item: ProceduralConfigItem, idx=None): + if idx is None: + self.items.append(item) + self.box.addWidget(item) + else: + self.items.insert(idx, item) + self.box.insertWidget(idx, item) + self.created_items.add(item.declaration) + item.move_up.connect(lambda: self.move_item(item, -1)) + item.move_down.connect(lambda: self.move_item(item, 1)) + item.closed.connect(lambda: self.remove_item(item)) + item.duplicate.connect(lambda: self.duplicate_item(item)) + item.n_changed.connect(self.update_n) + item.total_changed.connect(self.update_total) + self.update_menu() + + def remove_item(self, item: ProceduralConfigItem): + item.setGeometry(QRect(0, 0, 0, 0)) + self.box.removeWidget(item) + item.hide() + self.items.remove(item) + self.created_items.remove(item.declaration) + self.update_menu() + + def move_item(self, item: ProceduralConfigItem, direction: int): + """moves items up (-) and down (+) the list + + Parameters + ---------- + item : FlowItem + the item to move + direction : int + how much to move, and what direction + """ + index: int = self.box.indexOf(item) + if index == -1: + return + new_index = min(max(index + direction, 0), self.box.count()) + + self.box.removeWidget(item) + self.box.insertWidget(new_index, item) + self.items.insert(new_index, self.items.pop(index)) + + def duplicate_item(self, item: ProceduralConfigItem): + """duplicates an item""" + self.add_item( + item, + self.box.indexOf(item) + 1, + ) + + def empty(self): + """Removes every item in the list""" + for item in self.items.copy(): + self.remove_item(item) + + @Slot() + def get_cfg(self) -> list[ItemConfig]: + return [ + ItemConfig[ItemData]( + data=item.get_cfg(), + enabled=item.enabled, + name=item.cfg_name(), + open=item.opened, + ) # type: ignore + for item in self.items + ] + + def add_from_cfg(self, lst: list[ItemConfig]): + for new_item in lst: + item: ProceduralConfigItem = ProceduralConfigItem(self.registered_items[new_item["name"]], self) + item.from_cfg(new_item["data"]) + item.enabled = new_item.get("enabled", True) + item.opened = new_item.get("open", False) + self.add_item(item) + + def get(self, include_not_enabled=False) -> list: + if include_not_enabled: + return list(map(ProceduralConfigItem.get, self.items)) + return [item.get() for item in self.items if item.enabled] + + +class ProceduralFlowListSettings(SettingsItem): # TODO: Better name lmao + def __init__(self, *items: ItemDeclaration, parent: QWidget | None = None): + self.items = items + self.parent = parent + + def create(self): + self.widget: ProceduralConfigList = ProceduralConfigList(*self.items, parent=self.parent) + return [self.widget] + + def from_cfg(self, val): + self.widget.add_from_cfg(val) + + def get_cfg(self): + return self.widget.get_cfg() + + def reset(self): + self.widget.empty() + + +class ProceduralFlowListInput(BaseInput): + def __init__(self, *items: ItemDeclaration, parent: QWidget | None = None): + super().__init__() + self.items = items + self.parent: QWidget | None = parent + + def get_settings(self): + return ProceduralFlowListSettings(*self.items, parent=self.parent) + + +@fancy_repr +class ItemDeclaration: + def __init__( + self, + title: str, + bound_item: type[Keyworded], + desc: str | None = None, + settings: ItemSettings | None = None, + ): + self.title: str = title + self.description: str | None = desc + self.bound_item = bound_item + self.settings: ItemSettings | None = settings + + def create_settings_widget(self, parent=None): + assert self.settings is not None + return SettingsBox(self.settings, parent) + + def get(self, box: SettingsBox | None = None): + if box is None: + return self.bound_item() + return self.bound_item.from_cfg(box) diff --git a/imdataset_creator/gui/frames.py b/imdataset_creator/gui/frames.py index bf8500d..489c166 100644 --- a/imdataset_creator/gui/frames.py +++ b/imdataset_creator/gui/frames.py @@ -24,355 +24,6 @@ from ..configs.keyworded import Keyworded -class FlowItem(QFrame): # TODO: Better name lmao - title: str = "" - desc: str = "" - needs_settings: bool = False - movable: bool = True - - move_down = Signal() - move_up = Signal() - position_changed = Signal() - closed = Signal() - duplicate = Signal() - - n_changed = Signal(int) - total_changed = Signal(int) - - bound_item: type[Keyworded] | Keyworded - - def __init__(self, parent=None): - super().__init__(parent) - self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) - self.setLineWidth(2) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Maximum) - - self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) - (collapse_action := QAction("collapse", self)).triggered.connect(self.toggle_group) - (duplicate_action := QAction("duplicate", self)).triggered.connect(self.duplicate.emit) - (revert_action := QAction("revert to defaults", self)).triggered.connect(self.reset_settings_group) - self.addActions([collapse_action, duplicate_action, revert_action]) - - self._minimum_size = self.size() - self.previous_position = None - - self._n = 0 - self._total = 0 - - self.setup_widget() - self.configure_settings_group() - self.reset_settings_group() - self.opened = True - - def setup_widget(self): - self._layout = QGridLayout() - self.setLayout(self._layout) - - top_bar: list[QWidget] = self._top_bar() - for idx, widget in enumerate(top_bar): - self._layout.addWidget(widget, 0, idx) - - self.group = QGroupBox() - self.description_widget = QLabel(self.desc, self) - - self.description_widget.hide() - self.description_widget.setWordWrap(True) - - self.group_grid = QGridLayout() - self.group.hide() - self.group.setLayout(self.group_grid) - self._layout.addWidget(self.description_widget, 1, 0, 1, len(top_bar)) - self._layout.addWidget(self.group, 2, 0, 1, len(top_bar)) - - def _top_bar(self) -> list[QWidget]: - widgets: list[QWidget] = [] - self.checkbox = QCheckBox() - self.checkbox.setChecked(True) - if self.title: - self.checkbox.setText(self.title) - widgets.append(self.checkbox) - - if self.movable: - self.up_arrow = QToolButton() - self.down_arrow = QToolButton() - self.up_arrow.setText("↑") - self.down_arrow.setText("↓") - self.up_arrow.clicked.connect(self.position_changed) - self.up_arrow.clicked.connect(self.move_up) - self.down_arrow.clicked.connect(self.position_changed) - self.down_arrow.clicked.connect(self.move_down) - widgets.append(self.up_arrow) - widgets.append(self.down_arrow) - - self.close_button = QToolButton() - self.close_button.setText("X") - self.close_button.clicked.connect(self.closed) - widgets.append(self.close_button) - - return widgets - - @abstractmethod - def get(self): - """produces something the item represents""" - - @abstractmethod - def configure_settings_group(self) -> None: - ... - - @abstractmethod - def reset_settings_group(self): - ... - - @Slot() - def toggle_group(self): - self.opened = not self.opened - - def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: - self.toggle_group() - event.accept() - - @property - def n(self) -> int: - return self._n - - @n.setter - def n(self, val): - self._n = val - self.n_changed.emit(self._n) - - @property - def total(self): - return self._total - - @total.setter - def total(self, val): - self._total = val - self.total_changed.emit(self._total) - - @property - def enabled(self): - return self.checkbox.isChecked() - - @enabled.setter - def enabled(self, b: bool): - self.checkbox.setChecked(b) - - @property - def opened(self): - return self.__opened - - @opened.setter - def opened(self, b: bool): - if self.needs_settings: - self.group.setVisible(b) - if self.desc: - self.description_widget.setVisible(b) - if not b: - self._minimum_size = self.minimumSize() - self.setMinimumSize(0, 0) - else: - self.setMinimumSize(self._minimum_size) - self.__opened = b - - @classmethod - def cfg_name(cls): - return cls.bound_item.cfg_kwd() - - # Saving and creating methods - - @abstractmethod - def get_config(self) -> ItemData: - return {} - - @classmethod - @abstractmethod - def from_config(cls, cfg: ItemData, parent=None): - return cls(parent=parent) - - def mouseMoveEvent(self, event: QMouseEvent) -> None: - if event.buttons() == Qt.MouseButton.LeftButton and self.opened and self.previous_position is not None: - pos_change = event.position() - self.previous_position - new_size = QSize(self.size().width(), int(self.size().height() + pos_change.y())) - self.setMinimumHeight(new_size.height()) - self.previous_position = event.position() - return super().mouseMoveEvent(event) - - def mousePressEvent(self, event: QMouseEvent) -> None: - self.previous_position = event.position() - return super().mousePressEvent(event) - - def __repr__(self): - return f"{self.__class__.__name__}({self.cfg_name()})" - - -class FlowList(QGroupBox): # TODO: Better name lmao - n = Signal(int) - total = Signal(int) - - changed = Signal() - - def __init__(self, parent=None): - super().__init__(parent) - self._layout = QGridLayout() - self.items: list[FlowItem] = [] - self.registered_items: dict[str, type[FlowItem]] = {} - - self.setLayout(self._layout) - self.scroll_area = QScrollArea(self) - self.scroll_area.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) - self.scroll_widget = QWidget(self) - - self.name_text = QLabel(self) - self.box = QVBoxLayout(self.scroll_widget) - self.scroll_widget.setLayout(self.box) - - self.scroll_widget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Maximum) - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setWidget(self.scroll_widget) - self.add_box = QToolButton(self) - self.add_box.setText("+") - self.add_box.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) - self.add_box.hide() - self.add_button = QToolButton(self) - self.add_button.setText("+") - self.add_button.hide() - self.progressbar = QProgressBar(self) - self.progressbar.setFormat("%p% %v/%m") - self.progressbar.hide() - - self.total.connect(self.progressbar.setMaximum) - self.n.connect(self.progressbar.setValue) - - self.add_menu = QMenu(self) - self.add_box.setMenu(self.add_menu) - - self._layout.addWidget(self.add_box, 0, 0) - self._layout.addWidget(self.add_button, 0, 0) - self._layout.addWidget(self.name_text, 0, 1) - self._layout.addWidget(self.progressbar, 0, 2) - self._layout.addWidget(self.scroll_area, 1, 0, 1, 3) - - def set_text(self, s: str): - self.name_text.setText(s) - - @Slot() - def update_n(self): - n = 0 - for item in self.items: - n += item.n - self.n.emit(n) - - @Slot() - def update_total(self): - total = 0 - for item in self.items: - total += item.total - - if total > 0 and not self.progressbar.isVisible(): - self.progressbar.setVisible(True) - elif total == 0 and self.progressbar.isVisible(): - self.progressbar.setVisible(False) - self.total.emit(total) - - def _register_item(self, item: type[FlowItem]): - self.add_item_to_menu(item) - self.registered_items[item.cfg_name()] = item - if len(self.add_menu.actions()) == 1: - self.add_button.show() - self.add_button.clicked.connect(self.add_menu.actions()[0].trigger) - elif not self.add_box.isVisible(): - self.add_box.show() - self.add_button.hide() - - def add_item_to_menu(self, item: type[FlowItem]): - self.add_menu.addAction(item.title, lambda: self.initialize_item(item)) - - def initialize_item(self, item: type[FlowItem]): - self.add_item(item(self)) - - def register_item(self, *items: type[FlowItem]): - for item in items: - self._register_item(item) - - def add_item(self, item: FlowItem, idx=None): - if idx is None: - self.items.append(item) - self.box.addWidget(item) - else: - self.items.insert(idx, item) - self.box.insertWidget(idx, item) - item.move_up.connect(lambda: self.move_item(item, -1)) - item.move_down.connect(lambda: self.move_item(item, 1)) - item.closed.connect(lambda: self.remove_item(item)) - item.duplicate.connect(lambda: self.duplicate_item(item)) - item.n_changed.connect(self.update_n) - item.total_changed.connect(self.update_total) - - def remove_item(self, item: FlowItem): - item.setGeometry(QRect(0, 0, 0, 0)) - self.box.removeWidget(item) - item.hide() - self.items.remove(item) - - def move_item(self, item: FlowItem, direction: int): - """moves items up (-) and down (+) the list - - Parameters - ---------- - item : FlowItem - the item to move - direction : int - how much to move, and what direction - """ - index: int = self.box.indexOf(item) - if index == -1: - return - new_index = min(max(index + direction, 0), self.box.count()) - - self.box.removeWidget(item) - self.box.insertWidget(new_index, item) - self.items.insert(new_index, self.items.pop(index)) - - def duplicate_item(self, item: FlowItem): - """duplicates an item""" - self.add_item( - item.from_config(item.get_config(), parent=self), - self.box.indexOf(item) + 1, - ) - - def empty(self): - for item in self.items.copy(): - self.remove_item(item) - - @Slot() - def get_config(self) -> list[ItemConfig]: - return [ - ItemConfig[ItemData]( - data=item.get_config(), - enabled=item.enabled, - name=item.cfg_name(), - open=item.opened, - ) # type: ignore - for item in self.items - ] - - def add_from_cfg(self, lst: list[ItemConfig]): - for new_item in lst: - item: FlowItem = self.registered_items[new_item["name"]].from_config(new_item["data"], parent=self) - item.enabled = new_item.get("enabled", True) - item.opened = new_item.get("open", False) - self.add_item(item) - - def get(self, include_not_enabled=False) -> list: - if include_not_enabled: - return list(map(FlowItem.get, self.items)) - return [item.get() for item in self.items if item.enabled] - - -class BuilderDependencyList(FlowList): - builder_changed = Signal() - - class MiniCheckList(QFrame): def __init__(self, items: Collection[str], *args, **kwargs): super().__init__(*args, **kwargs) @@ -400,11 +51,15 @@ def set_config(self, i: str, val: bool): def get_enabled(self): return [i for i, item in self.items.items() if item.isChecked()] + def update_items(self, dct: dict[str, bool]): + for item, val in dct.items(): + self.items[item].setChecked(val) + -UnderlineFont = QFont() -UnderlineFont.setUnderline(True) +TooltipFont = QFont() +TooltipFont.setUnderline(True) -def tooltip(widget: QWidget, txt: str): +def apply_tooltip(widget: QWidget, txt: str): widget.setToolTip(txt) - widget.setFont(UnderlineFont) + widget.setFont(TooltipFont) diff --git a/imdataset_creator/gui/input_view.py b/imdataset_creator/gui/input_view.py index 6956d26..07c1423 100644 --- a/imdataset_creator/gui/input_view.py +++ b/imdataset_creator/gui/input_view.py @@ -1,36 +1,14 @@ from __future__ import annotations import logging -import time -from pathlib import Path -from PySide6.QtCore import QRect, QThread, Signal, Slot -from PySide6.QtGui import QIcon -from PySide6.QtWidgets import ( - QAbstractItemView, - QFileDialog, - QLabel, - QLineEdit, - QListView, - QTextEdit, - QToolButton, - QTreeView, - QWidget, -) - -from .. import File, Input -from ..configs.configtypes import InputData -from .err_dialog import catch_errors -from .frames import FlowItem, FlowList -from .output_filters import FilterView +from .. import Input +from .config_inputs import ItemDeclaration, ProceduralConfigList +from .settings_inputs import DirectoryInput, ItemSettings, MultilineInput log = logging.getLogger() -class FilterList(FlowList): - items: list[FilterView] - - DEFAULT_IMAGE_FORMATS = ( ".webp", ".bmp", @@ -41,179 +19,41 @@ class FilterList(FlowList): ".tif", ) - -class GathererThread(QThread): - inputobj: Input - - total = Signal(int) - files = Signal(list) - - def run(self): - log.info(f"Starting search in: '{self.inputobj.folder}' with expressions: {self.inputobj.expressions}") - filelist = [] - - count = 0 - self.total.emit(0) - emit_timer = time.time() - for file in self.inputobj.run(): - count += 1 - if (new_time := time.time()) > emit_timer + 0.2: - self.total.emit(count) - emit_timer = new_time - filelist.append(file) - - log.info(f"Gathered {count} files from '{self.inputobj.folder}'") - self.total.emit(count) - self.files.emit(filelist) - - -class InputView(FlowItem): - needs_settings = True - movable = False - - text: QLineEdit - - gathered = Signal(dict) - - bound_item = Input - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def setup_widget(self): - super().setup_widget() - - self.text = QLineEdit(self) - self.text.textChanged.connect(self.top_text.setText) - - self.file_select = QToolButton(self) - self.file_select.setText("...") - self.file_select.setIcon(QIcon.fromTheme("folder-open")) - self.file_select.clicked.connect(self.select_folder) - self.group_grid.setGeometry(QRect(0, 0, 800, 800)) - self.group_grid.addWidget(QLabel("Folder: ", self), 0, 0) - self.group_grid.addWidget(self.text, 0, 1) - self.group_grid.addWidget(self.file_select, 0, 2) - - self.filedialog = QFileDialog(self) - self.filedialog.setFileMode(QFileDialog.FileMode.Directory) - self.filedialog.setOption(QFileDialog.Option.ShowDirsOnly, True) - self.filedialog.setOption(QFileDialog.Option.DontResolveSymlinks, True) - - file_view: QListView = self.filedialog.findChild(QListView, "listView") # type: ignore - if file_view: - file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - f_tree_view: QTreeView = self.filedialog.findChild(QTreeView) # type: ignore - if f_tree_view: - f_tree_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - - def configure_settings_group(self): - self.gather_button = QToolButton(self) - self.glob_exprs = QTextEdit(self) - self.gatherer = GathererThread(self) - - self.gatherer.total.connect(self.file_count.setNum) - self.gatherer.total.connect(self.on_total) - self.gatherer.files.connect(self.on_gathered) - self.gatherer.started.connect(self.on_started) - self.gatherer.finished.connect(self.on_finished) - - self.gather_button.setText("gather") - self.gather_button.clicked.connect(self.get) - - self.group_grid.addWidget(QLabel("Search patterns:", self), 1, 0, 1, 3) - self.group_grid.addWidget(self.glob_exprs, 2, 0, 1, 3) - self.group_grid.addWidget(self.gather_button, 3, 0, 1, 1) - - def _top_bar(self) -> list[QWidget]: - top: list[QWidget] = super()._top_bar() - self.top_text = QLabel(self) - self.file_count = QLabel(self) - - top[:1] += [self.top_text, self.file_count] - return top - - @Slot(int) - def on_total(self, val): - self.total = val - - @Slot() - def on_started(self): - self.n = 0 - self.setEnabled(False) - - @Slot() - def on_finished(self): - self.n = self.total - self.setEnabled(True) - - @Slot() - def reset_settings_group(self): - self.text.clear() - self.glob_exprs.setText("\n".join(f"**/*{ext}" for ext in DEFAULT_IMAGE_FORMATS)) - - @catch_errors("gathering failed") - @Slot() - def get(self): - self.total = 0 - self.n = 0 - if not self.text.text(): - raise NotADirectoryError(self.text.text()) - - self.gatherer.inputobj = Input(Path(self.text.text()), self.glob_exprs.toPlainText().splitlines()) - self.gatherer.start() - - @Slot(dict) - def on_gathered(self, lst): - self.gathered.emit({self.text.text(): [File.from_src(Path(self.text.text()), file) for file in lst]}) - - def get_config(self) -> InputData: - return { - "folder": self.text.text(), - "expressions": self.glob_exprs.toPlainText().splitlines(), +InputView_ = ItemDeclaration( + "Input", + Input, + settings=ItemSettings( + { + "folder": DirectoryInput().label("Folder:"), + "expressions": MultilineInput( + default="\n".join(f"**/*{ext}" for ext in DEFAULT_IMAGE_FORMATS), + is_list=True, + ), } + ), +) - @classmethod - def from_config(cls, cfg: InputData, parent=None): - self = cls(parent) - self.text.setText(cfg["folder"]) - self.glob_exprs.setText("\n".join(cfg["expressions"])) - - return self - - @Slot() - def select_folder(self): - # ! this as a whole is very fucky - - files = self._select_folder() - if files: - while len(files) > 1: - self.text.setText(files.pop(0)) - self.duplicate.emit() - self.text.setText(files.pop(0)) - - def _select_folder(self): - self.filedialog.setDirectory(self.text.text() or str(Path.home())) - if self.filedialog.exec(): - return self.filedialog.selectedFiles() - return [] - - -class InputList(FlowList): - items: list[InputView] - - gathered = Signal(dict) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_text("Inputs") - self.register_item(InputView) - - def add_item(self, item: InputView, *args, **kwargs): - item.gathered.connect(self.gathered.emit) - return super().add_item(item, *args, **kwargs) - @Slot() - def gather_all(self): - for item in self.items: - item.get() +def InputList(parent=None): + return ProceduralConfigList(InputView_, parent=parent).label("Inputs") + + +# class GathererThread(QThread): +# inputobj: Input +# total = Signal(int) +# files = Signal(list) +# def run(self): +# log.info(f"Starting search in: '{self.inputobj.folder}' with expressions: {self.inputobj.expressions}") +# filelist = [] +# count = 0 +# self.total.emit(0) +# emit_timer = time.time() +# for file in self.inputobj.run(): +# count += 1 +# if (new_time := time.time()) > emit_timer + 0.2: +# self.total.emit(count) +# emit_timer = new_time +# filelist.append(file) +# log.info(f"Gathered {count} files from '{self.inputobj.folder}'") +# self.total.emit(count) +# self.files.emit(filelist) diff --git a/imdataset_creator/gui/main_window.py b/imdataset_creator/gui/main_window.py index 29fc7e5..42f2661 100644 --- a/imdataset_creator/gui/main_window.py +++ b/imdataset_creator/gui/main_window.py @@ -71,8 +71,6 @@ def __init__(self, parent, cfg_path=Path("config.json")): # self.set_builder_button.clicked.connect(self.set_builder) self.input_list = InputList(self) - self.input_list.gathered.connect(self.collect_files) - self.file_dict: dict[str, list[File]] = {} # self.run_all_inputs_button = QPushButton("Gather all inputs", self) # self.run_all_inputs_button.clicked.connect(self.input_list.gather_all) @@ -139,10 +137,10 @@ def __init__(self, parent, cfg_path=Path("config.json")): def get_config(self) -> MainConfig: return { - "inputs": self.input_list.get_config(), - "output": self.output_list.get_config(), - "producers": self.producer_list.get_config(), - "rules": self.rule_list.get_config(), + "inputs": self.input_list.get_cfg(), + "output": self.output_list.get_cfg(), + "producers": self.producer_list.get_cfg(), + "rules": self.rule_list.get_cfg(), } @catch_errors("Error saving") @@ -244,11 +242,6 @@ def remove_lock(self): log.info("unlocked") ... - @Slot(dict) - def collect_files(self, dct): - for pth, files in dct.items(): - self.file_dict[pth] = files - @catch_building @Slot() def create_builder(self) -> DatasetBuilder: diff --git a/imdataset_creator/gui/output_filters.py b/imdataset_creator/gui/output_filters.py deleted file mode 100644 index d0ee36f..0000000 --- a/imdataset_creator/gui/output_filters.py +++ /dev/null @@ -1,448 +0,0 @@ -from PySide6.QtCore import QSize, Qt -from PySide6.QtWidgets import ( - QCheckBox, - QComboBox, - QDoubleSpinBox, - QFrame, - QLabel, - QLineEdit, - QProgressBar, - QPushButton, - QSlider, - QSpinBox, - QToolButton, - QWidget, -) - -from imdataset_creator.configs.configtypes import ItemData - -from ..datarules import Filter -from ..image_filters import destroyers, resizer -from .frames import FlowItem, FlowList, MiniCheckList, tooltip - - -class FilterView(FlowItem): - title = "Filter" - needs_settings = True - - bound_item: type[Filter] - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(QSize(self.size().width(), 200)) - - def get(self): - super().get() - - -TOOLTIPS = { - "blur_range": "Range of values for blur kernel size or standard deviation (e.g., 1,10)", - "blur_scale": "Adjusts the scaling of the blur range. For average and gaussian, this will add 1 when the new value is even", - "noise_range": "Range of values for noise intensity (e.g., 0,50)", - "scale_factor": "Adjusts the scaling of the noise range", -} - - -class ResizeFilterView(FilterView): - title = "Resize" - - bound_item = resizer.Resize - - def configure_settings_group(self): - self.resize_mode = QComboBox(self) - self.resize_mode.addItems(resizer.ResizeMode._member_names_) - self.scale = QDoubleSpinBox(self) - self.scale.setMinimum(1) - self.scale.setMaximum(100_000) - - self.group_grid.addWidget(QLabel("Resize mode: ", self), 0, 0) - self.group_grid.addWidget(self.resize_mode, 0, 1) - self.group_grid.addWidget(QLabel("Scale:", self), 1, 0) - self.group_grid.addWidget(self.scale, 1, 1) - - def reset_settings_group(self): - self.scale.setValue(100) - - def get_config(self): - return {"scale": self.scale.value() / 100} - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.scale.setValue(cfg["scale"] * 100) - return self - - -class CropFilterView(FilterView): - title = "Crop" - desc = "Crop the image to the specified size. If the item is 0, it will not be considered" - - bound_item = resizer.Crop - - def configure_settings_group(self) -> None: - self.left_box = QSpinBox(self) - self.top_box = QSpinBox(self) - self.width_box = QSpinBox(self) - self.height_box = QSpinBox(self) - self.left_box.setMaximum(9_999_999) - self.top_box.setMaximum(9_999_999) - self.width_box.setMaximum(9_999_999) - self.height_box.setMaximum(9_999_999) - - self.group_grid.addWidget(QLabel("Left:", self), 0, 0) - self.group_grid.addWidget(self.left_box, 0, 1) - self.group_grid.addWidget(QLabel("Top:", self), 1, 0) - self.group_grid.addWidget(self.top_box, 1, 1) - self.group_grid.addWidget(QLabel("Width:", self), 2, 0) - self.group_grid.addWidget(self.width_box, 2, 1) - self.group_grid.addWidget(QLabel("Height:", self), 3, 0) - self.group_grid.addWidget(self.height_box, 3, 1) - - def get_config(self) -> resizer.CropData: - return { - "left": val if (val := self.left_box.value()) else None, - "top": val if (val := self.top_box.value()) else None, - "width": val if (val := self.width_box.value()) else None, - "height": val if (val := self.height_box.value()) else None, - } - - @classmethod - def from_config(cls, cfg: resizer.CropData, parent=None): - self = cls(parent) - self.left_box.setValue(cfg["left"] or 0) - self.top_box.setValue(cfg["top"] or 0) - self.width_box.setValue(cfg["width"] or 0) - self.height_box.setValue(cfg["height"] or 0) - return self - - -class BlurFilterView(FilterView): - title = "Blur" - - bound_item = destroyers.Blur - - def configure_settings_group(self): - self.algorithms = MiniCheckList(destroyers.BlurAlgorithm._member_names_, self) - self.scale = QDoubleSpinBox(self) - scale_label = QLabel("Scale:", self) - tooltip(scale_label, TOOLTIPS["blur_scale"]) - - self.scale.setMinimum(0) - self.scale.setMaximum(100) - self.scale.setSingleStep(0.1) - - blur_label = QLabel("Blur Range:", self) - tooltip(blur_label, TOOLTIPS["blur_range"]) - self.blur_range_x = QSpinBox(self) - self.blur_range_x.setMinimum(0) - self.blur_range_y = QSpinBox(self) - self.blur_range_y.setMinimum(0) - - self.group_grid.addWidget(self.algorithms, 0, 0, 1, 2) - self.group_grid.addWidget(scale_label, 1, 0) - self.group_grid.addWidget(self.scale, 1, 1) - self.group_grid.addWidget(blur_label, 2, 0) - self.group_grid.addWidget(self.blur_range_x, 2, 1) - self.group_grid.addWidget(self.blur_range_y, 3, 1) - - def reset_settings_group(self): - self.algorithms.disable_all() - self.scale.setValue(0.25) - self.blur_range_x.setValue(1) - self.blur_range_y.setValue(16) - - def get_config(self) -> destroyers.BlurData: - algos = self.algorithms.get_enabled() - if not algos: - raise EmptyAlgorithmsError(self) - return destroyers.BlurData( - { - "algorithms": algos, - "blur_range": [self.blur_range_x.value(), self.blur_range_y.value()], - "scale": self.scale.value(), - } - ) - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.scale.setValue(cfg["scale"]) - for item in cfg["algorithms"]: - self.algorithms.set_config(item, True) - r_x, r_y = cfg["blur_range"] - self.blur_range_x.setValue(r_x) - self.blur_range_y.setValue(r_y) - - return self - - -class NoiseFilterView(FilterView): - title = "Noise" - - bound_item = destroyers.Noise - - def configure_settings_group(self): - self.algorithms = MiniCheckList(destroyers.NoiseAlgorithm._member_names_, self) - self.scale = QDoubleSpinBox(self) - self.scale.setSuffix("%") - self.scale.setMinimum(1) - self.scale.setMaximum(1_000) - intensity_label = QLabel("Intensity Range:", self) - tooltip(intensity_label, TOOLTIPS["noise_range"]) - self.intensity_range_x = QSpinBox(self) - self.intensity_range_x.setMinimum(0) - self.intensity_range_y = QSpinBox(self) - self.intensity_range_y.setMinimum(0) - - self.group_grid.addWidget(self.algorithms, 0, 0, 1, 2) - self.group_grid.addWidget(QLabel("Scale:", self), 1, 0) - self.group_grid.addWidget(self.scale, 1, 1) - self.group_grid.addWidget(intensity_label, 2, 0) - self.group_grid.addWidget(self.intensity_range_x, 2, 1) - self.group_grid.addWidget(self.intensity_range_y, 3, 1) - - def reset_settings_group(self): - self.scale.setValue(25) - self.algorithms.disable_all() - self.intensity_range_x.setValue(1) - self.intensity_range_y.setValue(16) - - def get_config(self) -> destroyers.NoiseData: - algos = self.algorithms.get_enabled() - if not algos: - raise EmptyAlgorithmsError(self) - return destroyers.NoiseData( - { - "algorithms": algos, - "intensity_range": [self.intensity_range_x.value(), self.intensity_range_y.value()], - "scale": self.scale.value() / 100, - } - ) - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.scale.setValue(cfg["scale"] * 100) - for item in cfg["algorithms"]: - self.algorithms.set_config(item, True) - r_x, r_y = cfg["intensity_range"] - self.intensity_range_x.setValue(r_x) - self.intensity_range_y.setValue(r_y) - - return self - - -class CompressionFilterView(FilterView): - title = "Compression" - - bound_item = destroyers.Compression - - def configure_settings_group(self): - self.algorithms = MiniCheckList(destroyers.CompressionAlgorithms._member_names_, self) - self.group_grid.addWidget(self.algorithms, 0, 0, 1, 3) - - # jpeg quality - self.j_range_min = QSpinBox(self) - self.j_range_max = QSpinBox(self) - self.j_range_max.setMaximum(100) - self.j_range_min.valueChanged.connect(self.j_range_max.setMinimum) - self.j_range_max.valueChanged.connect(self.j_range_min.setMaximum) - self.group_grid.addWidget(QLabel("JPEG quality range:", self), 1, 0) - self.group_grid.addWidget(self.j_range_min, 1, 1) - self.group_grid.addWidget(self.j_range_max, 1, 2) - # webp quality - self.w_range_min = QSpinBox(self) - self.w_range_max = QSpinBox(self) - self.w_range_max.setMaximum(100) - self.w_range_min.valueChanged.connect(self.w_range_max.setMinimum) - self.w_range_max.valueChanged.connect(self.w_range_min.setMaximum) - self.group_grid.addWidget(QLabel("WebP quality range:", self), 2, 0) - self.group_grid.addWidget(self.w_range_min, 2, 1) - self.group_grid.addWidget(self.w_range_max, 2, 2) - # h264 crf - self.h264_range_min = QSpinBox(self) - self.h264_range_max = QSpinBox(self) - self.h264_range_max.setMaximum(100) - self.h264_range_min.valueChanged.connect(self.h264_range_max.setMinimum) - self.h264_range_max.valueChanged.connect(self.h264_range_min.setMaximum) - self.group_grid.addWidget(QLabel("H264 CRF range:", self), 3, 0) - self.group_grid.addWidget(self.h264_range_min, 3, 1) - self.group_grid.addWidget(self.h264_range_max, 3, 2) - # hevc crf - self.hevc_range_min = QSpinBox(self) - self.hevc_range_max = QSpinBox(self) - self.hevc_range_min.setMaximum(100) - self.hevc_range_min.valueChanged.connect(self.hevc_range_max.setMinimum) - self.hevc_range_max.valueChanged.connect(self.hevc_range_min.setMaximum) - self.group_grid.addWidget(QLabel("HEVC CRF range:", self), 4, 0) - self.group_grid.addWidget(self.hevc_range_min, 4, 1) - self.group_grid.addWidget(self.hevc_range_max, 4, 2) - # mpeg bitrate - self.mpeg_bitrate = QSpinBox(self) - self.mpeg_bitrate.setMaximum(1_000_000_000) # idek what this is in gb - self.group_grid.addWidget(QLabel("MPEG bitrate:", self), 5, 0) - self.group_grid.addWidget(self.mpeg_bitrate, 5, 1, 1, 2) - # mpeg2 bitrate - self.mpeg2_bitrate = QSpinBox(self) - self.mpeg2_bitrate.setMaximum(1_000_000_000) - self.group_grid.addWidget(QLabel("MPEG2 bitrate:", self), 6, 0) - self.group_grid.addWidget(self.mpeg2_bitrate, 6, 1, 1, 2) - - def reset_settings_group(self): - self.algorithms.disable_all() - self.j_range_min.setValue(0) - self.j_range_max.setValue(100) - self.w_range_min.setValue(1) - self.w_range_max.setValue(100) - self.h264_range_min.setValue(20) - self.h264_range_max.setValue(28) - self.hevc_range_min.setValue(25) - self.hevc_range_max.setValue(33) - - def get_config(self) -> destroyers.CompressionData: - algos = self.algorithms.get_enabled() - if not algos: - raise EmptyAlgorithmsError(self) - return destroyers.CompressionData( - { - "algorithms": algos, - "jpeg_quality_range": [self.j_range_min.value(), self.j_range_max.value()], - "webp_quality_range": [self.w_range_min.value(), self.w_range_max.value()], - "h264_crf_range": [self.h264_range_min.value(), self.h264_range_max.value()], - "hevc_crf_range": [self.hevc_range_min.value(), self.hevc_range_max.value()], - "mpeg_bitrate": self.mpeg_bitrate.value(), - "mpeg2_bitrate": self.mpeg2_bitrate.value(), - } - ) - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - for item in cfg["algorithms"]: - self.algorithms.set_config(item, True) - self.j_range_min.setValue(cfg["jpeg_quality_range"][0]) - self.j_range_max.setValue(cfg["jpeg_quality_range"][1]) - self.w_range_min.setValue(cfg["webp_quality_range"][0]) - self.w_range_max.setValue(cfg["webp_quality_range"][1]) - self.h264_range_min.setValue(cfg["h264_crf_range"][0]) - self.h264_range_max.setValue(cfg["h264_crf_range"][1]) - self.hevc_range_min.setValue(cfg["hevc_crf_range"][0]) - self.hevc_range_max.setValue(cfg["hevc_crf_range"][1]) - self.mpeg_bitrate.setValue(cfg["mpeg_bitrate"]) - self.mpeg2_bitrate.setValue(cfg["mpeg2_bitrate"]) - - return self - - -class RandomFlipFilterView(FilterView): - title = "Random Flip" - - bound_item = resizer.RandomFlip - - def configure_settings_group(self): - self.flip_x_slider = QSlider(Qt.Orientation.Horizontal, self) - self.flip_x_slider.setMaximum(100) - self.flip_x_slider.setMinimum(0) - self.flip_y_slider = QSlider(Qt.Orientation.Horizontal, self) - self.flip_y_slider.setMaximum(100) - self.flip_y_slider.setMinimum(0) - self.flip_x_chance = QDoubleSpinBox(self) - self.flip_x_chance.setRange(0, 100) - self.flip_x_chance.setSuffix("%") - self.flip_x_chance.setSingleStep(0.5) - self.flip_y_chance = QDoubleSpinBox(self) - self.flip_y_chance.setRange(0, 100) - self.flip_y_chance.setSuffix("%") - self.flip_y_chance.setSingleStep(0.5) - - self.flip_x_slider.valueChanged.connect(self.flip_x_chance.setValue) - self.flip_y_slider.valueChanged.connect(self.flip_y_chance.setValue) - self.flip_x_chance.valueChanged.connect(self.flip_x_slider.setValue) - self.flip_y_chance.valueChanged.connect(self.flip_y_slider.setValue) - - self.group_grid.addWidget(QLabel("Flip X Chance", self), 0, 0, 1, 1) - self.group_grid.addWidget(self.flip_x_slider, 0, 1, 1, 1) - self.group_grid.addWidget(self.flip_x_chance, 0, 2, 1, 1) - self.group_grid.addWidget(QLabel("Flip Y Chance", self), 1, 0, 1, 1) - self.group_grid.addWidget(self.flip_y_slider, 1, 1, 1, 1) - self.group_grid.addWidget(self.flip_y_chance, 1, 2, 1, 1) - - def reset_settings_group(self): - self.flip_x_chance.setValue(50) - self.flip_y_chance.setValue(50) - - @classmethod - def from_config(cls, cfg: resizer.RandomFlipData, parent=None): - self = cls(parent) - self.flip_x_chance.setValue(cfg["flip_x_chance"] * 100) - self.flip_y_chance.setValue(cfg["flip_y_chance"] * 100) - return self - - def get_config(self) -> resizer.RandomFlipData: - return { - "flip_x_chance": self.flip_x_chance.value() / 100, - "flip_y_chance": self.flip_y_chance.value() / 100, - } - - -class RandomRotateFilterView(FilterView): - title = "Random Rotate" - - bound_item = resizer.RandomRotate - - def configure_settings_group(self) -> None: - self.rotate_chance = QDoubleSpinBox(self) - self.rotate_slider = QSlider(Qt.Orientation.Horizontal, self) - self.rotate_chance.valueChanged.connect(self.rotate_slider.setValue) - self.rotate_slider.valueChanged.connect(self.rotate_chance.setValue) - self.rotate_slider.setRange(0, 100) - - self.rotation_list = MiniCheckList(resizer.RandomRotateDirections._member_names_, self) - self.group_grid.addWidget(self.rotation_list, 0, 0, 1, 3) - self.group_grid.addWidget(QLabel("Rotation chance:", self), 1, 0, 1, 1) - self.group_grid.addWidget(self.rotate_slider, 1, 1, 1, 1) - self.group_grid.addWidget(self.rotate_chance, 1, 2, 1, 1) - - def reset_settings_group(self): - self.rotation_list.disable_all() - - @classmethod - def from_config(cls, cfg: resizer.RandomRotateData, parent=None): - self = cls(parent) - for item in cfg["rotate_directions"]: - self.rotation_list.set_config(item, True) - self.rotate_chance.setValue(cfg["rotate_chance"] * 100) - return self - - def get_config(self) -> resizer.RandomRotateData: - rots = self.rotation_list.get_enabled() - if not rots: - raise EmptyAlgorithmsError(self) - return { - "rotate_chance": self.rotate_chance.value() / 100, - "rotate_directions": rots, - } - - -class EmptyAlgorithmsError(Exception): - """Raised when no algorithms are enabled""" - - def __init__(self, f: FilterView): - super().__init__(f"No algorithms enabled in {f}") - - -class FilterList(FlowList): - items: list[FilterView] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_text("Filters") - self.register_item( - ResizeFilterView, - CropFilterView, - BlurFilterView, - NoiseFilterView, - CompressionFilterView, - RandomFlipFilterView, - RandomRotateFilterView, - ) diff --git a/imdataset_creator/gui/output_view.py b/imdataset_creator/gui/output_view.py index 1911c27..ee916c4 100644 --- a/imdataset_creator/gui/output_view.py +++ b/imdataset_creator/gui/output_view.py @@ -1,71 +1,189 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence -from string import Formatter -from typing import Any +from PySide6.QtCore import Qt -from PySide6.QtWidgets import QCheckBox, QFileDialog, QLabel, QLineEdit - -from ..configs import OutputData from ..datarules import Output -from .frames import FlowList -from .input_view import InputView -from .output_filters import FilterList - - -class OutputView(InputView): - bound_item = Output - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setMouseTracking(True) - self.setMinimumHeight(400) - self.previous_position = None - - def configure_settings_group(self): - self.format_str = QLineEdit(self) - - self.overwrite = QCheckBox(self) - self.overwrite.setText("overwrite existing files") - - self.list = FilterList(self) - - self.list.register_item() - self.group_grid.addWidget(self.overwrite, 1, 0, 1, 3) - self.group_grid.addWidget(QLabel("format text: ", self), 2, 0, 1, 3) - self.group_grid.addWidget(self.format_str, 3, 0, 1, 3) - self.group_grid.addWidget(self.list, 4, 0, 1, 3) - - def reset_settings_group(self): - self.format_str.setText("{relative_path}/{file}.{ext}") - self.overwrite.setChecked(False) - self.list.items.clear() - - def get(self) -> Output: - return Output.from_cfg(self.get_config()) - - def get_config(self) -> OutputData: - return { - "folder": self.text.text(), - "output_format": self.format_str.text() or self.format_str.placeholderText(), - "lst": self.list.get_config(), - "overwrite": self.overwrite.isChecked(), +from ..image_filters import destroyers, resizer +from .config_inputs import ItemDeclaration, ProceduralConfigList, ProceduralFlowListInput +from .settings_inputs import ( + BoolInput, + DirectoryInput, + DoubleInput, + DropdownInput, + EnumChecklistInput, + ItemSettings, + NumberInput, + RangeInput, + TextInput, +) + +# class FilterView(FlowItem): +# title = "Filter" +# needs_settings = True + +# bound_item: type[Filter] + +# def __init__(self, parent=None): +# super().__init__(parent) +# self.setMinimumSize(QSize(self.size().width(), 200)) + +# def get(self): +# super().get() + + +TOOLTIPS = { + "blur_range": "Range of values for blur kernel size or standard deviation (e.g., 1,10)", + "blur_scale": "Adjusts the scaling of the blur range. For average and gaussian, this will add 1 when the new value is even", + "noise_range": "Range of values for noise intensity (e.g., 0,50)", + "scale_factor": "Adjusts the scaling of the noise range", +} + + +def mult_100(val): + return val * 100 + + +def div_100(val): + return val / 100 + + +ResizeFilterView_ = ItemDeclaration( + "Resize", + resizer.Resize, + settings=ItemSettings( + { + "mode": ( + DropdownInput(list(resizer.ResizeMode.__members__.values())).label("Resize mode: ") # type: ignore + ), + "scale": ( + DoubleInput((1, 100_000), default=100) + .label("Scale: ") + .from_config_modification(mult_100) + .to_config_modification(div_100) + ), } + ), +) + +CropFilterView_ = ItemDeclaration( + "Crop", + resizer.Crop, + desc="Crop the image to the specified size. If the item is 0, it will not be considered", + settings=ItemSettings( + { + "left": NumberInput((0, 9_999_999)).label("Left:").set_optional(), + "top": NumberInput((0, 9_999_999)).label("Top:").set_optional(), + "width": NumberInput((0, 9_999_999)).label("Width:").set_optional(), + "height": NumberInput((0, 9_999_999)).label("Height").set_optional(), + } + ), +) + + +BlurFilterView_ = ItemDeclaration( + "Blur", + destroyers.Blur, + settings=ItemSettings( + { + "algorithms": EnumChecklistInput(destroyers.BlurAlgorithm), + "scale": DoubleInput((0, 100), default=0.25, step=0.1).label("Scale:").tooltip(TOOLTIPS["blur_scale"]), + "blur_range": (RangeInput(min_and_max_correlate=True).label("Blur Range:").tooltip(TOOLTIPS["blur_range"])), + }, + ), +) + +NoiseFilterView_ = ItemDeclaration( + "Noise", + destroyers.Noise, + settings=ItemSettings( + { + "algorithms": EnumChecklistInput(destroyers.NoiseAlgorithm), + "intensity_range": RangeInput().label("Intensity Range:").tooltip(TOOLTIPS["noise_range"]), + "scale": ( + NumberInput((1, 1_000), default=25) + .label("Scale:") + .from_config_modification(mult_100) + .to_config_modification(div_100) + ), + }, + ), +) + + +CompressionFilterView_ = ItemDeclaration( + "Compression", + destroyers.Compression, + settings=ItemSettings( + { + "algorithms": EnumChecklistInput(destroyers.CompressionAlgorithms), + "jpeg_quality_range": RangeInput(default=(0, 100)).label("JPEG quality:"), + "webp_quality_range": RangeInput(default=(1, 100)).label("WebP quality:"), + "h264_crf_range": RangeInput(default=(20, 28)).label("H.264 CRF"), + "hevc_crf_range": RangeInput(default=(25, 33)).label("HEVC CRF"), + "mpeg_bitrate": NumberInput((0, 1_000_000_000)).label("MPEG bitrate:"), + "mpeg2_bitrate": NumberInput((0, 1_000_000_000)).label("MPEG2 bitrate:"), + }, + ), +) + +RandomFlipFilterView_ = ItemDeclaration( + "Random Flip", + bound_item=resizer.RandomFlip, + settings=ItemSettings( + { + "flip_x_chance": ( + DoubleInput((0, 100), default=50, slider=Qt.Orientation.Horizontal) + .label("horizontal flip chance:") + .from_config_modification(mult_100) + .to_config_modification(div_100) + ), + "flip_y_chance": ( + DoubleInput((0, 100), default=50, slider=Qt.Orientation.Horizontal) + .label("vertical flip chance:") + .from_config_modification(mult_100) + .to_config_modification(div_100) + ), + }, + ), +) + +RandomRotateFilterView_ = ItemDeclaration( + "Random Rotate", + bound_item=resizer.RandomRotate, + settings=ItemSettings( + { + "rotate_direction": (EnumChecklistInput(resizer.RandomRotateDirections)), + "rotate_chance": ( + DoubleInput((0, 100), default=50, slider=Qt.Orientation.Horizontal) + .from_config_modification(mult_100) + .to_config_modification(div_100) + .label("Rotation chance:") + ), + }, + ), +) + +OutputView_ = ItemDeclaration( + "Output", + Output, + settings=ItemSettings( + { + "folder": DirectoryInput().label("Folder: "), + "output_format": TextInput(default="{relative_path}/{file}.{ext}"), + "overwrite": BoolInput(default=False).label("overwrite existing files"), + "lst": ProceduralFlowListInput( + ResizeFilterView_, + CropFilterView_, + BlurFilterView_, + NoiseFilterView_, + CompressionFilterView_, + RandomFlipFilterView_, + RandomRotateFilterView_, + ), + } + ), +) - @classmethod - def from_config(cls, cfg: OutputData, parent=None): - self = cls(parent) - self.text.setText(cfg["folder"]) - self.format_str.setText(cfg["output_format"]) - self.list.add_from_cfg(cfg["lst"]) - self.overwrite.setChecked(cfg["overwrite"]) - return self - - -class OutputList(FlowList): - items: list[OutputView] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_text("Outputs") - self.register_item(OutputView) +def OutputList(parent=None): + return ProceduralConfigList(OutputView_, parent=parent) diff --git a/imdataset_creator/gui/producer_views.py b/imdataset_creator/gui/producer_views.py index 504832b..41dcbe2 100644 --- a/imdataset_creator/gui/producer_views.py +++ b/imdataset_creator/gui/producer_views.py @@ -1,93 +1,44 @@ from __future__ import annotations -from PySide6.QtWidgets import QComboBox, QLabel - from ..datarules import base_rules, data_rules, image_rules -from .frames import BuilderDependencyList, FlowItem, FlowList - - -class ProducerView(FlowItem): - title = "Producer" - movable = False - - bound_item: type[base_rules.Producer] - - def setup_widget(self): - super().setup_widget() - if self.desc: - self.desc += "\n" - self.desc += f"Produces: {set(self.bound_item.produces)}" - self.description_widget.setText(self.desc) - - -class FileInfoProducerView(ProducerView): - title = "File Info Producer" - - bound_item = data_rules.FileInfoProducer - - def get(self): - super().get() - return self.bound_item() - - -class ImShapeProducerView(ProducerView): - title = "Image shape" - bound_item = image_rules.ImShapeProducer - - def get(self): - super().get() - return self.bound_item() - - -class HashProducerView(ProducerView): - title = "Hash Producer" - desc = "gets a hash for the contents of an image" - bound_item: type[image_rules.HashProducer] = image_rules.HashProducer - needs_settings = True - - def configure_settings_group(self): - self.hash_type = QComboBox() - self.hash_type.addItems([*image_rules.HASHERS]) - self.group_grid.addWidget(QLabel("Hash type: ", self), 0, 0) - self.group_grid.addWidget(self.hash_type, 0, 1) - - def reset_settings_group(self): - self.hash_type.setCurrentIndex(0) - - def get_config(self): - return {"hash_type": self.hash_type.currentText()} - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.hash_type.setCurrentText(cfg["hash_type"]) - return self - - def get(self): - super().get() - return self.bound_item(image_rules.HASHERS(self.hash_type.currentText())) - - -class ProducerList(BuilderDependencyList): - items: list[ProducerView] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__registered_by: dict[str, type[ProducerView]] = {} - self.set_text("Producers") - self.register_item( - FileInfoProducerView, - ImShapeProducerView, - HashProducerView, - ) - - def add_item_to_menu(self, item: type[ProducerView]): - self.add_menu.addAction(f"{item.title}: {set(item.bound_item.produces)}", lambda: self.initialize_item(item)) - - def _register_item(self, item: type[ProducerView]): - super()._register_item(item) - for produces in item.bound_item.produces: - self.__registered_by[produces] = item - - def registered_by(self, s: str): - return self.__registered_by.get(s) +from .config_inputs import ( + ItemDeclaration, + ItemSettings, + ProceduralConfigList, +) +from .settings_inputs import DropdownInput + +# class ProducerView(FlowItem): +# title = "Producer" +# movable = False + +# bound_item: type[base_rules.Producer] + +# def setup_widget(self): +# super().setup_widget() +# if self.desc: +# self.desc += "\n" +# self.desc += f"Produces: {set(self.bound_item.produces)}" +# self.description_widget.setText(self.desc) + + +FileInfoProducerView = ItemDeclaration("File Info Producer", data_rules.FileInfoProducer) +ImShapeProducerView = ItemDeclaration("Image shape", image_rules.ImShapeProducer) +HashProducerView = ItemDeclaration( + "Hash Producer", + image_rules.HashProducer, + desc="gets a hash for the contents of an image", + settings=ItemSettings( + {"hash_type": DropdownInput(list(image_rules.HASHERS.__members__.values())).label("Hash type:")} + ), +) + + +def ProducerList(parent=None) -> ProceduralConfigList: + return ProceduralConfigList( + FileInfoProducerView, + ImShapeProducerView, + HashProducerView, + parent=parent, + unique=True, + ).label("Producers") diff --git a/imdataset_creator/gui/rule_views.py b/imdataset_creator/gui/rule_views.py index 5e06048..68e3435 100644 --- a/imdataset_creator/gui/rule_views.py +++ b/imdataset_creator/gui/rule_views.py @@ -1,349 +1,133 @@ from __future__ import annotations -from abc import abstractmethod -from typing import Self - -from PySide6.QtCore import QDate, QDateTime, QTime, Slot -from PySide6.QtWidgets import QCheckBox, QDateTimeEdit, QLabel, QLineEdit, QSpinBox, QTextEdit - -from ..configs import ItemData -from ..datarules import base_rules, data_rules, image_rules -from .frames import BuilderDependencyList, FlowItem - - -class RuleView(FlowItem): - title = "Rule" - bound_item: type[base_rules.Rule] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_desc = self.desc - - def __init_subclass__(cls) -> None: - cls.__wrap_get() - - def setup_widget(self, *args, **kwargs): - super().setup_widget(*args, **kwargs) - - @abstractmethod - def get(self) -> base_rules.Rule: - """Evaluates the settings and returns a Rule instance""" - super().get() - return base_rules.Rule() - - @classmethod - def __wrap_get(cls: type[Self]): - original_get = cls.get - original_get_config = cls.get_config - - def get_wrapper(self: Self): - rule = original_get(self) - if rule.requires: - if isinstance(rule.requires, base_rules.DataColumn): - self.set_requires(str({rule.requires.name})) - else: - self.set_requires(str(set({r.name for r in rule.requires}))) - return rule - - def get_config_wrapper(self: Self): - self.get() - return original_get_config(self) - - cls.get = get_wrapper - cls.get_config = get_config_wrapper - - def set_requires(self, val): - newdesc = self.__original_desc - if val: - newdesc = newdesc + ("\n" if newdesc else "") + f"requires: {val}" - print("updated requires") - self.desc = newdesc - self.description_widget.setText(newdesc) - if not self.description_widget.isVisible(): - self.description_widget.show() - - -class ItemsUnusedError(ValueError): - def __init__(self): - super().__init__("At least one item must be selected") - - -class StatRuleView(RuleView): - title = "Time Range" - desc = "only allow files created within a time frame." - - needs_settings = True - bound_item = data_rules.StatRule - _datetime_format: str = "dd/MM/yyyy h:mm AP" - - def configure_settings_group(self): - self.use_after = QCheckBox(self) - self.use_before = QCheckBox(self) - self.after_widget = QDateTimeEdit(self) - self.before_widget = QDateTimeEdit(self) - self.after_widget.setCalendarPopup(True) - self.before_widget.setCalendarPopup(True) - self.after_widget.setDisplayFormat(self._datetime_format) - self.before_widget.setDisplayFormat(self._datetime_format) - self.use_after.stateChanged.connect(self.after_widget.setEnabled) - self.use_before.stateChanged.connect(self.before_widget.setEnabled) - format_label = QLabel(self._datetime_format, self) - format_label.setEnabled(False) - self.group_grid.addWidget(format_label, 0, 2, 1, 2) - self.group_grid.addWidget(QLabel("After: ", self), 1, 0) - self.group_grid.addWidget(QLabel("Before: ", self), 2, 0) - self.group_grid.addWidget(self.use_after, 1, 1, 1, 1) - self.group_grid.addWidget(self.use_before, 2, 1, 1, 1) - self.group_grid.addWidget(self.after_widget, 1, 2, 1, 2) - self.group_grid.addWidget(self.before_widget, 2, 2, 1, 2) - - def get(self): - super().get() - if not (self.use_before.isChecked() or self.use_after.isChecked()): - raise ItemsUnusedError() - return data_rules.StatRule( - self.before_widget.dateTime().toString(self._datetime_format) if self.use_before.isChecked() else None, - self.after_widget.dateTime().toString(self._datetime_format) if self.use_after.isChecked() else None, - ) - - def reset_settings_group(self): - self.use_after.setChecked(False) - self.use_before.setChecked(True) - self.after_widget.setDateTime(QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0))) - self.after_widget.setEnabled(False) - self.before_widget.setDateTime(QDateTime.currentDateTime()) - self.before_widget.setEnabled(True) - - def get_config(self): - if not (self.use_before.isChecked() or self.use_after.isChecked()): - raise ItemsUnusedError() - return { - "after": self.after_widget.dateTime().toString(self._datetime_format) - if self.use_after.isChecked() - else None, - "before": self.before_widget.dateTime().toString(self._datetime_format) - if self.use_before.isChecked() - else None, +from PySide6.QtCore import QDate, QDateTime, QTime + +from ..datarules import data_rules, image_rules +from .config_inputs import ( + ItemDeclaration, + ItemSettings, + ProceduralConfigList, +) +from .settings_inputs import ( + BoolInput, + DateTimeInput, + MultilineInput, + NumberInput, + RangeInput, + TextInput, +) + +# class RuleView(FlowItem): +# title = "Rule" +# bound_item: type[base_rules.Rule] +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__original_desc = self.desc +# def __init_subclass__(cls) -> None: +# cls.__wrap_get() +# def setup_widget(self, *args, **kwargs): +# super().setup_widget(*args, **kwargs) +# @abstractmethod +# def get(self) -> base_rules.Rule: +# """Evaluates the settings and returns a Rule instance""" +# super().get() +# return base_rules.Rule() +# @classmethod +# def __wrap_get(cls: type[Self]): +# original_get = cls.get +# original_get_config = cls.get_config +# def get_wrapper(self: Self): +# rule = original_get(self) +# if rule.requires: +# if isinstance(rule.requires, base_rules.DataColumn): +# self.set_requires(str({rule.requires.name})) +# else: +# self.set_requires(str(set({r.name for r in rule.requires}))) +# return rule +# def get_config_wrapper(self: Self): +# self.get() +# return original_get_config(self) +# cls.get = get_wrapper +# cls.get_config = get_config_wrapper +# def set_requires(self, val): +# newdesc = self.__original_desc +# if val: +# newdesc = newdesc + ("\n" if newdesc else "") + f"requires: {val}" +# print("updated requires") +# self.desc = newdesc +# self.description_widget.setText(newdesc) +# if not self.description_widget.isVisible(): +# self.description_widget.show() + + +StatRuleView_ = ItemDeclaration( + "Time Range", + data_rules.StatRule, + desc="Only allow files created within a time frame.", + settings=ItemSettings( + { + "after": DateTimeInput(default=QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0))).label("After:"), + "before": DateTimeInput(default=QDateTime.currentDateTime()).label("Before:"), + }, + ), +) + +BlacklistWhitelistView_ = ItemDeclaration( + "Blacklist and whitelist", + data_rules.BlackWhitelistRule, + desc="Only allows paths that include strs in the whitelist and not in the blacklist", + settings=ItemSettings( + { + "whitelist": MultilineInput(is_list=True), + "blacklist": MultilineInput(is_list=True), + }, + ), +) + + +TotalLimitRuleView_ = ItemDeclaration( + "Total count", + data_rules.TotalLimitRule, + desc="Limits the total number of files past this point", + settings=ItemSettings({"limit": NumberInput((0, 1_000_000_000)).label("Limit:")}), +) + +ResRuleView_ = ItemDeclaration( + "Resolution", + image_rules.ResRule, + settings=ItemSettings( + { + ("min", "max"): RangeInput(default=(128, 2048), min_and_max_correlate=True).label("Min / Max:"), + "crop": BoolInput(default=True).label("Try to crop:"), + "scale": NumberInput((0, 128), default=4).label("Scale:"), } - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - if cfg["after"] is not None: - self.use_after.setChecked(True) - self.after_widget.setDateTime(QDateTime.fromString(cfg["after"], cls._datetime_format)) - if cfg["before"] is not None: - self.use_before.setChecked(True) - self.before_widget.setDateTime(QDateTime.fromString(cfg["before"], cls._datetime_format)) - return self - - -class BlacklistWhitelistView(RuleView): - title = "Blacklist and whitelist" - desc = "Only allows paths that include strs in the whitelist and not in the blacklist" - - needs_settings = True - bound_item = data_rules.BlackWhitelistRule - - def configure_settings_group(self): - self.whitelist = QTextEdit(self) - self.blacklist = QTextEdit(self) - - self.group_grid.addWidget(QLabel("Whitelist: ", self), 0, 0) - self.group_grid.addWidget(QLabel("Blacklist: ", self), 2, 0) - self.group_grid.addWidget(self.whitelist, 1, 0, 1, 2) - self.group_grid.addWidget(self.blacklist, 3, 0, 1, 2) - - def reset_settings_group(self): - self.whitelist.clear() - self.blacklist.clear() - - def get(self): - super().get() - return data_rules.BlackWhitelistRule( - whitelist=self.whitelist.toPlainText().splitlines(), - blacklist=self.blacklist.toPlainText().splitlines(), - ) - - def get_config(self) -> data_rules.BlackWhitelistData: - return data_rules.BlackWhitelistData( - { - "whitelist": self.whitelist.toPlainText().splitlines(), - "blacklist": self.blacklist.toPlainText().splitlines(), - } - ) - - @classmethod - def from_config(cls, cfg: data_rules.BlackWhitelistData, parent=None): - self = cls(parent) - self.whitelist.setText("\n".join(cfg["whitelist"])) - self.blacklist.setText("\n".join(cfg["blacklist"])) - return self - - -class TotalLimitRuleView(RuleView): - title = "Total count" - desc = "Limits the total number of files past this point" - - needs_settings = True - bound_item = data_rules.TotalLimitRule - - def configure_settings_group(self): - self.limit_widget = QSpinBox(self) - self.limit_widget.setRange(0, 1000000000) - self.group_grid.addWidget(QLabel("Limit: ", self), 0, 0) - self.group_grid.addWidget(self.limit_widget, 0, 1) - - def reset_settings_group(self): - self.limit_widget.setValue(0) - - def get(self): - super().get() - return data_rules.TotalLimitRule(self.limit_widget.value()) - - def get_config(self): - return {"limit": self.limit_widget.value()} - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.limit_widget.setValue(cfg["limit"]) - return self - - -class ResRuleView(RuleView): - title = "Resolution" - - needs_settings = True - bound_item = image_rules.ResRule - - def configure_settings_group(self): - self.min = QSpinBox(self) - self.max = QSpinBox(self) - self.crop = QCheckBox(self) - self.scale = QSpinBox(self) - self.min.valueChanged.connect(self.max.setMinimum) - self.max.valueChanged.connect(self.min.setMaximum) - self.min.setMaximum(1_000_000_000) - self.max.setMaximum(1_000_000_000) - self.scale.setRange(1, 128) # I think this is valid - self.group_grid.addWidget(QLabel("Min / Max: ", self), 0, 0) - self.group_grid.addWidget(self.min, 1, 0) - self.group_grid.addWidget(self.max, 1, 1) - self.group_grid.addWidget(QLabel("Try to crop: ", self), 2, 0) - self.group_grid.addWidget(self.crop, 2, 1) - self.group_grid.addWidget(QLabel("Scale: ", self), 3, 0) - self.group_grid.addWidget(self.scale, 3, 1) - - def reset_settings_group(self): - self.min.setValue(0) - self.max.setValue(2048) - self.scale.setValue(4) - self.crop.setChecked(True) - - def get(self): - return image_rules.ResRule( - min_res=self.min.value(), - max_res=self.max.value(), - crop=self.crop.isChecked(), - scale=self.scale.value(), - ) - - def get_config(self) -> image_rules.ResData: - return { - "min_res": self.min.value(), - "max_res": self.max.value(), - "crop": self.crop.isChecked(), - "scale": self.scale.value(), - } - - @classmethod - def from_config(cls, cfg: image_rules.ResData, parent=None): - self = cls(parent) - self.min.setValue(cfg["min_res"]) - self.max.setValue(cfg["max_res"]) - self.crop.setChecked(cfg["crop"]) - self.scale.setValue(cfg["scale"]) - return self - - -class ChannelRuleView(RuleView): - title = "Channels" - - needs_settings = True - bound_item = image_rules.ChannelRule - - def configure_settings_group(self): - self.min = QSpinBox(self) - self.max = QSpinBox(self) - self.min.setMinimum(1) - self.min.valueChanged.connect(self.max.setMinimum) - self.max.setMinimum(1) - self.group_grid.addWidget(QLabel("Min / Max: ", self), 0, 0) - self.group_grid.addWidget(self.min, 0, 1) - self.group_grid.addWidget(self.max, 0, 2) - - def reset_settings_group(self): - self.min.setValue(1) - self.max.setValue(3) - - def get(self): - super().get() - return image_rules.ChannelRule(min_channels=self.min.value(), max_channels=self.max.value()) - - @classmethod - def from_config(cls, cfg: ItemData, parent=None): - self = cls(parent) - self.min.setValue(cfg["min_channels"]) - self.max.setValue(cfg["max_channels"]) - return self - - def get_config(self): - return { - "min_channels": self.min.value(), - "max_channels": self.max.value(), - } - - -class HashRuleView(RuleView): - title = "Hash" - desc = "Uses imagehash functions to eliminate similar looking images" - - needs_settings = True - bound_item = image_rules.HashRule - - def configure_settings_group(self): - self.resolver = QLineEdit(self) - self.resolver.setText("ignore_all") - self.group_grid.addWidget(QLabel("Conflict resolver column: ", self), 0, 0) - self.group_grid.addWidget(self.resolver, 1, 0, 1, 2) - - def get(self): - return image_rules.HashRule(resolver=self.resolver.text()) - - def get_config(self): - return { - "resolver": self.resolver.text(), - } - - @classmethod - def from_config(cls, cfg, parent=None): - self = cls(parent) - self.resolver.setText(cfg["resolver"]) - return self - - -class RuleList(BuilderDependencyList): - items: list[RuleView] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_text("Rules") - self.register_item( - StatRuleView, - BlacklistWhitelistView, - TotalLimitRuleView, - ResRuleView, - ChannelRuleView, - HashRuleView, - ) + ), +) + + +ChannelRuleView_ = ItemDeclaration( + "Channels", + image_rules.ChannelRule, + settings=ItemSettings( + {("min_channels", "max_channels"): RangeInput(min_and_max_correlate=True).label("Min / Max:")} + ), +) + +HashRuleView_ = ItemDeclaration( + "Hash", + image_rules.HashRule, + desc="Uses imagehash functions to eliminate similar looking images", + settings=ItemSettings({"resolver": TextInput(default="ignore_all").label("Conflict resolver column:")}), +) + + +def RuleList(parent=None): + return ProceduralConfigList( + StatRuleView_, + BlacklistWhitelistView_, + TotalLimitRuleView_, + ResRuleView_, + ChannelRuleView_, + HashRuleView_, + parent=parent, + ).label("Rules") diff --git a/imdataset_creator/gui/settings_inputs.py b/imdataset_creator/gui/settings_inputs.py new file mode 100644 index 0000000..3b29832 --- /dev/null +++ b/imdataset_creator/gui/settings_inputs.py @@ -0,0 +1,653 @@ +# from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import Enum +from pathlib import Path +from typing import Any, Callable + +from PySide6.QtCore import QDate, QDateTime, QRect, QSize, Qt, QTime, Signal, Slot +from PySide6.QtGui import QAction, QIcon, QMouseEvent +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QDateTimeEdit, + QDoubleSpinBox, + QFileDialog, + QFrame, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QProgressBar, + QScrollArea, + QSizePolicy, + QSpinBox, + QTextEdit, + QToolButton, + QVBoxLayout, + QWidget, +) +from rich import print as rprint + +from ..configs.keyworded import fancy_repr +from .frames import MiniCheckList, apply_tooltip + + +class SettingsItem(ABC): + widget: QWidget + + @abstractmethod + def create(self) -> list[QWidget]: + raise NotImplementedError + + @abstractmethod + def from_cfg(self, val) -> None: + raise NotImplementedError + + @abstractmethod + def get_cfg(self) -> Any: + raise NotImplementedError + + @abstractmethod + def reset(self): + raise NotImplementedError + + +class SettingsRow(QHBoxLayout): + def __init__( + self, + item: SettingsItem, + label: str | None, + tooltip: str | None, + from_config_mod: Callable | None = None, + to_config_mod: Callable | None = None, + parent: QWidget | None = None, + ): + if parent is not None: # why is this inconsistent to every other widget + super().__init__(parent) + else: + super().__init__() + self.setContentsMargins(0, 0, 0, 0) + self.setSpacing(2) + + self.label = label + self.tooltip = tooltip + self.from_config_mod = from_config_mod + self.to_config_mod = to_config_mod + self.item: SettingsItem = item + for widget in self.create_widgets(): + self.addWidget(widget) + + @abstractmethod + def create_widgets(self) -> list[QWidget]: + lst: list[QWidget] = [] + if self.label: + label = QLabel(self.label) + if self.tooltip: + apply_tooltip(label, self.tooltip) + lst.append(label) + lst.extend(self.item.create()) + return lst + + def from_cfg(self, val): + if self.from_config_mod is not None: + val = self.from_config_mod(val) + self.item.from_cfg(val) + + @abstractmethod + def get_cfg(self): + val = self.item.get_cfg() + if self.to_config_mod is not None: + val = self.to_config_mod(val) + return val + + @abstractmethod + @Slot() + def reset(self): + self.item.reset() + + +@fancy_repr +class BaseInput(ABC): + _label: str | None + _tooltip: str | None + optional: bool = False + _from_config_mod: Callable | None = None + _to_config_mod: Callable | None = None + + def __init__(self): + self._label = None + self._tooltip = None + + def create_layout(self) -> SettingsRow: + return SettingsRow( + self.get_settings(), + self._label, + self._tooltip, + self._from_config_mod, + self._to_config_mod, + ) + + @abstractmethod + def get_settings(self) -> SettingsItem: + raise NotImplementedError + + def label(self, text: str): + self._label = text + return self + + def tooltip(self, text: str): + self._tooltip = text + return self + + def set_optional(self): + self.optional = True + return self + + def from_config_modification(self, func: Callable): + self._from_config_mod = func + return self + + def to_config_modification(self, func: Callable): + self._to_config_mod = func + return self + + +class NumberInputSettings(SettingsItem): + def __init__( + self, bounds: tuple[int, int], default: float = 0, step: float = 1, slider: Qt.Orientation | None = None + ): + super().__init__() + self.bounds: tuple[int, int] = bounds + self.default: float = default + self.step: float = step + self.slider: Qt.Orientation | None = slider + + def create(self): + self.widget: QSpinBox = QSpinBox() + self.widget.setRange(*self.bounds) + self.reset() + return [self.widget] + + def from_cfg(self, val): + self.widget.setValue(val) + + def get_cfg(self): + return self.widget.value() + + @Slot() + def reset(self): + self.widget.setValue(int(self.default)) + + +class NumberInput(BaseInput): + def __init__( + self, + bounds: tuple[int, int], + default: float = 0, + step: float = 1, + slider: Qt.Orientation | None = None, + ) -> None: + super().__init__() + self.bounds: tuple[int, int] = bounds + self.default: float = default + self.step: float = step + self.slider: Qt.Orientation | None = slider + + def get_settings(self): + return NumberInputSettings(self.bounds, self.default, self.step, self.slider) + + +class DoubleInputSettings(NumberInputSettings): + def create(self): + self.widget: QDoubleSpinBox = QDoubleSpinBox() + self.widget.setRange(*self.bounds) + self.reset() + return [self.widget] + + def from_cfg(self, val): + self.widget.setValue(val) + + def get_cfg(self): + return self.widget.value() + + @Slot() + def reset(self): + self.widget.setValue(self.default) + + +class DoubleInput(NumberInput): + def get_settings(self) -> SettingsItem: + return DoubleInputSettings(self.bounds, self.default, self.step, self.slider) + + +def small_widget(parent=None): + widget = QWidget(parent) + layout = QHBoxLayout(widget) + layout.setSpacing(2) + layout.setContentsMargins(0, 0, 0, 0) + return widget + + +class RangeInputSettings(SettingsItem): + def __init__( + self, + bounds: tuple[int, int], + default: float | tuple[float, float] = 0, + step: float = 1, + min_and_max_correlate=True, + ): + super().__init__() + self.bounds: tuple[int, int] = bounds + self.default: float | tuple[float, float] = default + self.step: float = step + self.min_and_max_correlate = min_and_max_correlate + + def create(self): + self.min_widget: QSpinBox = QSpinBox() + self.max_widget: QSpinBox = QSpinBox() + if self.min_and_max_correlate: + self.min_widget.valueChanged.connect(self.max_widget.setMinimum) + self.max_widget.valueChanged.connect(self.min_widget.setMaximum) + self.min_widget.setMinimum(self.bounds[0]) + self.max_widget.setMaximum(self.bounds[1]) + + return [self.min_widget, self.max_widget] + + def from_cfg(self, val: tuple[int, int]): + self.max_widget.setValue(val[1]) + self.min_widget.setValue(val[0]) + + def get_cfg(self) -> tuple[int, int]: + return (self.min_widget.value(), self.max_widget.value()) + + @Slot() + def reset(self): + mi: float + ma: float + if isinstance(self.default, tuple): + mi, ma = self.default + else: + mi = ma = self.default + self.max_widget.setValue(int(ma)) + self.min_widget.setValue(int(mi)) + + +class RangeInput(BaseInput): + def __init__( + self, + bounds: tuple[int, int] = (0, 9_999_999), + default: float | tuple[float, float] = 0, + step: float = 1, + min_and_max_correlate=True, + ): + super().__init__() + self.bounds: tuple[int, int] = bounds + self.default: float | tuple[float, float] = default + self.step: float = step + self.min_and_max_correlate: bool = min_and_max_correlate + + def get_settings(self) -> SettingsItem: + return RangeInputSettings(self.bounds, self.default, self.step, self.min_and_max_correlate) + + +class BoolInputSettings(SettingsItem): + widget: QCheckBox + + def __init__(self, default: bool): + self.default: bool = default + + def create(self): + self.widget = QCheckBox() + self.widget.setChecked(self.default) + return [self.widget] + + def from_cfg(self, val): + self.widget.setChecked(val) + + def get_cfg(self): + return self.widget.isChecked() + + def reset(self): + self.widget.setChecked(self.default) + + +class BoolInput(BaseInput): + widget: QCheckBox + + def __init__(self, default=False): + super().__init__() + self.default: bool = default + + def get_settings(self) -> SettingsItem: + return BoolInputSettings(self.default) + + +class TextInputSettings(SettingsItem): + def __init__(self, default="", placeholder=""): + super().__init__() + self.default = default + self.placeholder = placeholder + + def create(self): + self.widget: QLineEdit = QLineEdit() + if self.default: + self.widget.setText(str(self.default)) + if self.placeholder: + self.widget.setPlaceholderText(self.placeholder) + return [self.widget] + + def from_cfg(self, val) -> None: + self.widget.setText(val) + + def get_cfg(self): + return self.widget.text() + + @Slot() + def reset(self): + self.widget.setText(self.default) + + +class TextInput(BaseInput): + def __init__(self, default="", placeholder=""): + super().__init__() + self.default = default + self.placeholder = placeholder + + def get_settings(self) -> SettingsItem: + return TextInputSettings(self.default, self.placeholder) + + +class MultilineInputSettings(SettingsItem): + def __init__(self, default="", is_list=False): + super().__init__() + self.default = default + self.is_list = is_list + + def create(self): + self.widget: QTextEdit = QTextEdit() + if self.default: + self.widget.setText(str(self.default)) + return [self.widget] + + def from_cfg(self, val) -> None: + if isinstance(val, list): + val = "\n".join(val) + self.widget.setText(val) + + def get_cfg(self): + val = self.widget.toPlainText() + if self.is_list: + return val.splitlines() + return val + + @Slot() + def reset(self): + self.widget.setText(self.default) + + +class MultilineInput(BaseInput): + widget: QTextEdit + + def __init__(self, default="", is_list=False): + super().__init__() + self.default = default + self.is_list = is_list + + def get_settings(self): + return MultilineInputSettings(self.default, self.is_list) + + +class DropdownInputSettings(SettingsItem): + def __init__(self, choices: list[str], default_idx=0): + super().__init__() + self.choices = choices + self.default_idx = default_idx + + def create(self): + self.widget: QComboBox = QComboBox() + self.widget.addItems(self.choices) + self.widget.setCurrentIndex(self.default_idx) + return [self.widget] + + def from_cfg(self, val): + self.widget.setCurrentIndex(self.choices.index(val)) + + def get_cfg(self): + return self.choices[self.widget.currentIndex()] + + def reset(self): + self.widget.setCurrentIndex(self.default_idx) + + +class DropdownInput(BaseInput): + choices: list[str] + + def __init__(self, choices: list[str], default_idx=0): + super().__init__() + self.choices = choices + self.default_idx = default_idx + + def get_settings(self) -> SettingsItem: + return DropdownInputSettings(self.choices, self.default_idx) + + +class EnumChecklistInputSettings(SettingsItem): + def __init__(self, enum: type[Enum]): + super().__init__() + self.enum: type[Enum] = enum + + def create(self): + self.widget: MiniCheckList = MiniCheckList(self.enum.__members__.keys()) + return [self.widget] + + def from_cfg(self, val) -> None: + newval: dict[str, bool] = {k: True for k in val} + self.widget.update_items(newval) + + def get_cfg(self): + return self.widget.get_enabled() + + def reset(self): + self.widget.disable_all() + + +class EnumChecklistInput(BaseInput): + checklist: MiniCheckList + + def __init__(self, enum: type[Enum]): + super().__init__() + self.enum: type[Enum] = enum + + def get_settings(self): + return EnumChecklistInputSettings(self.enum) + + +StartOfDateTime = QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0)) + + +class DateTimeInputSettings(SettingsItem): + def __init__( + self, + dt_format: str = "dd/MM/yyyy h:mm AP", + default: QDateTime = StartOfDateTime, + calendar_popup=False, + ): + super().__init__() + self.format = dt_format + self.default = default + self.calendar_popup = calendar_popup + + def create(self): + self.widget: QDateTimeEdit = QDateTimeEdit() + self.widget.setCalendarPopup(self.calendar_popup) + if self.default: + self.widget.setDateTime(self.default) + self.widget.setDisplayFormat(self.format) + return [self.widget] + + def from_cfg(self, val) -> None: + self.widget.setDateTime(QDateTime.fromString(val, self.format)) + + def get_cfg(self) -> Any: + return self.widget.dateTime().toString(self.format) + + def reset(self): + self.widget.setDateTime(self.default) + + +class DateTimeInput(BaseInput): + widget: QDateTimeEdit + + def __init__( + self, + dt_format: str = "dd/MM/yyyy h:mm AP", + default: QDateTime = StartOfDateTime, + calendar_popup=True, + ): + super().__init__() + self.format = dt_format + self.default = default + self.calendar_popup = calendar_popup + + def get_settings(self): + return DateTimeInputSettings(self.format, self.default, self.calendar_popup) + + +class DirectoryInputSettings(SettingsItem): + def __init__(self, default: str = ""): + super().__init__() + self.default = default + + def create(self): + self.text_widget: QLineEdit = QLineEdit() + if self.default: + self.text_widget.setText(self.default) + self.folder_select = QToolButton() + self.folder_select.setText("...") + self.folder_select.setIcon(QIcon.fromTheme("folder-open")) + self.folder_select.clicked.connect(self.select_folder) + self.filedialog = QFileDialog() + self.filedialog.setFileMode(QFileDialog.FileMode.Directory) + self.filedialog.setOption(QFileDialog.Option.ShowDirsOnly, True) + self.filedialog.setOption(QFileDialog.Option.DontResolveSymlinks, True) + + return [self.text_widget, self.folder_select] + + @Slot() + def select_folder(self): + # ! this as a whole is very fucky + + files = self._select_folder() + if files: + while len(files) > 1: + self.text_widget.setText(files.pop(0)) + # self.duplicate.emit() + self.text_widget.setText(files.pop(0)) + + def _select_folder(self): + self.filedialog.setDirectory(self.text_widget.text() or str(Path.home())) + if self.filedialog.exec(): + return self.filedialog.selectedFiles() + return [] + + def from_cfg(self, val): + self.text_widget.setText(val) + + def get_cfg(self): + return self.text_widget.text() + + def reset(self): + self.text_widget.setText(self.default) + + +class DirectoryInput(BaseInput): + def __init__(self, default: str = ""): + super().__init__() + self.default = default + + def get_settings(self): + return DirectoryInputSettings(self.default) + + +ItemSettings = dict[str | tuple[str, ...], BaseInput] + + +class SettingsBox(QFrame): + def __init__(self, settings: ItemSettings, parent=None): + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(2) + layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetMinimumSize) + self.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken) + self.settings = settings + self.rows: dict[str | tuple[str, ...], SettingsRow] = {} + + for key, inpt in settings.items(): + box = QWidget(self) + self.rows[key] = (sl := inpt.create_layout()) + box.setLayout(sl) + layout.addWidget(box) + + def get_cfg(self): + dct = {} + for key, row in self.rows.items(): + val = row.get_cfg() + if isinstance(key, tuple): + dct.update(dict(zip(key, val))) + else: + dct[key] = val + + return dct + + def from_cfg(self, cfg: dict): + for key, row in self.rows.items(): + if isinstance(key, tuple): + vals = [cfg[k] for k in key] + row.from_cfg(vals) + else: + if key in cfg: + row.from_cfg(cfg[key]) + + def mouseMoveEvent(self, event: QMouseEvent) -> None: + if event.buttons() == Qt.MouseButton.LeftButton and self.previous_position is not None: + pos_change = event.position() - self.previous_position + new_height = int(self.size().height() + pos_change.y()) + self.setMinimumHeight(max(new_height, 0)) + self.previous_position = event.position() + return super().mouseMoveEvent(event) + + def mousePressEvent(self, event: QMouseEvent) -> None: + self.previous_position = event.position() + return super().mousePressEvent(event) + + +# @fancy_repr +# class ItemSettings(dict[str | tuple[str, ...], BaseSettingsInput]): +# def from_cfg(self, cfg: dict): +# for key, setting in self.items(): +# if isinstance(key, tuple): +# vals = [cfg[k] for k in key] # sort based on key order +# setting.from_cfg(vals) +# else: +# if key in cfg: +# setting.from_cfg(cfg[key]) + +# def get_cfg(self) -> dict: +# dct = {} +# for key, setting in self.items(): +# val = setting.get_cfg() + +# if isinstance(key, tuple): +# for k, v in zip(key, val): +# dct[k] = v +# else: +# dct[key] = val +# return dct + +# def get_widgets(self) -> list[list[QWidget]]: +# return [setting.create_widgets() for setting in self.values()] diff --git a/imdataset_creator/image_filters/destroyers.py b/imdataset_creator/image_filters/destroyers.py index a364ca5..1ecd541 100644 --- a/imdataset_creator/image_filters/destroyers.py +++ b/imdataset_creator/image_filters/destroyers.py @@ -58,11 +58,16 @@ def run( ksize = ksize + (ksize % 2 == 0) # ensure ksize is odd return cv2.GaussianBlur(img, (ksize, ksize), 0) - if algorithm == BlurAlgorithm.ISOTROPIC or algorithm == BlurAlgorithm.ANISOTROPIC: + if ( + algorithm == BlurAlgorithm.ISOTROPIC + or algorithm == BlurAlgorithm.ANISOTROPIC + ): sigma1: float = ri * self.scale ksize1: int = 2 * int(4 * sigma1 + 0.5) + 1 if algorithm == BlurAlgorithm.ANISOTROPIC: - return cv2.GaussianBlur(img, (ksize1, ksize1), sigmaX=sigma1, sigmaY=sigma1) + return cv2.GaussianBlur( + img, (ksize1, ksize1), sigmaX=sigma1, sigmaY=sigma1 + ) sigma2: float = ri * self.scale ksize2: int = 2 * int(4 * sigma2 + 0.5) + 1 @@ -114,7 +119,11 @@ def run(self, img: ndarray) -> ndarray: if algorithm == NoiseAlgorithm.COLOR: noise = np.zeros_like(img) - s = (randint(*self.intensity_range), randint(*self.intensity_range), randint(*self.intensity_range)) + s = ( + randint(*self.intensity_range), + randint(*self.intensity_range), + randint(*self.intensity_range), + ) cv2.randn(noise, 0, s) # type: ignore return img + noise @@ -170,12 +179,16 @@ def run(self, img: ndarray): enc_img: ndarray if algorithm == CompressionAlgorithms.JPEG: quality = randint(*self.jpeg_quality_range) - enc_img = cv2.imencode(".jpg", img, [int(cv2.IMWRITE_JPEG_QUALITY), quality])[1] + enc_img = cv2.imencode( + ".jpg", img, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + )[1] return cv2.imdecode(enc_img, 1) if algorithm == CompressionAlgorithms.WEBP: quality = randint(*self.webp_quality_range) - enc_img = cv2.imencode(".webp", img, [int(cv2.IMWRITE_WEBP_QUALITY), quality])[1] + enc_img = cv2.imencode( + ".webp", img, [int(cv2.IMWRITE_WEBP_QUALITY), quality] + )[1] return cv2.imdecode(enc_img, 1) if algorithm in [ @@ -205,7 +218,9 @@ def run(self, img: ndarray): codec = "mpeg2video" compressor = ( - ffmpeg.input("pipe:", format="rawvideo", pix_fmt="bgr24", s=f"{width}x{height}") + ffmpeg.input( + "pipe:", format="rawvideo", pix_fmt="bgr24", s=f"{width}x{height}" + ) .output("pipe:", format=container, vcodec=codec, **output_args) .global_args("-loglevel", "error") .global_args("-max_muxing_queue_size", "300000") @@ -226,7 +241,9 @@ def run(self, img: ndarray): newimg = np.frombuffer(out, np.uint8) if len(newimg) != height * width * 3: log.warning("New image size does not match") - newimg = newimg[: height * width * 3] # idrk why i need this sometimes + newimg = newimg[ + : height * width * 3 + ] # idrk why i need this sometimes return newimg.reshape((height, width, 3)) except subprocess.TimeoutExpired as e: diff --git a/imdataset_creator/image_filters/resizer.py b/imdataset_creator/image_filters/resizer.py index 860d157..47851b5 100644 --- a/imdataset_creator/image_filters/resizer.py +++ b/imdataset_creator/image_filters/resizer.py @@ -6,7 +6,7 @@ import cv2 import numpy as np -from ..configs.configtypes import FilterData +from ..configs.configtypes import FilterData, SpecialItemData from ..datarules import Filter from ..enum_helpers import listostr2listoenum @@ -31,6 +31,13 @@ class ResizeMode(Enum): MIN_RESOLUTION = 2 +class ResizeData(SpecialItemData): + mode: str + algorithms: list[str] + down_up_range: list[float] + scale: float + + @dataclass(frozen=True) class Resize(Filter): mode: ResizeMode = ResizeMode.MIN_RESOLUTION @@ -149,5 +156,7 @@ def run(self, img: np.ndarray) -> np.ndarray: def from_cfg(cls, cfg: RandomRotateData): return cls( rotate_chance=cfg["rotate_chance"], - rotate_directions=[RandomRotateDirections[d] for d in cfg["rotate_directions"]], + rotate_directions=[ + RandomRotateDirections[d] for d in cfg["rotate_directions"] + ], ) diff --git a/poetry.lock b/poetry.lock index f1a0230..89d0817 100644 --- a/poetry.lock +++ b/poetry.lock @@ -259,17 +259,17 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "polars" -version = "0.19.9" +version = "0.19.11" description = "Blazingly fast DataFrame library" optional = false python-versions = ">=3.8" files = [ - {file = "polars-0.19.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:733525ad608b8668de3749aa21d1c61bd96944b1d88895765b34d8340421a1d2"}, - {file = "polars-0.19.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:5091fe78dbf82a2e30380cd6e73a3411b65a1ea9dd55afd07065944534fa5246"}, - {file = "polars-0.19.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b3fc4b2ed02efa29855c8f4dbc26d2e45d2c1a586f6ae692489d3e7786abb27"}, - {file = "polars-0.19.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02314810dbdd0821645e7ce05a2295102992a229d46938c7f520ccde68625cc9"}, - {file = "polars-0.19.9-cp38-abi3-win_amd64.whl", hash = "sha256:3c99bbe5ee2a2938fe750c8df429c42967d47e302b2b4b6dcb9b4267c8d42510"}, - {file = "polars-0.19.9.tar.gz", hash = "sha256:4936b563c609c9def81c2a85ab856ee4b162d2d87566858671fd9f5cba49aa46"}, + {file = "polars-0.19.11-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:de8158e5f09346ec4622057b7afa7e5339eed61c3c3e874b469c9cb27339df51"}, + {file = "polars-0.19.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c76c2107260a1ca8a57f02d77ea12dc4db2090d7404b814570474db0392ecf6b"}, + {file = "polars-0.19.11-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c6cf2aa2d301230a80277f8711646453b89eadd6058baf30b7104f420daad2"}, + {file = "polars-0.19.11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac2890c96736dee83335b1f0b403233aba18b86760505e81eb9f96112afc55d"}, + {file = "polars-0.19.11-cp38-abi3-win_amd64.whl", hash = "sha256:95be83cb0bbd2d608849e24a973ea3135bd25ae6ce7168e31ad25a02e7773122"}, + {file = "polars-0.19.11.tar.gz", hash = "sha256:156eab31d9f9bac218bbd391559c667848372a5c584472784695e4fac087fd5b"}, ] [package.extras] @@ -548,4 +548,4 @@ bracex = ">=2.1.1" [metadata] lock-version = "2.0" python-versions = "^3.10, <3.12" -content-hash = "7770af11b3a45085323e89ba44cb93350672a1d8c753831ffb2269b8cf6ac9f6" +content-hash = "94ab020b39fe7a2782debba9c1945c5d2b636ee38dc73fa0769b2bb7a9b4ff47" diff --git a/pyproject.toml b/pyproject.toml index aa918c3..a641257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ PySide6-Essentials = "^6.5.2" ffmpeg-python = "^0.2.0" imagesize = "^1.4.1" numpy = "^1.26.0" -opencv-python = "^4.8.0.76" polars = "^0.19.3" pyarrow = "^13.0.0" python = "^3.10, <3.12" @@ -23,6 +22,7 @@ python-dateutil = "^2.8.2" rich = "^13.5.3" typer = "^0.9.0" wcmatch = "^8.5" +opencv-python = "^4.8.1.78" [tool.poetry.scripts] imdataset-creator = "imdataset_creator.__main__:app" @@ -55,3 +55,7 @@ line-length = 120 [tool.ruff.per-file-ignores] "__init__.py" = ["E402"] + + +[tool.isort] +profile = "black"