diff --git a/app/lang/de.toml b/app/lang/de.toml index 6d692fa..54179e4 100644 --- a/app/lang/de.toml +++ b/app/lang/de.toml @@ -59,3 +59,4 @@ "dialog.no_image_loaded" = "Kein Bild geladen." "dialog.no_preview_available" = "Keine Preview vorhanden." "dialog.overlay_saved" = "Overlay gespeichert: {path}" +"status.drag_drop" = "Bild oder Ordner hier ablegen." diff --git a/app/lang/en.toml b/app/lang/en.toml index 3eec912..1857249 100644 --- a/app/lang/en.toml +++ b/app/lang/en.toml @@ -59,3 +59,4 @@ "dialog.no_image_loaded" = "No image loaded." "dialog.no_preview_available" = "No preview available." "dialog.overlay_saved" = "Overlay saved: {path}" +"status.drag_drop" = "Drop an image or folder here to open it." diff --git a/app/logic/constants.py b/app/logic/constants.py index 34c5076..4ef98fd 100644 --- a/app/logic/constants.py +++ b/app/logic/constants.py @@ -108,8 +108,6 @@ def _extract_options(data: dict[str, Any]) -> dict[str, Any]: return result -_CONFIG_DATA = _load_config_data() - DEFAULTS = {**_DEFAULTS_BASE, **_extract_default_overrides(_CONFIG_DATA)} LANGUAGE = _extract_language(_CONFIG_DATA) OPTIONS = {**_OPTION_DEFAULTS, **_extract_options(_CONFIG_DATA)} diff --git a/app/qt/image_processor.py b/app/qt/image_processor.py index c2b75b5..d89219a 100644 --- a/app/qt/image_processor.py +++ b/app/qt/image_processor.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Dict, Iterable, Tuple +import numpy as np from PIL import Image, ImageDraw from PySide6 import QtGui @@ -38,6 +39,36 @@ class Stats: ) +def _rgb_to_hsv_numpy(arr: np.ndarray) -> np.ndarray: + """Vectorized RGB→HSV conversion. arr shape: (H, W, 3), dtype float32, range [0,1]. + Returns array of same shape with channels [H(0-360), S(0-100), V(0-100)]. + """ + r = arr[..., 0] + g = arr[..., 1] + b = arr[..., 2] + + cmax = np.maximum(np.maximum(r, g), b) + cmin = np.minimum(np.minimum(r, g), b) + delta = cmax - cmin + + # Value + v = cmax + + # Saturation + s = np.where(cmax > 0, delta / cmax, 0.0) + + # Hue + h = np.zeros_like(r) + mask_r = (delta > 0) & (cmax == r) + mask_g = (delta > 0) & (cmax == g) + mask_b = (delta > 0) & (cmax == b) + h[mask_r] = (60.0 * ((g[mask_r] - b[mask_r]) / delta[mask_r])) % 360.0 + h[mask_g] = (60.0 * ((b[mask_g] - r[mask_g]) / delta[mask_g]) + 120.0) % 360.0 + h[mask_b] = (60.0 * ((r[mask_b] - g[mask_b]) / delta[mask_b]) + 240.0) % 360.0 + + return np.stack([h, s * 100.0, v * 100.0], axis=-1) + + class QtImageProcessor: """Process images and build overlays for the Qt UI.""" @@ -132,44 +163,59 @@ class QtImageProcessor: self.preview_img = self.orig_img.resize(size, Image.LANCZOS) def _rebuild_overlay(self) -> None: + """Build colour-match overlay using vectorized NumPy operations.""" if self.preview_img is None: self.overlay_img = None self.stats = Stats() return + base = self.preview_img.convert("RGBA") - overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) - draw = ImageDraw.Draw(overlay) - pixels = base.load() - width, height = base.size - highlight = (255, 0, 0, int(self.alpha)) - matches_all = total_all = 0 - matches_keep = total_keep = 0 - matches_excl = total_excl = 0 + arr = np.asarray(base, dtype=np.float32) # (H, W, 4) - mask = self._build_exclusion_mask(base.size) - mask_px = mask.load() if mask else None + rgb = arr[..., :3] / 255.0 + alpha_ch = arr[..., 3] # alpha channel of the image - for y in range(height): - for x in range(width): - r, g, b, a = pixels[x, y] - if a == 0: - continue - match = self._matches(r, g, b) - excluded = bool(mask_px and mask_px[x, y]) - total_all += 1 - if excluded: - total_excl += 1 - if match: - matches_excl += 1 - else: - total_keep += 1 - if match: - draw.point((x, y), fill=highlight) - matches_keep += 1 - if match: - matches_all += 1 + hsv = _rgb_to_hsv_numpy(rgb) # (H, W, 3): H°, S%, V% - self.overlay_img = overlay + hue = hsv[..., 0] + sat = hsv[..., 1] + val = hsv[..., 2] + + hue_min = float(self.hue_min) + hue_max = float(self.hue_max) + if hue_min <= hue_max: + hue_ok = (hue >= hue_min) & (hue <= hue_max) + else: + hue_ok = (hue >= hue_min) | (hue <= hue_max) + + match_mask = ( + hue_ok + & (sat >= float(self.sat_min)) + & (val >= float(self.val_min)) + & (val <= float(self.val_max)) + & (alpha_ch > 0) + ) + + # Exclusion mask (same pixel space as preview) + excl_mask = self._build_exclusion_mask_numpy(base.size) # bool (H,W) + + keep_match = match_mask & ~excl_mask + excl_match = match_mask & excl_mask + visible = alpha_ch > 0 + + matches_all = int(match_mask[visible].sum()) + total_all = int(visible.sum()) + matches_keep = int(keep_match[visible].sum()) + total_keep = int((visible & ~excl_mask).sum()) + matches_excl = int(excl_match[visible].sum()) + total_excl = int((visible & excl_mask).sum()) + + # Build overlay image + overlay_arr = np.zeros((base.height, base.width, 4), dtype=np.uint8) + overlay_arr[keep_match, 0] = 255 + overlay_arr[keep_match, 3] = int(self.alpha) + + self.overlay_img = Image.fromarray(overlay_arr, "RGBA") self.stats = Stats( matches_all=matches_all, total_all=total_all, @@ -182,6 +228,7 @@ class QtImageProcessor: # helpers ---------------------------------------------------------------- def _matches(self, r: int, g: int, b: int) -> bool: + """Single-pixel match — kept for compatibility / eyedropper use.""" h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) hue = (h * 360.0) % 360.0 if self.hue_min <= self.hue_max: @@ -192,6 +239,20 @@ class QtImageProcessor: val_ok = self.val_min <= v * 100.0 <= self.val_max return hue_ok and sat_ok and val_ok + def pick_colour(self, x: int, y: int) -> Tuple[float, float, float] | None: + """Return (hue°, sat%, val%) of the preview pixel at (x, y), or None.""" + if self.preview_img is None: + return None + img = self.preview_img.convert("RGBA") + try: + r, g, b, a = img.getpixel((x, y)) + except IndexError: + return None + if a == 0: + return None + h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) + return (h * 360.0) % 360.0, s * 100.0, v * 100.0 + # exported data ---------------------------------------------------------- def preview_pixmap(self) -> QtGui.QPixmap: @@ -241,3 +302,13 @@ class QtImageProcessor: if len(points) >= 3: draw.polygon(points, fill=255) return mask + + def _build_exclusion_mask_numpy(self, size: Tuple[int, int]) -> np.ndarray: + """Return a boolean (H, W) mask — True where pixels are excluded.""" + w, h = size + if not self.exclude_shapes: + return np.zeros((h, w), dtype=bool) + pil_mask = self._build_exclusion_mask(size) + if pil_mask is None: + return np.zeros((h, w), dtype=bool) + return np.asarray(pil_mask, dtype=bool) diff --git a/app/qt/main_window.py b/app/qt/main_window.py index 8eccee9..0c6129f 100644 --- a/app/qt/main_window.py +++ b/app/qt/main_window.py @@ -75,24 +75,6 @@ class ToolbarButton(QtWidgets.QPushButton): metrics = QtGui.QFontMetrics(self.font()) width = metrics.horizontalAdvance(text) + 28 self.setMinimumWidth(width) - self.setStyleSheet( - """ - QPushButton { - padding: 8px 16px; - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.08); - background-color: rgba(255, 255, 255, 0.04); - color: #f7f7fb; - font-weight: 600; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 0.12); - } - QPushButton:pressed { - background-color: rgba(255, 255, 255, 0.18); - } - """ - ) self.clicked.connect(callback) def apply_theme(self, colours: Dict[str, str]) -> None: @@ -159,13 +141,15 @@ class ColourSwatch(QtWidgets.QPushButton): class SliderControl(QtWidgets.QWidget): - """Slider with header and live value label.""" + """Slider with header and editable live value input.""" value_changed = QtCore.Signal(str, int) def __init__(self, title: str, key: str, minimum: int, maximum: int, initial: int): super().__init__() self.key = key + self._minimum = minimum + self._maximum = maximum layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -176,47 +160,46 @@ class SliderControl(QtWidgets.QWidget): self.title_label = QtWidgets.QLabel(title) header.addWidget(self.title_label) header.addStretch(1) - self.value_label = QtWidgets.QLabel(str(initial)) - header.addWidget(self.value_label) + self.value_edit = QtWidgets.QLineEdit(str(initial)) + self.value_edit.setFixedWidth(44) + self.value_edit.setAlignment(QtCore.Qt.AlignRight) + self.value_edit.setValidator(QtGui.QIntValidator(minimum, maximum, self)) + self.value_edit.editingFinished.connect(self._commit_edit) + header.addWidget(self.value_edit) layout.addLayout(header) self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(minimum, maximum) self.slider.setValue(initial) self.slider.setCursor(QtCore.Qt.PointingHandCursor) - self.slider.setStyleSheet( - """ - QSlider::groove:horizontal { - border: 1px solid rgba(255,255,255,0.08); - height: 6px; - background: rgba(255,255,255,0.1); - border-radius: 4px; - } - QSlider::handle:horizontal { - background: #9a4dff; - border: 1px solid rgba(255,255,255,0.2); - width: 14px; - margin: -5px 0; - border-radius: 7px; - } - """ - ) self.slider.valueChanged.connect(self._sync_value) layout.addWidget(self.slider) def _sync_value(self, value: int) -> None: - self.value_label.setText(str(value)) + self.value_edit.setText(str(value)) self.value_changed.emit(self.key, value) + def _commit_edit(self) -> None: + text = self.value_edit.text().strip() + try: + value = int(text) + except ValueError: + value = self.slider.value() + value = max(self._minimum, min(self._maximum, value)) + self.slider.setValue(value) # triggers _sync_value -> signal + def set_value(self, value: int) -> None: self.slider.blockSignals(True) self.slider.setValue(value) self.slider.blockSignals(False) - self.value_label.setText(str(value)) + self.value_edit.setText(str(value)) def apply_theme(self, colours: Dict[str, str]) -> None: self.title_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;") - self.value_label.setStyleSheet(f"color: {colours['text_dim']};") + self.value_edit.setStyleSheet( + f"color: {colours['text_dim']}; background: transparent; " + f"border: 1px solid {colours['border']}; border-radius: 4px; padding: 0 2px;" + ) self.slider.setStyleSheet( f""" QSlider::groove:horizontal {{ @@ -240,6 +223,7 @@ class CanvasView(QtWidgets.QGraphicsView): """Interactive canvas for drawing exclusion shapes over the preview image.""" shapes_changed = QtCore.Signal(list) + pixel_clicked = QtCore.Signal(int, int) # x, y in image coordinates def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent) @@ -257,6 +241,7 @@ class CanvasView(QtWidgets.QGraphicsView): self.shapes: list[dict[str, object]] = [] self.mode: str = "rect" + self.pick_mode: bool = False self._drawing = False self._start_pos = QtCore.QPointF() self._last_pos = QtCore.QPointF() @@ -305,6 +290,12 @@ class CanvasView(QtWidgets.QGraphicsView): # event handling -------------------------------------------------------- def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: + if event.button() == QtCore.Qt.LeftButton and self.pick_mode and self._pixmap_item: + scene_pos = self.mapToScene(event.position().toPoint()) + clamped = self._clamp_to_image(scene_pos) + self.pixel_clicked.emit(int(clamped.x()), int(clamped.y())) + event.accept() + return if event.button() == QtCore.Qt.RightButton and self._pixmap_item: self._drawing = True scene_pos = self.mapToScene(event.position().toPoint()) @@ -407,6 +398,35 @@ class CanvasView(QtWidgets.QGraphicsView): self._shape_items.append(path_item) +class OverlayCanvas(QtWidgets.QGraphicsView): + """Read-only QGraphicsView for displaying the colour-match overlay.""" + + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: + super().__init__(parent) + self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._scene = QtWidgets.QGraphicsScene(self) + self.setScene(self._scene) + self._pixmap_item: QtWidgets.QGraphicsPixmapItem | None = None + + def set_pixmap(self, pixmap: QtGui.QPixmap) -> None: + self._scene.clear() + self._pixmap_item = self._scene.addPixmap(pixmap) + self._scene.setSceneRect(pixmap.rect()) + self.resetTransform() + self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio) + + def clear_canvas(self) -> None: + self._scene.clear() + self._pixmap_item = None + + def resizeEvent(self, event: QtGui.QResizeEvent) -> None: # type: ignore[override] + super().resizeEvent(event) + if self._pixmap_item: + self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio) + + class TitleBar(QtWidgets.QWidget): """Custom title bar with native window controls.""" @@ -561,8 +581,10 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): self._register_default_actions() self.exclude_mode = "rect" + self._pick_mode = False self.image_view.set_mode(self.exclude_mode) self.image_view.shapes_changed.connect(self._on_shapes_changed) + self.image_view.pixel_clicked.connect(self._on_pixel_picked) self._sync_sliders_from_processor() self._update_colour_display(DEFAULT_COLOUR, self._t("palette.current")) @@ -570,6 +592,18 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): self.current_theme = "dark" self._apply_theme(self.current_theme) + # Drag-and-drop + self.setAcceptDrops(True) + + # Keyboard shortcuts + self._setup_shortcuts() + + # Restore window geometry + self._settings = QtCore.QSettings("ICRA", "MainWindow") + geometry = self._settings.value("geometry") + if geometry: + self.restoreGeometry(geometry) + # Window control helpers ------------------------------------------------- def toggle_maximise(self) -> None: @@ -684,9 +718,7 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): self.image_view = CanvasView() layout.addWidget(self.image_view, 0, 1) - self.overlay_view = QtWidgets.QLabel("") - self.overlay_view.setAlignment(QtCore.Qt.AlignCenter) - self.overlay_view.setScaledContents(True) + self.overlay_view = OverlayCanvas() layout.addWidget(self.overlay_view, 0, 2) self.next_button = QtWidgets.QToolButton() @@ -723,7 +755,7 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): "open_image": self.open_image, "open_folder": self.open_folder, "choose_color": self.choose_colour, - "pick_from_image": self._coming_soon, + "pick_from_image": self.pick_from_image, "save_overlay": self.save_overlay, "toggle_free_draw": self.toggle_free_draw, "clear_excludes": self.clear_exclusions, @@ -835,6 +867,127 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): "Feature coming soon in the PySide6 migration.", ) + # Shortcuts -------------------------------------------------------------- + + def _setup_shortcuts(self) -> None: + shortcuts = [ + (QtGui.QKeySequence.Open, self.open_image), + (QtGui.QKeySequence("Ctrl+Shift+O"), self.open_folder), + (QtGui.QKeySequence.Save, self.save_overlay), + (QtGui.QKeySequence.Undo, self.undo_exclusion), + (QtGui.QKeySequence("Ctrl+R"), self._reset_sliders), + (QtGui.QKeySequence(QtCore.Qt.Key_Left), self.show_previous_image), + (QtGui.QKeySequence(QtCore.Qt.Key_Right), self.show_next_image), + (QtGui.QKeySequence(QtCore.Qt.Key_Escape), self._exit_pick_mode), + ] + for seq, slot in shortcuts: + sc = QtGui.QShortcut(seq, self) + sc.activated.connect(slot) + + # Pick-mode / eyedropper ------------------------------------------------- + + def pick_from_image(self) -> None: + if self.processor.preview_img is None: + QtWidgets.QMessageBox.information( + self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded") + ) + return + self._pick_mode = True + self.image_view.pick_mode = True + self.image_view.setCursor(QtCore.Qt.CrossCursor) + self.status_label.setText(self._t("status.pick_mode_ready")) + + def _exit_pick_mode(self) -> None: + self._pick_mode = False + self.image_view.pick_mode = False + self.image_view.setCursor(QtCore.Qt.ArrowCursor) + self.status_label.setText(self._t("status.pick_mode_ended")) + + def _on_pixel_picked(self, x: int, y: int) -> None: + result = self.processor.pick_colour(x, y) + if result is None: + self._exit_pick_mode() + return + hue, sat, val = result + + # Derive a narrow hue range (±15°) and minimum sat/val from the pixel + margin = 15 + hue_min = max(0, int(hue) - margin) + hue_max = min(360, int(hue) + margin) + sat_min = max(0, int(sat) - 20) + val_min = max(0, int(val) - 30) + val_max = 100 + + for attr, value in [ + ("hue_min", hue_min), ("hue_max", hue_max), + ("sat_min", sat_min), ("val_min", val_min), ("val_max", val_max), + ]: + ctrl = self._slider_controls.get(attr) + if ctrl: + ctrl.set_value(value) + self.processor.set_threshold(attr, value) + + # Update colour swatch to the picked pixel colour + h_norm = hue / 360.0 + s_norm = sat / 100.0 + v_norm = val / 100.0 + import colorsys + r, g, b = colorsys.hsv_to_rgb(h_norm, s_norm, v_norm) + hex_code = "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255)) + self._update_colour_display(hex_code, "") + + self.status_label.setText( + self._t("status.pick_mode_from_image", hue=hue, saturation=sat, value=val) + ) + self._exit_pick_mode() + self._refresh_overlay_only() + + # Drag-and-drop ---------------------------------------------------------- + + def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: + if event.mimeData().hasUrls(): + event.acceptProposedAction() + + def dropEvent(self, event: QtGui.QDropEvent) -> None: + urls = event.mimeData().urls() + if not urls: + return + path = Path(urls[0].toLocalFile()) + if path.is_dir(): + paths = sorted( + (p for p in path.iterdir() if p.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and p.is_file()), + key=lambda p: p.name.lower(), + ) + if not paths: + QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.folder_empty")) + return + try: + loaded_path = self.processor.load_folder(paths) + except ValueError as exc: + QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), str(exc)) + return + self._current_image_path = loaded_path + elif path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS: + try: + loaded_path = self.processor.load_single_image(path) + except Exception as exc: + QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(exc)) + return + self._current_image_path = loaded_path + if self.processor.reset_exclusions_on_switch: + self.image_view.clear_shapes() + self.processor.set_exclusions([]) + else: + return + self._refresh_views() + event.acceptProposedAction() + + # Window lifecycle ------------------------------------------------------- + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + self._settings.setValue("geometry", self.saveGeometry()) + super().closeEvent(event) + def choose_colour(self) -> None: colour = QtWidgets.QColorDialog.getColor(parent=self, title=self._t("dialog.choose_colour_title")) if not colour.isValid(): @@ -888,7 +1041,7 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): ) self.image_view.set_accent(colours["highlight"]) self.overlay_view.setStyleSheet( - f"background-color: {colours['panel_bg']}; border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};" + f"background-color: {colours['panel_bg']}; border: 1px solid {colours['border']}; border-radius: 12px;" ) self.status_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;") @@ -938,18 +1091,16 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): def _refresh_views(self) -> None: preview_pix = self.processor.preview_pixmap() - overlay_pix = self.processor.overlay_pixmap() if preview_pix.isNull(): self.image_view.clear_canvas() self.image_view.set_shapes([]) - self.overlay_view.setText("") - self.overlay_view.setPixmap(QtGui.QPixmap()) + self.overlay_view.clear_canvas() else: self.image_view.set_pixmap(preview_pix) self.image_view.set_shapes(self.processor.exclude_shapes.copy()) - self.overlay_view.setText("") - overlay_pix = self._overlay_with_outlines(overlay_pix) - self.overlay_view.setPixmap(overlay_pix) + overlay_pix = self.processor.overlay_pixmap() + overlay_pix = self._overlay_with_outlines(overlay_pix) + self.overlay_view.set_pixmap(overlay_pix) if self._current_image_path and self.processor.preview_img: width, height = self.processor.preview_img.size total = len(self.processor.preview_paths) @@ -968,11 +1119,9 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin): return pix = self.processor.overlay_pixmap() if pix.isNull(): - self.overlay_view.setText("") - self.overlay_view.setPixmap(QtGui.QPixmap()) + self.overlay_view.clear_canvas() else: - self.overlay_view.setText("") - self.overlay_view.setPixmap(self._overlay_with_outlines(pix)) + self.overlay_view.set_pixmap(self._overlay_with_outlines(pix)) self.ratio_label.setText(self.processor.stats.summary(self._t)) def _on_shapes_changed(self, shapes: list[dict[str, object]]) -> None: diff --git a/images/ingame/271.jpg b/images/ingame/271.jpg new file mode 100644 index 0000000..facb12f Binary files /dev/null and b/images/ingame/271.jpg differ diff --git a/images/ingame/296.jpg b/images/ingame/296.jpg new file mode 100644 index 0000000..a11b96e Binary files /dev/null and b/images/ingame/296.jpg differ diff --git a/images/ingame/328.jpg b/images/ingame/328.jpg new file mode 100644 index 0000000..44f4173 Binary files /dev/null and b/images/ingame/328.jpg differ diff --git a/images/ingame/460.jpg b/images/ingame/460.jpg new file mode 100644 index 0000000..9b94004 Binary files /dev/null and b/images/ingame/460.jpg differ diff --git a/images/ingame/487.jpg b/images/ingame/487.jpg new file mode 100644 index 0000000..0d20a35 Binary files /dev/null and b/images/ingame/487.jpg differ diff --git a/images/ingame/552.jpg b/images/ingame/552.jpg new file mode 100644 index 0000000..a076fb0 Binary files /dev/null and b/images/ingame/552.jpg differ diff --git a/images/ingame/572.jpg b/images/ingame/572.jpg new file mode 100644 index 0000000..2ae589b Binary files /dev/null and b/images/ingame/572.jpg differ diff --git a/images/ingame/583.jpg b/images/ingame/583.jpg new file mode 100644 index 0000000..5137977 Binary files /dev/null and b/images/ingame/583.jpg differ diff --git a/images/ingame/654.jpg b/images/ingame/654.jpg new file mode 100644 index 0000000..38bbfba Binary files /dev/null and b/images/ingame/654.jpg differ diff --git a/images/ingame/696.jpg b/images/ingame/696.jpg new file mode 100644 index 0000000..c94d636 Binary files /dev/null and b/images/ingame/696.jpg differ diff --git a/images/ingame/70.jpg b/images/ingame/70.jpg new file mode 100644 index 0000000..4de06f4 Binary files /dev/null and b/images/ingame/70.jpg differ diff --git a/images/ingame/705.jpg b/images/ingame/705.jpg new file mode 100644 index 0000000..cdd2087 Binary files /dev/null and b/images/ingame/705.jpg differ diff --git a/images/ingame/83.jpg b/images/ingame/83.jpg new file mode 100644 index 0000000..0b0500c Binary files /dev/null and b/images/ingame/83.jpg differ diff --git a/images/ingame/858.jpg b/images/ingame/858.jpg new file mode 100644 index 0000000..edfeef1 Binary files /dev/null and b/images/ingame/858.jpg differ diff --git a/images/ingame/86.jpg b/images/ingame/86.jpg new file mode 100644 index 0000000..581dcbf Binary files /dev/null and b/images/ingame/86.jpg differ diff --git a/images/ingame/862.jpg b/images/ingame/862.jpg new file mode 100644 index 0000000..7c86960 Binary files /dev/null and b/images/ingame/862.jpg differ diff --git a/images/ingame/911.jpg b/images/ingame/911.jpg new file mode 100644 index 0000000..63ae397 Binary files /dev/null and b/images/ingame/911.jpg differ diff --git a/images/271.webp b/images/render/271.webp similarity index 100% rename from images/271.webp rename to images/render/271.webp diff --git a/images/296.webp b/images/render/296.webp similarity index 100% rename from images/296.webp rename to images/render/296.webp diff --git a/images/328.webp b/images/render/328.webp similarity index 100% rename from images/328.webp rename to images/render/328.webp diff --git a/images/460.webp b/images/render/460.webp similarity index 100% rename from images/460.webp rename to images/render/460.webp diff --git a/images/487.webp b/images/render/487.webp similarity index 100% rename from images/487.webp rename to images/render/487.webp diff --git a/images/552.webp b/images/render/552.webp similarity index 100% rename from images/552.webp rename to images/render/552.webp diff --git a/images/572.webp b/images/render/572.webp similarity index 100% rename from images/572.webp rename to images/render/572.webp diff --git a/images/583.webp b/images/render/583.webp similarity index 100% rename from images/583.webp rename to images/render/583.webp diff --git a/images/654.webp b/images/render/654.webp similarity index 100% rename from images/654.webp rename to images/render/654.webp diff --git a/images/696.webp b/images/render/696.webp similarity index 100% rename from images/696.webp rename to images/render/696.webp diff --git a/images/70.webp b/images/render/70.webp similarity index 100% rename from images/70.webp rename to images/render/70.webp diff --git a/images/705.webp b/images/render/705.webp similarity index 100% rename from images/705.webp rename to images/render/705.webp diff --git a/images/83.webp b/images/render/83.webp similarity index 100% rename from images/83.webp rename to images/render/83.webp diff --git a/images/858.webp b/images/render/858.webp similarity index 100% rename from images/858.webp rename to images/render/858.webp diff --git a/images/86.webp b/images/render/86.webp similarity index 100% rename from images/86.webp rename to images/render/86.webp diff --git a/images/862.webp b/images/render/862.webp similarity index 100% rename from images/862.webp rename to images/render/862.webp diff --git a/images/911.webp b/images/render/911.webp similarity index 100% rename from images/911.webp rename to images/render/911.webp diff --git a/pyproject.toml b/pyproject.toml index da38ed1..4e96982 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ authors = [{ name = "ICRA contributors" }] license = "MIT" requires-python = ">=3.10" dependencies = [ + "numpy>=1.26", "pillow>=10.0.0", "PySide6>=6.7", ]