SAM Annotaton Tool
import sys
import tomllib

from PyQt5.QtWidgets import QApplication

from src import MainWindow


if __name__ == "__main__":
    with open("config.toml", "rb") as f:
        config = tomllib.load(f)
    path_to_dataset = config["paths"]["data"]
    app = QApplication(sys.argv)
    mw = MainWindow(path_to_dataset)
    mw.show()
    mw.load_latest_sample()
    sys.exit(app.exec_())

__main__.py
Divelix/samat
""" Predicts segmentation masks via SAM model
for all images in given dataset.
Saves mask as 8-bit grayscale .PNG
"""
from pathlib import Path
import time
from PIL import Image
import tomllib

import numpy as np
from tqdm import tqdm
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator


def make_annotator(weights_path: str) -> SamAutomaticMaskGenerator:
    model_type = "vit_h"
    device = "cuda"
    print(f"Loading {model_type} on {device} device")
    t1 = time.perf_counter()
    sam = sam_model_registry[model_type](sam_path)
    t2 = time.perf_counter()
    sam.to(device)
    t3 = time.perf_counter()
    mask_generator = SamAutomaticMaskGenerator(sam)
    print(f"Load weights: {(t2-t1):.3f}s\nMove to {device}: {(t3-t2):.3f}s")
    return mask_generator


if __name__ == "__main__":
    with open("config.toml", "rb") as f:
        config = tomllib.load(f)
    data_path = Path(config["paths"]["data"])
    images_path = data_path / "images"
    assert (
        images_path.exists()
    ), "Data path must contain 'images' folder with all source data images"
    sam_path = data_path / "sam"
    sam_path.mkdir(exist_ok=True)
    sam = make_annotator(config["paths"]["sam_weights"])

    max_masks = 0

    img_stems = [path.stem for path in sorted(images_path.iterdir())]
    for stem in tqdm(img_stems):
        filename = f"{stem}.png"
        img_path = images_path / filename
        out_path = sam_path / filename
        img = Image.open(img_path)
        img = np.array(img)
        masks = sam.generate(img)
        sorted_masks = sorted(masks, key=(lambda x: x["area"]), reverse=True)
        label = np.zeros(sorted_masks[0]["segmentation"].shape, dtype=np.uint8)
        for i, sm in enumerate(sorted_masks):
            m = sm["segmentation"]
            label[m] = i + 1
        max_masks = max(max_masks, np.max(label))
        label_img = Image.fromarray(label, mode="L")
        label_img.save(out_path)

scripts/preprocess_dataset.py
Divelix/samat
from PyQt5.QtCore import Qt, QRectF
from PyQt5.QtGui import QColor, QPen, QBrush
from PyQt5.QtWidgets import QGraphicsEllipseItem


class BrushCursor(QGraphicsEllipseItem):
    def __init__(self, parent=None):
        super().__init__(parent)
        rect = QRectF(-25, -25, 50, 50)
        self.setRect(rect)
        self._border_pen = QPen()
        self._border_pen.setColor(Qt.GlobalColor.black)
        self._border_pen.setWidth(1)
        self._fill_brush = QBrush()
        self._fill_brush.setColor(Qt.GlobalColor.transparent)

    def paint(self, painter, option, widget=None):
        super().paint(painter, option, widget)
        painter.setPen(self._border_pen)
        painter.setBrush(self._fill_brush)
        painter.drawEllipse(self.rect())

    def set_size(self, value: int):
        offset = -value / 2
        new_rect = QRectF(offset, offset, value, value)
        self.setRect(new_rect)

    def set_border_color(self, color: QColor):
        self._border_pen.setColor(color)
        self.update()  # to make changes be visible instantly

src/brush_cursor.py
Divelix/samat
from pathlib import Path

from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
    QGraphicsScene,
    QGraphicsPixmapItem,
    QGraphicsSceneMouseEvent,
)
from PyQt5.QtCore import pyqtSignal, QPointF

from .brush_cursor import BrushCursor
from .label_layer import LabelLayer
from .sam_layer import SamLayer

import numpy as np


class GraphicsScene(QGraphicsScene):
    label2sam_signal = pyqtSignal(QPointF)
    sam2label_signal = pyqtSignal(np.ndarray)

    def __init__(self, parent):
        super().__init__(parent)
        self._brush_size = 50
        self._brush_step = 5
        self._brush_limits = (1, 150)

        self.image_item = QGraphicsPixmapItem()
        self.sam_item = SamLayer(self.image_item, self.sam2label_signal)
        self.label_item = LabelLayer(self.image_item, self.label2sam_signal)
        self.cursor_item = BrushCursor(self.image_item)

        self.label2sam_signal.connect(self.sam_item.handle_click)
        self.sam2label_signal.connect(self.label_item.handle_bundle)

        self.addItem(self.image_item)

    def handle_sam_mode(self, is_sam: bool):
        self.sam_item.handle_sam_mode(is_sam)
        self.label_item.handle_sam_mode(is_sam)
        self._sam_mode = is_sam

    def set_eraser(self, value):
        self.label_item.set_eraser(value)
        if value:
            self.cursor_item.set_border_color(QColor(255, 255, 255))

    def set_brush_color(self, color: QColor):
        self.cursor_item.set_border_color(color)
        self.label_item.set_brush_color(color)

    def set_brush_size(self, value: int):
        assert self._brush_limits[0] <= value <= self._brush_limits[1]
        self._brush_size = value
        self.cursor_item.set_size(self._brush_size)
        self.label_item.set_size(self._brush_size)

    def change_brush_size(self, sign: int, bf: pyqtSignal):
        # fmt: off
        assert sign in (-1, 1), f"Sign value must be either 1 or -1, but {sign} was given"  # noqa: E501
        # fmt: on
        new_size = self._brush_size + (self._brush_step * sign)
        new_size = max(new_size, self._brush_limits[0])
        new_size = min(new_size, self._brush_limits[1])
        self.cursor_item.set_size(new_size)
        self.label_item.set_size(new_size)
        self._brush_size = new_size
        bf.emit(new_size)

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self.cursor_item.setPos(event.scenePos())
        super().mouseMoveEvent(event)

    def save_label(self, label_path: Path):
        self.label_item.export_pixmap(label_path)

src/graphics_scene.py
Divelix/samat
from pathlib import Path

from PyQt5.QtCore import (
    Qt,
    pyqtSlot,
    QPoint,
    QRectF,
    QPointF,
    QSizeF,
)
from PyQt5.QtGui import (
    QColor,
    QPixmap,
    QMouseEvent,
    QWheelEvent,
    QBrush,
    QPainter,
)
from PyQt5.QtWidgets import QFrame, QGraphicsView

from .graphics_scene import GraphicsScene


class GraphicsView(QGraphicsView):
    def __init__(self, brush_feedback, parent=None):
        super().__init__(parent)
        self._scene = GraphicsScene(self)
        self._pan_mode = False
        self._last_pos = QPoint()
        self._brush_feedback = brush_feedback
        self._sam_mode = False

        self.setScene(self._scene)
        self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setBackgroundBrush(QBrush(QColor(50, 50, 50)))
        self.setFrameShape(QFrame.Shape.NoFrame)  # removes white widget outline
        self.setRenderHint(QPainter.RenderHint.HighQualityAntialiasing)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setCursor(Qt.CursorShape.BlankCursor)

    def set_label_opacity(self, value: int):
        self._scene.label_item.setOpacity(value / 100.0)

    def set_sam_opacity(self, value: int):
        self._scene.sam_item.setOpacity(value / 100.0)

    @pyqtSlot(bool)
    def handle_sam_signal(self, is_sam: bool):
        self._sam_mode = is_sam
        self._scene.handle_sam_mode(is_sam)

    def set_brush_color(self, color: QColor):
        self._scene.set_brush_color(color)

    def set_brush_size(self, value: int):
        self._scene.set_brush_size(value)

    def set_eraser(self, value: bool):
        self._scene.set_eraser(value)

    def reset_zoom(self):
        self.fitInView(self._scene.image_item, Qt.AspectRatioMode.KeepAspectRatio)

    def clear_label(self):
        self._scene.label_item.clear()

    def save_label_to(self, path: Path):
        self._scene.save_label(path)

    def load_sample(self, image_path: Path, label_path: Path, sam_path: Path):
        image = QPixmap(str(image_path))
        self._scene.setSceneRect(QRectF(QPointF(), QSizeF(image.size())))
        self._scene.image_item.setPixmap(QPixmap(str(image_path)))
        if label_path.exists():
            self._scene.label_item.set_image(str(label_path))
        else:
            self._scene.label_item.clear()
        if sam_path.exists():
            self._scene.sam_item.set_image(str(sam_path))
        self.fitInView(self._scene.image_item, Qt.AspectRatioMode.KeepAspectRatio)
        self.centerOn(self._scene.image_item)

    def scrollBy(self, point: QPoint):
        h_val = self.horizontalScrollBar().value() - point.x()
        v_val = self.verticalScrollBar().value() - point.y()
        self.horizontalScrollBar().setValue(h_val)
        self.verticalScrollBar().setValue(v_val)

    def mousePressEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.MouseButton.RightButton:
            self._pan_mode = True
            self._last_pos = event.pos()
            self.setCursor(Qt.CursorShape.ClosedHandCursor)
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if self._pan_mode:
            curr_pos = event.pos()
            delta = curr_pos - self._last_pos
            self.scrollBy(delta)
            self._last_pos = curr_pos
        super().mouseMoveEvent(event)  # allows proper zoom-to-cursor

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.MouseButton.RightButton:
            self._pan_mode = False
            self.setCursor(Qt.CursorShape.BlankCursor)
        super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent) -> None:
        forward = event.angleDelta().y() > 0
        if event.modifiers() == Qt.KeyboardModifier.NoModifier:
            # zoom in/out
            factor = 1.25 if forward else 0.8
            self.scale(factor, factor)
        elif event.modifiers() == Qt.KeyboardModifier.ControlModifier:
            # change brush size
            sign = -1 if forward else 1
            self._scene.change_brush_size(sign, self._brush_feedback)

src/graphics_view.py
Divelix/samat
from pathlib import Path

from PyQt5.QtCore import Qt, QLineF, QPoint, QRectF
from PyQt5.QtWidgets import QGraphicsSceneMouseEvent, QGraphicsRectItem
from PyQt5.QtGui import QColor, QPixmap, QPainter, QPen
import numpy as np


class LabelLayer(QGraphicsRectItem):
    def __init__(self, parent, sam_signal):
        super().__init__(parent)
        self.setOpacity(0.5)
        self.setPen(QPen(Qt.PenStyle.NoPen))
        self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)

        self._sam_signal = sam_signal
        self._erase_state = False
        self._brush_color = QColor(0, 0, 0)
        self._brush_size = 50
        self._pixmap = QPixmap()
        self._line = QLineF()
        self._sam_mode = False

    def set_brush_color(self, color: QColor):
        self.set_eraser(False)
        self._brush_color = color

    def set_eraser(self, value: bool):
        self._erase_state = value

    def set_size(self, size: int):
        self._brush_size = size

    def _draw_line(self):
        painter = QPainter(self._pixmap)
        if self._erase_state:
            painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
        pen = QPen(self._brush_color, self._brush_size)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        painter.setPen(pen)
        painter.drawLine(self._line)
        painter.end()
        self.update()

    def _draw_bundle(self, bundle: np.ndarray):
        painter = QPainter(self._pixmap)
        if self._erase_state:
            painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
        pen = QPen(self._brush_color, 1)
        painter.setPen(pen)
        for x, y in bundle:
            painter.drawPoint(x, y)
        self._bundle_to_draw = None
        painter.end()
        self.update()

    def set_image(self, path: str):
        r = self.parentItem().pixmap().rect()
        self.setRect(QRectF(r))
        self._pixmap.load(path)

    def clear(self):
        r = self.parentItem().pixmap().rect()
        self.setRect(QRectF(r))
        self._pixmap = QPixmap(r.size())
        self._pixmap.fill(Qt.GlobalColor.transparent)
        self.update()  # to make changes be visible instantly

    def export_pixmap(self, out_path: Path):
        self._pixmap.save(str(out_path))

    def handle_bundle(self, bundle: np.ndarray):
        if self._sam_mode:
            self._draw_bundle(bundle)

    def paint(self, painter, option, widget=None):
        super().paint(painter, option, widget)
        painter.save()
        painter.drawPixmap(QPoint(), self._pixmap)
        painter.restore()

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self._sam_signal.emit(event.pos())
        self._line.setP1(event.pos())
        self._line.setP2(event.pos())
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self._line.setP2(event.pos())
        self._draw_line()
        self._line.setP1(event.pos())
        super().mouseMoveEvent(event)

    def handle_sam_mode(self, is_sam: bool):
        self._sam_mode = is_sam

src/label_layer.py
Divelix/samat
from pathlib import Path
import json

from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QColor, QKeyEvent, QCloseEvent, QIcon, QPixmap
from PyQt5.QtWidgets import (
    QMainWindow,
    QWidget,
    QGroupBox,
    QCheckBox,
    QSlider,
    QVBoxLayout,
    QHBoxLayout,
    QLabel,
    QListWidget,
    QListWidgetItem,
)

from .graphics_view import GraphicsView


class MainWindow(QMainWindow):
    brush_feedback = pyqtSignal(int)  # allows QSlider react on mouse wheel
    sam_signal = pyqtSignal(bool)  # used to propagate sam mode to all widgets

    def __init__(self, workdir: str):
        super(MainWindow, self).__init__()
        self.setWindowTitle("sam_annotator")
        self.resize(1000, 1000)

        self._workdir = Path(workdir)
        self._class_dir = self._workdir / "classes.json"
        self._image_dir = self._workdir / "images"
        self._label_dir = self._workdir / "labels"
        self._sam_dir = self._workdir / "sam"
        self._label_dir.mkdir(exist_ok=True)
        self._image_stems = [path.stem for path in sorted(self._image_dir.iterdir())]
        with open(self._class_dir, "r") as f:
            self._classes = json.loads("".join(f.readlines()))["classes"]
        ids = [c["id"] for c in self._classes]
        colors = [c["color"] for c in self._classes]
        self._id2color = {k: v for k, v in zip(ids, colors)}

        self.brush_feedback.connect(self.on_brush_size_change)
        self._graphics_view = GraphicsView(self.brush_feedback)
        self.sam_signal.connect(self._graphics_view.handle_sam_signal)

        # Dataset group
        ds_group = QGroupBox(self.tr("Dataset"))

        self.ds_label = QLabel()
        self.ds_label.setText("Sample: 000000.png")

        ds_vlay = QVBoxLayout(ds_group)
        ds_vlay.addWidget(self.ds_label)

        # Layers group
        ls_group = QGroupBox(self.tr("Layers"))

        self.ls_label_value = QLabel()
        self.ls_label_value.setText("Label opacity: 50%")

        self.ls_label_slider = QSlider()
        self.ls_label_slider.setOrientation(Qt.Orientation.Horizontal)
        self.ls_label_slider.setMinimum(0)
        self.ls_label_slider.setMaximum(100)
        self.ls_label_slider.setSliderPosition(50)
        self.ls_label_slider.valueChanged.connect(self.on_ls_label_slider_change)

        self.ls_sam_value = QLabel()
        self.ls_sam_value.setText("SAM opacity: 0%")

        self.ls_sam_slider = QSlider()
        self.ls_sam_slider.setOrientation(Qt.Orientation.Horizontal)
        self.ls_sam_slider.setMinimum(0)
        self.ls_sam_slider.setMaximum(100)
        self.ls_sam_slider.setSliderPosition(0)
        self.ls_sam_slider.valueChanged.connect(self.on_ls_sam_slider_change)

        ls_vlay = QVBoxLayout(ls_group)
        ls_vlay.addWidget(self.ls_label_value)
        ls_vlay.addWidget(self.ls_label_slider)
        ls_vlay.addWidget(self.ls_sam_value)
        ls_vlay.addWidget(self.ls_sam_slider)

        # SAM group
        sam_group = QGroupBox(self.tr("SAM"))

        self.sam_checkbox = QCheckBox("SAM assistance")
        self.sam_checkbox.stateChanged.connect(self.on_sam_change)

        sam_vlay = QVBoxLayout(sam_group)
        sam_vlay.addWidget(self.sam_checkbox)

        # Brush size group
        bs_group = QGroupBox(self.tr("Brush"))

        self.bs_value = QLabel()
        self.bs_value.setText("Size: 50 px")

        self.bs_slider = QSlider()
        self.bs_slider.setOrientation(Qt.Orientation.Horizontal)
        self.bs_slider.setMinimum(1)
        self.bs_slider.setMaximum(150)
        self.bs_slider.setSliderPosition(50)
        self.bs_slider.valueChanged.connect(self.on_bs_slider_change)

        bs_vlay = QVBoxLayout(bs_group)
        bs_vlay.addWidget(self.bs_value)
        bs_vlay.addWidget(self.bs_slider)

        # Classs selection group
        cs_group = QGroupBox(self.tr("Classes"))

        self.cs_list = QListWidget()
        for i, c in enumerate(self._classes):
            color = QColor(c["color"])
            pixmap = QPixmap(20, 20)
            pixmap.fill(color)
            text = f"[{i+1}] {c['name']}"
            item = QListWidgetItem(QIcon(pixmap), text)
            self.cs_list.addItem(item)
        self.cs_list.itemClicked.connect(self.on_item_clicked)

        cs_vlay = QVBoxLayout(cs_group)
        cs_vlay.addWidget(self.cs_list)

        vlay = QVBoxLayout()
        vlay.addWidget(ds_group)
        vlay.addWidget(sam_group)
        vlay.addWidget(ls_group)
        vlay.addWidget(bs_group)
        vlay.addWidget(cs_group)
        vlay.addStretch()

        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        lay = QHBoxLayout(central_widget)
        lay.addWidget(self._graphics_view, stretch=1)
        lay.addLayout(vlay, stretch=0)

        self._curr_id = 0
        self._graphics_view.set_brush_color(QColor(colors[0]))
        self.cs_list.setCurrentRow(0)

    @pyqtSlot(int)
    def on_sam_change(self, state: int):
        if state == Qt.CheckState.Checked:
            self.sam_signal.emit(True)
        elif state == Qt.CheckState.Unchecked:
            self.sam_signal.emit(False)
        else:
            print("unsupported check state")

    @pyqtSlot(int)
    def on_ls_label_slider_change(self, value: int):
        self.ls_label_value.setText(f"Label opacity: {value}%")
        self._graphics_view.set_label_opacity(value)

    @pyqtSlot(int)
    def on_ls_sam_slider_change(self, value: int):
        self.ls_sam_value.setText(f"SAM opacity: {value}%")
        self._graphics_view.set_sam_opacity(value)

    @pyqtSlot(int)
    def on_bs_slider_change(self, value: int):
        self.bs_value.setText(f"Size: {value} px")
        self._graphics_view.set_brush_size(value)

    @pyqtSlot(int)
    def on_brush_size_change(self, value: int):
        # updates slider and value label on brush size change via mouse wheel
        self.bs_value.setText(f"Size: {value} px")
        self.bs_slider.setSliderPosition(value)

    def on_item_clicked(self, item: QListWidgetItem):
        idx = self.sender().currentRow()
        color = self._id2color[idx + 1]
        self._graphics_view.set_brush_color(QColor(color))

    def save_current_label(self):
        curr_label_path = self._label_dir / f"{self._image_stems[self._curr_id]}.png"
        self._graphics_view.save_label_to(curr_label_path)

    def _load_sample_by_id(self, id: int):
        self._curr_id = id
        name = f"{self._image_stems[self._curr_id]}.png"
        image_path = self._image_dir / name
        label_path = self._label_dir / name
        sam_path = self._sam_dir / name
        self._graphics_view.load_sample(image_path, label_path, sam_path)
        self.ds_label.setText(f"Sample: {name}")

    def load_latest_sample(self):
        labels = list(self._label_dir.iterdir())
        images = list(self._image_dir.iterdir())
        if len(labels) < len(images):
            self._load_sample_by_id(len(labels))
        else:
            self._load_sample_by_id(0)

    def _switch_sample_by(self, step: int):
        if step == 0:
            return
        self.save_current_label()
        max_id = len(self._image_stems) - 1
        corner_case_id = 0 if step < 0 else max_id
        new_id = self._curr_id + step
        new_id = new_id if new_id in range(max_id + 1) else corner_case_id
        self._load_sample_by_id(new_id)

    def keyPressEvent(self, a0: QKeyEvent) -> None:
        if a0.key() == Qt.Key.Key_Space:
            self._graphics_view.reset_zoom()
        elif a0.key() == Qt.Key.Key_S:
            self.sam_checkbox.toggle()
        elif a0.key() == Qt.Key.Key_C:
            self._graphics_view.clear_label()
        elif a0.key() == Qt.Key.Key_E:
            self.cs_list.clearSelection()
            self._graphics_view.set_eraser(True)
        elif a0.key() in range(49, 58):
            num_key = int(a0.key()) - 48
            color = self._id2color.get(num_key)
            if color:
                self._graphics_view.set_brush_color(QColor(color))
                self.cs_list.setCurrentRow(num_key - 1)
        elif a0.key() == Qt.Key.Key_Comma:
            self._switch_sample_by(-1)
        elif a0.key() == Qt.Key.Key_Period:
            self._switch_sample_by(1)

        return super().keyPressEvent(a0)

    def closeEvent(self, a0: QCloseEvent) -> None:
        self.save_current_label()
        return super().closeEvent(a0)

src/main_window.py
Divelix/samat
from PyQt5.QtCore import Qt, QPoint, QRectF, QPointF
from PyQt5.QtWidgets import QGraphicsRectItem
from PyQt5.QtGui import QPixmap, QPen
import numpy as np


class SamLayer(QGraphicsRectItem):
    def __init__(self, parent, label_signal):
        super().__init__(parent)
        self.setOpacity(0.0)
        self.setPen(QPen(Qt.PenStyle.NoPen))

        self._label_signal = label_signal
        self._pixmap = QPixmap()
        self._sam_mode = False
        self._img = None  # QImage to fetch color from
        self._np_img = None  # np array for fast pixels fetch

    def set_image(self, path: str):
        r = self.parentItem().pixmap().rect()
        self.setRect(QRectF(r))
        self._pixmap.load(path)
        self._update_img()

    def _update_img(self):
        image = self._pixmap.toImage()
        buffer = image.bits()
        buffer.setsize(image.byteCount())
        np_img = np.frombuffer(buffer, dtype=np.uint8)
        np_img = np_img.reshape((image.height(), image.width(), 4))
        self._img = image
        self._np_img = np_img

    def clear(self):
        r = self.parentItem().pixmap().rect()
        self.setRect(QRectF(r))
        self._pixmap = QPixmap(r.size())
        self._pixmap.fill(Qt.GlobalColor.transparent)
        self.update()  # to make changes be visible instantly

    def paint(self, painter, option, widget=None):
        super().paint(painter, option, widget)
        painter.save()
        painter.drawPixmap(QPoint(), self._pixmap)
        painter.restore()

    def handle_click(self, pos: QPointF):
        if not self._sam_mode or not self._img:
            return
        x = int(pos.x())
        y = int(pos.y())
        pc = self._img.pixelColor(x, y)
        print(f"pixel_color: ({pc.red()}, {pc.green()}, {pc.blue()})")
        if pc.red() == pc.green() == pc.blue() == 0:
            return
        ids = np.where((self._np_img[:, :, :3] == pc.getRgb()[:3]).all(axis=2))
        pixels = np.column_stack((ids[1], ids[0]))
        self._label_signal.emit(pixels)

    def handle_sam_mode(self, is_sam: bool):
        self._sam_mode = is_sam

src/sam_layer.py
Divelix/samat