<reponame>paaksing/ayolo<filename>ayolo/window.py
import sys
from functools import partial
from pathlib import Path
from typing import List, Tuple

from PyQt5 import QtWidgets, QtCore, QtGui
from darktheme.widget_template import DarkPalette

from .background import Background
from .utilities import PropagableLineEdit


class Annotator(QtWidgets.QWidget):
    '''Widget for annotator section (center)'''

    current_annotations: List[Tuple[int, int, int, int, int]]
    current_img_path: Path = None

    def __init__(self, background: Background):
        super().__init__()
        self.background = background
        self.background.annotator = self

        self.begin = QtCore.QPoint()
        self.end = QtCore.QPoint()
        self.img_bounds = (0, 0, 0, 0)
        self.ratio = (1, 1)
        self.drawing = False
        self.deleted = False
        self.nulled = False
        self.current_annotations = []

        self.setMouseTracking(True)
        self.setCursor(QtCore.Qt.CursorShape.BlankCursor)
        self.show()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        if self.current_img_path:
            self.draw_img(painter, self.current_img_path)
        for x1, y1, x2, y2, clas in self.current_annotations:
            rgb = self.background.get_color(clas, self.background.control_panel.classlength)
            self.modify_painter_brush_and_pen(painter, rgb)
            painter.drawRect(QtCore.QRect(self.get_scaled_coordinate(x1, y1), self.get_scaled_coordinate(x2, y2)))
        rgb = self.background.get_color(self.background.control_panel.get_selected_class_id(), self.background.control_panel.classlength)
        self.modify_painter_brush_and_pen(painter, rgb)
        if self.drawing:
            painter.drawRect(QtCore.QRect(self.begin, self.end))
        self.force_bounded_pos(self.end)
        painter.pen().setWidth(1)
        painter.drawLine(self.end.x() + 1, self.img_bounds[1], self.end.x() + 1, self.img_bounds[3])
        painter.drawLine(self.img_bounds[0], self.end.y() + 1, self.img_bounds[2], self.end.y() + 1)
        coordinate_text_pos = QtCore.QPoint(self.end.x() + 1 if self.end.x() < self.img_bounds[2] - 75 else self.end.x() - 75, self.end.y() - 1 if self.end.y() > 20 else self.end.y() + 12)
        real_pos = self.get_real_coordinate(self.end)
        painter.drawText(coordinate_text_pos, f'({real_pos[0]}, {real_pos[1]})')

    def mousePressEvent(self, event: QtGui.QMouseEvent):
        if event.button() == QtCore.Qt.MouseButton.RightButton:
            if not self.drawing:
                self.undo_annotation()
            else:
                self.begin = QtCore.QPoint()
                self.end = QtCore.QPoint()
                self.end = event.pos()
                self.drawing = not self.drawing
                self.update()
        elif event.button() == QtCore.Qt.MouseButton.LeftButton:
            pos = self.force_bounded_pos(event.pos())
            if not self.drawing:
                self.begin = pos
            else:
                self.current_annotations.append(
                    (
                        *self.force_top_left_corner(self.get_real_coordinate(self.begin), self.get_real_coordinate(self.end)),
                        self.background.control_panel.get_selected_class_id()
                    )
                )
                self.background.control_panel.update_current_annotations_lw(self.current_annotations)
                self.background.control_panel.search.setFocus()
                self.background.control_panel.search.selectAll()
                self.begin = QtCore.QPoint()
                self.end = QtCore.QPoint()
            self.end = pos
            self.drawing = not self.drawing
            self.update()
        elif event.button() == QtCore.Qt.MouseButton.ForwardButton:
            self.background.image_browser.navigate_next()
        elif event.button() == QtCore.Qt.MouseButton.BackButton:
            self.background.image_browser.navigate_prev()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
        pos = self.force_bounded_pos(event.pos())
        self.end = pos
        self.update()

    def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
        wheelcounter = event.angleDelta()
        current_row = self.background.control_panel.classes_lw.currentRow()
        if wheelcounter.y() / 120 == -1:
            if current_row < self.background.control_panel.classes_lw.count() - 1:
                self.background.control_panel.classes_lw.setCurrentRow(current_row + 1)
        elif wheelcounter.y() / 120 == 1:
            if current_row > 0:
                self.background.control_panel.classes_lw.setCurrentRow(current_row - 1)

    def resizeEvent(self, _):
        self.begin = QtCore.QPoint(self.img_bounds[0], self.img_bounds[1])
        self.end = QtCore.QPoint(self.img_bounds[0], self.img_bounds[1])
        self.drawing = False
        self.update()

    def get_real_coordinate(self, pos: QtCore.QPoint):
        return round((pos.x() - self.img_bounds[0]) * self.ratio[0]), round((pos.y() - self.img_bounds[1]) * self.ratio[1])

    def get_scaled_coordinate(self, x: int, y: int):
        return QtCore.QPoint(round(x / self.ratio[0] + self.img_bounds[0]), round(y / self.ratio[1] + self.img_bounds[1]))

    def force_top_left_corner(self, x: Tuple[int, int], y: Tuple[int, int]):
        return min(x[0], y[0]), min(x[1], y[1]), max(x[0], y[0]), max(x[1], y[1])

    def modify_painter_brush_and_pen(self, painter: QtGui.QPainter, rgb):
        painter.setBrush(QtGui.QBrush(QtGui.QColor(*rgb, 70)))
        pen = QtGui.QPen(QtGui.QColor(*rgb))
        pen.setWidth(2)
        painter.setPen(pen)

    def draw_img(self, painter: QtGui.QPainter, path: Path):
        size = self.size()
        point = QtCore.QPoint(0, 0)
        pixmap = QtGui.QPixmap(str(path))
        original_size = (pixmap.size().width(), pixmap.size().height())
        scaledPix = pixmap.scaled(size, QtCore.Qt.AspectRatioMode.KeepAspectRatio, transformMode=QtCore.Qt.TransformationMode.SmoothTransformation)
        point.setX((size.width() - scaledPix.width())/2)
        point.setY((size.height() - scaledPix.height())/2)
        self.img_bounds = (point.x(), point.y(), point.x() + scaledPix.size().width(), point.y() + scaledPix.size().height())
        try:
            self.ratio = (original_size[0] / scaledPix.width(), original_size[1] / scaledPix.height())
        except ZeroDivisionError:
            msg_box = QtWidgets.QtWidgets.Qtclose = QtWidgets.QMessageBox()
            msg_box.critical(self.background.image_browser, 'Error loading image', "Image not found or it's dimension could not be resolved.", QtWidgets.QMessageBox.StandardButton.Ok)
            painter.end()
            self.background.image_browser.navigate_next()
        painter.drawPixmap(point, scaledPix)

    def force_bounded_pos(self, pos: QtCore.QPoint):
        if pos.x() < self.img_bounds[0]:
            pos.setX(self.img_bounds[0])
        elif pos.x() > self.img_bounds[2]:
            pos.setX(self.img_bounds[2])
        if pos.y() < self.img_bounds[1]:
            pos.setY(self.img_bounds[1])
        elif pos.y() > self.img_bounds[3]:
            pos.setY(self.img_bounds[3])
        return pos

    def clear_annotations(self):
        self.drawing = False
        self.current_annotations = []
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.update()

    def save_annotations(self, null=False):
        if self.deleted or self.current_img_path is None:
            return
        if null:
            self.background.images.save(self.current_img_path.name, self.current_annotations)
            self.nulled = True
            self.background.image_browser.navigate_next()
        elif len(self.current_annotations):
            self.background.images.save(self.current_img_path.name, self.current_annotations)
        elif not self.nulled:
            self.background.images.pop(self.current_img_path.name)

    def undo_annotation(self):
        try:
            self.current_annotations.pop()
            self.background.control_panel.update_current_annotations_lw(self.current_annotations)
            self.update()
        except IndexError:
            pass

    def open_image(self, path: Path, annotations):
        self.current_img_path = path
        self.current_annotations = annotations
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.deleted = False
        self.drawing = False
        self.nulled = False
        self.update()

    def update_last_annotation_class(self, clas):
        # try:
        #     last = list(self.current_annotations[-1])
        #     last[-1] = clas
        #     self.current_annotations[-1] = tuple(last)
        #     self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        # except IndexError:
        #     pass
        # THIS WAS REMOVED SINCE v0.2.0 AS YOU SHOULD SELECT CLASS BEFORE ANNOTATING
        self.update()

    def push_back_annotation(self, ind):
        self.current_annotations.append(self.current_annotations.pop(ind))
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.update()


class ControlPanel(QtWidgets.QWidget):
    '''Widget for control panel section (right)'''

    def __init__(self, background: Background):        
        super().__init__()
        self.background = background
        self.background.control_panel = self
        self.classnames = self.background.classes
        self.classnames_lower_set = set(clas.lower() for clas in self.classnames)
        self.classlength = len(self.classnames)
        self.sorted_class_ids_by_name = sorted(list(range(self.classlength)), key=lambda x: self.classnames[x])
        self.search_result_class_ids = list(range(self.classlength))

        layout = QtWidgets.QVBoxLayout()

        self.search = PropagableLineEdit()
        self.classes_lw = QtWidgets.QListWidget()
        
        self.current_annotations_lb = QtWidgets.QLabel('Current Annotations')
        self.current_annotations_lw = QtWidgets.QListWidget()
        self.current_annotations_lw.currentRowChanged.connect(self.current_annotations_row_changed)

        self.save_btn = QtWidgets.QPushButton('Save (S)')
        self.save_btn.clicked.connect(self.background.annotator.save_annotations)
        self.null_btn = QtWidgets.QPushButton('Null (N)')
        self.null_btn.clicked.connect(partial(self.background.annotator.save_annotations, null=True))
        self.undo_btn = QtWidgets.QPushButton('Undo (Z)')
        self.undo_btn.clicked.connect(self.background.annotator.undo_annotation)
        self.delete_btn = QtWidgets.QPushButton('Delete (D)')
        self.delete_btn.clicked.connect(self.delete_current_image)
        self.clear_btn = QtWidgets.QPushButton('Clear (X)')
        self.clear_btn.clicked.connect(self.background.annotator.clear_annotations)

        layout.addWidget(self.current_annotations_lb)
        layout.addWidget(self.current_annotations_lw)
        layout.addWidget(self.save_btn)
        layout.addWidget(self.null_btn)
        layout.addWidget(self.undo_btn)
        layout.addWidget(self.delete_btn)
        layout.addWidget(self.clear_btn)
        layout.addWidget(QtWidgets.QLabel('Class Search'))
        layout.addWidget(self.search)
        layout.addWidget(self.classes_lw)

        self.search.textChanged.connect(self.update_search_results)
        self.classes_lw.currentRowChanged.connect(self.selected_class_id_changed)

        self.update_search_results("")
        layout.setContentsMargins(20, 0, 20, 0)
        self.setLayout(layout)

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
        current_row = self.classes_lw.currentRow()
        if event.key() == QtCore.Qt.Key.Key_Down:
            if current_row < self.classes_lw.count() - 1:
                self.classes_lw.setCurrentRow(current_row + 1)
        elif event.key() == QtCore.Qt.Key.Key_Up:
            if current_row > 0:
                self.classes_lw.setCurrentRow(current_row - 1)
        elif event.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter):
            if current_row == self.classes_lw.count() - 1 and "Create class " in self.classes_lw.currentItem().text():
                self.background.classes.create_class(self.search.text())
                self.update_classes_vars()
                self.update_search_results(self.search.text())

    def update_classes_vars(self):
        self.classnames_lower_set = set(clas.lower() for clas in self.classnames)
        self.classlength = len(self.classnames)
        self.sorted_class_ids_by_name = sorted(list(range(self.classlength)), key=lambda x: self.classnames[x])
        self.search_result_class_ids = list(range(self.classlength))

    def update_search_results(self, text: str):
        self.classes_lw.currentRowChanged.disconnect()
        self.classes_lw.clear()
        self.search_result_class_ids = []
        for clas in (self.sorted_class_ids_by_name if text else range(self.classlength)):
            if self.classnames[clas].lower().startswith(text.lower()):
                list_item = QtWidgets.QListWidgetItem(f"{self.classnames[clas]} (#{clas})")
                color = self.background.get_color(clas, self.classlength)
                list_item.setForeground(QtGui.QColor(*color))
                self.classes_lw.addItem(list_item)
                self.search_result_class_ids.append(clas)
        if text not in self.classnames_lower_set and "Create class " not in self.search.text():
            list_item = QtWidgets.QListWidgetItem(f'Create class "{self.search.text()}" (Enter)')
            self.classes_lw.addItem(list_item)
        self.classes_lw.setCurrentRow(0)
        self.classes_lw.currentRowChanged.connect(self.selected_class_id_changed)
        self.selected_class_id_changed()
        self.update()

    def update_current_annotations_lw(self, annotations):
        self.current_annotations_lw.clear()
        for x1, y1, x2, y2, clas in annotations:
            list_item = QtWidgets.QListWidgetItem(f"{self.classnames[clas]} ({x1}, {y1}) * ({x2}, {y2})")
            color = self.background.get_color(clas, self.classlength)
            list_item.setForeground(QtGui.QColor(*color))
            self.current_annotations_lw.addItem(list_item)
        self.update()

    def current_annotations_row_changed(self, ind: int):
        if ind == self.current_annotations_lw.count() - 1:
            return
        self.background.annotator.push_back_annotation(ind)
        self.current_annotations_lw.setCurrentRow(self.current_annotations_lw.count() - 1)

    def get_selected_class_id(self):
        row = self.classes_lw.currentRow()
        if row < self.classes_lw.count() and "Create class " not in self.classes_lw.currentItem().text():
            return self.search_result_class_ids[self.classes_lw.currentRow()]
        return -1

    def selected_class_id_changed(self):
        self.background.annotator.update_last_annotation_class(self.get_selected_class_id())

    def navigate_prev_image(self):
        self.background.image_browser.navigate_prev()

    def navigate_next_image(self):
        self.background.image_browser.navigate_next()

    def delete_current_image(self):
        self.background.annotator.deleted = True
        self.background.images.remove(self.background.annotator.current_img_path.name)
        self.navigate_next_image()

    def mark_current_image_unannotate(self):
        self.background.images.remove(self.background.annotator.current_img_path.name)
        self.background.annotator.clear_annotations()


class ImageBrowser(QtWidgets.QWidget):
    '''Widget for image browser section (left)'''

    def __init__(self, background: Background):
        super().__init__()
        self.background = background
        self.background.image_browser = self
        self.images_states = self.background.images.image_annotation_counts
        self.image_names = {}
        self.current_image_name = None
        self.updating = False
        self.current_lw = None
        self.current_lw_index = None

        layout = QtWidgets.QVBoxLayout()

        self.annotated_lw = QtWidgets.QListWidget()
        self.unannotated_lw = QtWidgets.QListWidget()

        self.prev_btn = QtWidgets.QPushButton('Prev <<')
        self.prev_btn.clicked.connect(self.navigate_prev)
        self.next_btn = QtWidgets.QPushButton('Next >>')
        self.next_btn.clicked.connect(self.navigate_next)
        self.annotated_lw.currentRowChanged.connect(partial(self.selected_image_changed, 0))
        self.unannotated_lw.currentRowChanged.connect(partial(self.selected_image_changed, 1))
        self.annotated_lb = QtWidgets.QLabel('Annotated')
        self.unannotated_lb = QtWidgets.QLabel('Unannotated')

        self.lw_list: List[QtWidgets.QListWidget] = [self.annotated_lw, self.unannotated_lw]

        layout.addWidget(self.prev_btn)
        layout.addWidget(self.next_btn)
        layout.addWidget(self.annotated_lb)
        layout.addWidget(self.annotated_lw)
        layout.addWidget(self.unannotated_lb)
        layout.addWidget(self.unannotated_lw)

        layout.setContentsMargins(20, 0, 20, 0)
        self.setLayout(layout)
        self.update_list_widgets()
        if self.unannotated_lw.count():
            self.unannotated_lw.setCurrentRow(0)
        elif self.annotated_lw.count():
            self.annotated_lw.setCurrentRow(0)

    def selected_image_changed(self, lw_index: int, row: int):
        if row == -1 or self.updating:
            return
        self.background.annotator.save_annotations()
        self.background.annotator.clear_annotations()
        this_lw = self.lw_list[lw_index]
        img_name = self.image_names[this_lw.currentItem().text()] # Switch back (when item name != item path)
        other_ind = (lw_index + 1) % len(self.lw_list)
        other_lw = self.lw_list[other_ind]
        if other_lw.currentRow() != -1:
            other_lw.clearSelection()
        self.load_image(img_name)

    def update_list_widgets(self):
        self.updating = True
        self.annotated_lw.clear()
        self.unannotated_lw.clear()
        self.image_names = {}
        for img_name, count in self.images_states.items():
            if count is None:
                title = img_name
                self.unannotated_lw.addItem(QtWidgets.QListWidgetItem(title))
                self.image_names[title] = img_name
                if img_name == self.current_image_name:
                    self.unannotated_lw.setCurrentRow(self.unannotated_lw.count() - 1)
                    self.current_lw = self.unannotated_lw
                    self.current_lw_index = 1
            else:
                title = f"{img_name} ({count})"
                self.annotated_lw.addItem(QtWidgets.QListWidgetItem(title))
                self.image_names[title] = img_name
                if img_name == self.current_image_name:
                    self.annotated_lw.setCurrentRow(self.annotated_lw.count() - 1)
                    self.current_lw = self.annotated_lw
                    self.current_lw_index = 0
        self.annotated_lb.setText(f"Annotated ({self.annotated_lw.count()})")
        self.unannotated_lb.setText(f"Unannotated ({self.unannotated_lw.count()})")
        self.update()
        self.updating = False

    def load_image(self, img_name):
        self.background.annotator.open_image(self.background.dir_path / img_name, self.background.images.annotations.get(img_name, []))
        self.current_image_name = img_name
        self.update_list_widgets()

    def other_lw(self, index):
        return self.lw_list[(index + 1) % len(self.lw_list)]

    def navigate_prev(self):
        other_lw = self.other_lw(self.current_lw_index)
        if self.current_lw.currentRow() - 1 < 0 and other_lw.count():
            other_lw.setCurrentRow(other_lw.count() - 1)
        else:
            self.current_lw.setCurrentRow((self.current_lw.currentRow() - 1) % self.current_lw.count())

    def navigate_next(self):
        other_lw = self.other_lw(self.current_lw_index)
        if self.current_lw.currentRow() + 1 >= self.current_lw.count() and other_lw.count():
            other_lw.setCurrentRow(0)
        else:
            self.current_lw.setCurrentRow((self.current_lw.currentRow() + 1) % self.current_lw.count())


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, dir_path: str, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setWindowTitle("Ayolo - Annotating tool for yolo v4 datasets")

        widget = QtWidgets.QWidget(self)
        self.resize(1800, 900)
        self.setCentralWidget(widget)
        self.center_self()

        layout = QtWidgets.QHBoxLayout()

        self.background = Background(dir_path)
        self.annotator = Annotator(self.background)
        self.control_panel = ControlPanel(self.background)
        self.image_browser = ImageBrowser(self.background)

        self.save_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+S"), self)
        self.save_sc.activated.connect(self.control_panel.save_btn.animateClick)
        self.null_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+N"), self)
        self.null_sc.activated.connect(self.control_panel.null_btn.animateClick)
        self.undo_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Z"), self)
        self.undo_sc.activated.connect(self.control_panel.undo_btn.animateClick)
        self.delete_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self)
        self.delete_sc.activated.connect(self.control_panel.delete_btn.animateClick)
        self.clear_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+X"), self)
        self.clear_sc.activated.connect(self.control_panel.save_btn.animateClick)
        self.nav_prev_sc = QtWidgets.QShortcut(QtCore.Qt.Key.Key_PageUp, self)
        self.nav_prev_sc.activated.connect(self.image_browser.prev_btn.animateClick)
        self.nav_next_sc = QtWidgets.QShortcut(QtCore.Qt.Key.Key_PageDown, self)
        self.nav_next_sc.activated.connect(self.image_browser.next_btn.animateClick)

        layout.setContentsMargins(0, 20, 0, 20)

        layout.addWidget(self.image_browser, 2)
        layout.addWidget(self.annotator, 8)
        layout.addWidget(self.control_panel, 2)

        widget.setLayout(layout)
        self.background.control_panel.search.setFocus()
        self.activateWindow()

    def closeEvent(self, event):
        close = QtWidgets.QMessageBox()
        close.setText("Are you sure you want to exit ?")
        close.setWindowTitle("Exit")
        close.setIcon(QtWidgets.QMessageBox.Icon.Question)
        close.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
        close = close.exec()

        if close == QtWidgets.QMessageBox.Yes:
            self.annotator.save_annotations()
            event.accept()
        else:
            event.ignore()

    def center_self(self):
        qtRectangle = self.frameGeometry()
        centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        self.move(qtRectangle.topLeft())

    @classmethod
    def run(cls, dir_path):
        app = QtWidgets.QApplication(sys.argv)

        app.setStyle("Fusion")
        app.setPalette(DarkPalette())
        app.setStyleSheet("QToolTip { color: #ffffff; background-color: grey; border: 1px solid white; }")

        main = cls(dir_path)
        main.show()

        sys.exit(app.exec_())

import sys
from functools import partial
from pathlib import Path
from typing import List, Tuple

from PyQt5 import QtWidgets, QtCore, QtGui
from darktheme.widget_template import DarkPalette

from .background import Background
from .utilities import PropagableLineEdit


class Annotator(QtWidgets.QWidget):
    '''Widget for annotator section (center)'''

    current_annotations: List[Tuple[int, int, int, int, int]]
    current_img_path: Path = None

    def __init__(self, background: Background):
        super().__init__()
        self.background = background
        self.background.annotator = self

        self.begin = QtCore.QPoint()
        self.end = QtCore.QPoint()
        self.img_bounds = (0, 0, 0, 0)
        self.ratio = (1, 1)
        self.drawing = False
        self.deleted = False
        self.nulled = False
        self.current_annotations = []

        self.setMouseTracking(True)
        self.setCursor(QtCore.Qt.CursorShape.BlankCursor)
        self.show()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        if self.current_img_path:
            self.draw_img(painter, self.current_img_path)
        for x1, y1, x2, y2, clas in self.current_annotations:
            rgb = self.background.get_color(clas, self.background.control_panel.classlength)
            self.modify_painter_brush_and_pen(painter, rgb)
            painter.drawRect(QtCore.QRect(self.get_scaled_coordinate(x1, y1), self.get_scaled_coordinate(x2, y2)))
        rgb = self.background.get_color(self.background.control_panel.get_selected_class_id(), self.background.control_panel.classlength)
        self.modify_painter_brush_and_pen(painter, rgb)
        if self.drawing:
            painter.drawRect(QtCore.QRect(self.begin, self.end))
        self.force_bounded_pos(self.end)
        painter.pen().setWidth(1)
        painter.drawLine(self.end.x() + 1, self.img_bounds[1], self.end.x() + 1, self.img_bounds[3])
        painter.drawLine(self.img_bounds[0], self.end.y() + 1, self.img_bounds[2], self.end.y() + 1)
        coordinate_text_pos = QtCore.QPoint(self.end.x() + 1 if self.end.x() < self.img_bounds[2] - 75 else self.end.x() - 75, self.end.y() - 1 if self.end.y() > 20 else self.end.y() + 12)
        real_pos = self.get_real_coordinate(self.end)
        painter.drawText(coordinate_text_pos, f'({real_pos[0]}, {real_pos[1]})')

    def mousePressEvent(self, event: QtGui.QMouseEvent):
        if event.button() == QtCore.Qt.MouseButton.RightButton:
            if not self.drawing:
                self.undo_annotation()
            else:
                self.begin = QtCore.QPoint()
                self.end = QtCore.QPoint()
                self.end = event.pos()
                self.drawing = not self.drawing
                self.update()
        elif event.button() == QtCore.Qt.MouseButton.LeftButton:
            pos = self.force_bounded_pos(event.pos())
            if not self.drawing:
                self.begin = pos
            else:
                self.current_annotations.append(
                    (
                        *self.force_top_left_corner(self.get_real_coordinate(self.begin), self.get_real_coordinate(self.end)),
                        self.background.control_panel.get_selected_class_id()
                    )
                )
                self.background.control_panel.update_current_annotations_lw(self.current_annotations)
                self.background.control_panel.search.setFocus()
                self.background.control_panel.search.selectAll()
                self.begin = QtCore.QPoint()
                self.end = QtCore.QPoint()
            self.end = pos
            self.drawing = not self.drawing
            self.update()
        elif event.button() == QtCore.Qt.MouseButton.ForwardButton:
            self.background.image_browser.navigate_next()
        elif event.button() == QtCore.Qt.MouseButton.BackButton:
            self.background.image_browser.navigate_prev()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
        pos = self.force_bounded_pos(event.pos())
        self.end = pos
        self.update()

    def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
        wheelcounter = event.angleDelta()
        current_row = self.background.control_panel.classes_lw.currentRow()
        if wheelcounter.y() / 120 == -1:
            if current_row < self.background.control_panel.classes_lw.count() - 1:
                self.background.control_panel.classes_lw.setCurrentRow(current_row + 1)
        elif wheelcounter.y() / 120 == 1:
            if current_row > 0:
                self.background.control_panel.classes_lw.setCurrentRow(current_row - 1)

    def resizeEvent(self, _):
        self.begin = QtCore.QPoint(self.img_bounds[0], self.img_bounds[1])
        self.end = QtCore.QPoint(self.img_bounds[0], self.img_bounds[1])
        self.drawing = False
        self.update()

    def get_real_coordinate(self, pos: QtCore.QPoint):
        return round((pos.x() - self.img_bounds[0]) * self.ratio[0]), round((pos.y() - self.img_bounds[1]) * self.ratio[1])

    def get_scaled_coordinate(self, x: int, y: int):
        return QtCore.QPoint(round(x / self.ratio[0] + self.img_bounds[0]), round(y / self.ratio[1] + self.img_bounds[1]))

    def force_top_left_corner(self, x: Tuple[int, int], y: Tuple[int, int]):
        return min(x[0], y[0]), min(x[1], y[1]), max(x[0], y[0]), max(x[1], y[1])

    def modify_painter_brush_and_pen(self, painter: QtGui.QPainter, rgb):
        painter.setBrush(QtGui.QBrush(QtGui.QColor(*rgb, 70)))
        pen = QtGui.QPen(QtGui.QColor(*rgb))
        pen.setWidth(2)
        painter.setPen(pen)

    def draw_img(self, painter: QtGui.QPainter, path: Path):
        size = self.size()
        point = QtCore.QPoint(0, 0)
        pixmap = QtGui.QPixmap(str(path))
        original_size = (pixmap.size().width(), pixmap.size().height())
        scaledPix = pixmap.scaled(size, QtCore.Qt.AspectRatioMode.KeepAspectRatio, transformMode=QtCore.Qt.TransformationMode.SmoothTransformation)
        point.setX((size.width() - scaledPix.width())/2)
        point.setY((size.height() - scaledPix.height())/2)
        self.img_bounds = (point.x(), point.y(), point.x() + scaledPix.size().width(), point.y() + scaledPix.size().height())
        try:
            self.ratio = (original_size[0] / scaledPix.width(), original_size[1] / scaledPix.height())
        except ZeroDivisionError:
            msg_box = QtWidgets.QtWidgets.Qtclose = QtWidgets.QMessageBox()
            msg_box.critical(self.background.image_browser, 'Error loading image', "Image not found or it's dimension could not be resolved.", QtWidgets.QMessageBox.StandardButton.Ok)
            painter.end()
            self.background.image_browser.navigate_next()
        painter.drawPixmap(point, scaledPix)

    def force_bounded_pos(self, pos: QtCore.QPoint):
        if pos.x() < self.img_bounds[0]:
            pos.setX(self.img_bounds[0])
        elif pos.x() > self.img_bounds[2]:
            pos.setX(self.img_bounds[2])
        if pos.y() < self.img_bounds[1]:
            pos.setY(self.img_bounds[1])
        elif pos.y() > self.img_bounds[3]:
            pos.setY(self.img_bounds[3])
        return pos

    def clear_annotations(self):
        self.drawing = False
        self.current_annotations = []
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.update()

    def save_annotations(self, null=False):
        if self.deleted or self.current_img_path is None:
            return
        if null:
            self.background.images.save(self.current_img_path.name, self.current_annotations)
            self.nulled = True
            self.background.image_browser.navigate_next()
        elif len(self.current_annotations):
            self.background.images.save(self.current_img_path.name, self.current_annotations)
        elif not self.nulled:
            self.background.images.pop(self.current_img_path.name)

    def undo_annotation(self):
        try:
            self.current_annotations.pop()
            self.background.control_panel.update_current_annotations_lw(self.current_annotations)
            self.update()
        except IndexError:
            pass

    def open_image(self, path: Path, annotations):
        self.current_img_path = path
        self.current_annotations = annotations
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.deleted = False
        self.drawing = False
        self.nulled = False
        self.update()

    def update_last_annotation_class(self, clas):
        # try:
        #     last = list(self.current_annotations[-1])
        #     last[-1] = clas
        #     self.current_annotations[-1] = tuple(last)
        #     self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        # except IndexError:
        #     pass
        # THIS WAS REMOVED SINCE v0.2.0 AS YOU SHOULD SELECT CLASS BEFORE ANNOTATING
        self.update()

    def push_back_annotation(self, ind):
        self.current_annotations.append(self.current_annotations.pop(ind))
        self.background.control_panel.update_current_annotations_lw(self.current_annotations)
        self.update()


class ControlPanel(QtWidgets.QWidget):
    '''Widget for control panel section (right)'''

    def __init__(self, background: Background):        
        super().__init__()
        self.background = background
        self.background.control_panel = self
        self.classnames = self.background.classes
        self.classnames_lower_set = set(clas.lower() for clas in self.classnames)
        self.classlength = len(self.classnames)
        self.sorted_class_ids_by_name = sorted(list(range(self.classlength)), key=lambda x: self.classnames[x])
        self.search_result_class_ids = list(range(self.classlength))

        layout = QtWidgets.QVBoxLayout()

        self.search = PropagableLineEdit()
        self.classes_lw = QtWidgets.QListWidget()
        
        self.current_annotations_lb = QtWidgets.QLabel('Current Annotations')
        self.current_annotations_lw = QtWidgets.QListWidget()
        self.current_annotations_lw.currentRowChanged.connect(self.current_annotations_row_changed)

        self.save_btn = QtWidgets.QPushButton('Save (S)')
        self.save_btn.clicked.connect(self.background.annotator.save_annotations)
        self.null_btn = QtWidgets.QPushButton('Null (N)')
        self.null_btn.clicked.connect(partial(self.background.annotator.save_annotations, null=True))
        self.undo_btn = QtWidgets.QPushButton('Undo (Z)')
        self.undo_btn.clicked.connect(self.background.annotator.undo_annotation)
        self.delete_btn = QtWidgets.QPushButton('Delete (D)')
        self.delete_btn.clicked.connect(self.delete_current_image)
        self.clear_btn = QtWidgets.QPushButton('Clear (X)')
        self.clear_btn.clicked.connect(self.background.annotator.clear_annotations)

        layout.addWidget(self.current_annotations_lb)
        layout.addWidget(self.current_annotations_lw)
        layout.addWidget(self.save_btn)
        layout.addWidget(self.null_btn)
        layout.addWidget(self.undo_btn)
        layout.addWidget(self.delete_btn)
        layout.addWidget(self.clear_btn)
        layout.addWidget(QtWidgets.QLabel('Class Search'))
        layout.addWidget(self.search)
        layout.addWidget(self.classes_lw)

        self.search.textChanged.connect(self.update_search_results)
        self.classes_lw.currentRowChanged.connect(self.selected_class_id_changed)

        self.update_search_results("")
        layout.setContentsMargins(20, 0, 20, 0)
        self.setLayout(layout)

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
        current_row = self.classes_lw.currentRow()
        if event.key() == QtCore.Qt.Key.Key_Down:
            if current_row < self.classes_lw.count() - 1:
                self.classes_lw.setCurrentRow(current_row + 1)
        elif event.key() == QtCore.Qt.Key.Key_Up:
            if current_row > 0:
                self.classes_lw.setCurrentRow(current_row - 1)
        elif event.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter):
            if current_row == self.classes_lw.count() - 1 and "Create class " in self.classes_lw.currentItem().text():
                self.background.classes.create_class(self.search.text())
                self.update_classes_vars()
                self.update_search_results(self.search.text())

    def update_classes_vars(self):
        self.classnames_lower_set = set(clas.lower() for clas in self.classnames)
        self.classlength = len(self.classnames)
        self.sorted_class_ids_by_name = sorted(list(range(self.classlength)), key=lambda x: self.classnames[x])
        self.search_result_class_ids = list(range(self.classlength))

    def update_search_results(self, text: str):
        self.classes_lw.currentRowChanged.disconnect()
        self.classes_lw.clear()
        self.search_result_class_ids = []
        for clas in (self.sorted_class_ids_by_name if text else range(self.classlength)):
            if self.classnames[clas].lower().startswith(text.lower()):
                list_item = QtWidgets.QListWidgetItem(f"{self.classnames[clas]} (#{clas})")
                color = self.background.get_color(clas, self.classlength)
                list_item.setForeground(QtGui.QColor(*color))
                self.classes_lw.addItem(list_item)
                self.search_result_class_ids.append(clas)
        if text not in self.classnames_lower_set and "Create class " not in self.search.text():
            list_item = QtWidgets.QListWidgetItem(f'Create class "{self.search.text()}" (Enter)')
            self.classes_lw.addItem(list_item)
        self.classes_lw.setCurrentRow(0)
        self.classes_lw.currentRowChanged.connect(self.selected_class_id_changed)
        self.selected_class_id_changed()
        self.update()

    def update_current_annotations_lw(self, annotations):
        self.current_annotations_lw.clear()
        for x1, y1, x2, y2, clas in annotations:
            list_item = QtWidgets.QListWidgetItem(f"{self.classnames[clas]} ({x1}, {y1}) * ({x2}, {y2})")
            color = self.background.get_color(clas, self.classlength)
            list_item.setForeground(QtGui.QColor(*color))
            self.current_annotations_lw.addItem(list_item)
        self.update()

    def current_annotations_row_changed(self, ind: int):
        if ind == self.current_annotations_lw.count() - 1:
            return
        self.background.annotator.push_back_annotation(ind)
        self.current_annotations_lw.setCurrentRow(self.current_annotations_lw.count() - 1)

    def get_selected_class_id(self):
        row = self.classes_lw.currentRow()
        if row < self.classes_lw.count() and "Create class " not in self.classes_lw.currentItem().text():
            return self.search_result_class_ids[self.classes_lw.currentRow()]
        return -1

    def selected_class_id_changed(self):
        self.background.annotator.update_last_annotation_class(self.get_selected_class_id())

    def navigate_prev_image(self):
        self.background.image_browser.navigate_prev()

    def navigate_next_image(self):
        self.background.image_browser.navigate_next()

    def delete_current_image(self):
        self.background.annotator.deleted = True
        self.background.images.remove(self.background.annotator.current_img_path.name)
        self.navigate_next_image()

    def mark_current_image_unannotate(self):
        self.background.images.remove(self.background.annotator.current_img_path.name)
        self.background.annotator.clear_annotations()


class ImageBrowser(QtWidgets.QWidget):
    '''Widget for image browser section (left)'''

    def __init__(self, background: Background):
        super().__init__()
        self.background = background
        self.background.image_browser = self
        self.images_states = self.background.images.image_annotation_counts
        self.image_names = {}
        self.current_image_name = None
        self.updating = False
        self.current_lw = None
        self.current_lw_index = None

        layout = QtWidgets.QVBoxLayout()

        self.annotated_lw = QtWidgets.QListWidget()
        self.unannotated_lw = QtWidgets.QListWidget()

        self.prev_btn = QtWidgets.QPushButton('Prev <<')
        self.prev_btn.clicked.connect(self.navigate_prev)
        self.next_btn = QtWidgets.QPushButton('Next >>')
        self.next_btn.clicked.connect(self.navigate_next)
        self.annotated_lw.currentRowChanged.connect(partial(self.selected_image_changed, 0))
        self.unannotated_lw.currentRowChanged.connect(partial(self.selected_image_changed, 1))
        self.annotated_lb = QtWidgets.QLabel('Annotated')
        self.unannotated_lb = QtWidgets.QLabel('Unannotated')

        self.lw_list: List[QtWidgets.QListWidget] = [self.annotated_lw, self.unannotated_lw]

        layout.addWidget(self.prev_btn)
        layout.addWidget(self.next_btn)
        layout.addWidget(self.annotated_lb)
        layout.addWidget(self.annotated_lw)
        layout.addWidget(self.unannotated_lb)
        layout.addWidget(self.unannotated_lw)

        layout.setContentsMargins(20, 0, 20, 0)
        self.setLayout(layout)
        self.update_list_widgets()
        if self.unannotated_lw.count():
            self.unannotated_lw.setCurrentRow(0)
        elif self.annotated_lw.count():
            self.annotated_lw.setCurrentRow(0)

    def selected_image_changed(self, lw_index: int, row: int):
        if row == -1 or self.updating:
            return
        self.background.annotator.save_annotations()
        self.background.annotator.clear_annotations()
        this_lw = self.lw_list[lw_index]
        img_name = self.image_names[this_lw.currentItem().text()] # Switch back (when item name != item path)
        other_ind = (lw_index + 1) % len(self.lw_list)
        other_lw = self.lw_list[other_ind]
        if other_lw.currentRow() != -1:
            other_lw.clearSelection()
        self.load_image(img_name)

    def update_list_widgets(self):
        self.updating = True
        self.annotated_lw.clear()
        self.unannotated_lw.clear()
        self.image_names = {}
        for img_name, count in self.images_states.items():
            if count is None:
                title = img_name
                self.unannotated_lw.addItem(QtWidgets.QListWidgetItem(title))
                self.image_names[title] = img_name
                if img_name == self.current_image_name:
                    self.unannotated_lw.setCurrentRow(self.unannotated_lw.count() - 1)
                    self.current_lw = self.unannotated_lw
                    self.current_lw_index = 1
            else:
                title = f"{img_name} ({count})"
                self.annotated_lw.addItem(QtWidgets.QListWidgetItem(title))
                self.image_names[title] = img_name
                if img_name == self.current_image_name:
                    self.annotated_lw.setCurrentRow(self.annotated_lw.count() - 1)
                    self.current_lw = self.annotated_lw
                    self.current_lw_index = 0
        self.annotated_lb.setText(f"Annotated ({self.annotated_lw.count()})")
        self.unannotated_lb.setText(f"Unannotated ({self.unannotated_lw.count()})")
        self.update()
        self.updating = False

    def load_image(self, img_name):
        self.background.annotator.open_image(self.background.dir_path / img_name, self.background.images.annotations.get(img_name, []))
        self.current_image_name = img_name
        self.update_list_widgets()

    def other_lw(self, index):
        return self.lw_list[(index + 1) % len(self.lw_list)]

    def navigate_prev(self):
        other_lw = self.other_lw(self.current_lw_index)
        if self.current_lw.currentRow() - 1 < 0 and other_lw.count():
            other_lw.setCurrentRow(other_lw.count() - 1)
        else:
            self.current_lw.setCurrentRow((self.current_lw.currentRow() - 1) % self.current_lw.count())

    def navigate_next(self):
        other_lw = self.other_lw(self.current_lw_index)
        if self.current_lw.currentRow() + 1 >= self.current_lw.count() and other_lw.count():
            other_lw.setCurrentRow(0)
        else:
            self.current_lw.setCurrentRow((self.current_lw.currentRow() + 1) % self.current_lw.count())


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, dir_path: str, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setWindowTitle("Ayolo - Annotating tool for yolo v4 datasets")

        widget = QtWidgets.QWidget(self)
        self.resize(1800, 900)
        self.setCentralWidget(widget)
        self.center_self()

        layout = QtWidgets.QHBoxLayout()

        self.background = Background(dir_path)
        self.annotator = Annotator(self.background)
        self.control_panel = ControlPanel(self.background)
        self.image_browser = ImageBrowser(self.background)

        self.save_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+S"), self)
        self.save_sc.activated.connect(self.control_panel.save_btn.animateClick)
        self.null_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+N"), self)
        self.null_sc.activated.connect(self.control_panel.null_btn.animateClick)
        self.undo_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Z"), self)
        self.undo_sc.activated.connect(self.control_panel.undo_btn.animateClick)
        self.delete_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self)
        self.delete_sc.activated.connect(self.control_panel.delete_btn.animateClick)
        self.clear_sc = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+X"), self)
        self.clear_sc.activated.connect(self.control_panel.save_btn.animateClick)
        self.nav_prev_sc = QtWidgets.QShortcut(QtCore.Qt.Key.Key_PageUp, self)
        self.nav_prev_sc.activated.connect(self.image_browser.prev_btn.animateClick)
        self.nav_next_sc = QtWidgets.QShortcut(QtCore.Qt.Key.Key_PageDown, self)
        self.nav_next_sc.activated.connect(self.image_browser.next_btn.animateClick)

        layout.setContentsMargins(0, 20, 0, 20)

        layout.addWidget(self.image_browser, 2)
        layout.addWidget(self.annotator, 8)
        layout.addWidget(self.control_panel, 2)

        widget.setLayout(layout)
        self.background.control_panel.search.setFocus()
        self.activateWindow()

    def closeEvent(self, event):
        close = QtWidgets.QMessageBox()
        close.setText("Are you sure you want to exit ?")
        close.setWindowTitle("Exit")
        close.setIcon(QtWidgets.QMessageBox.Icon.Question)
        close.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
        close = close.exec()

        if close == QtWidgets.QMessageBox.Yes:
            self.annotator.save_annotations()
            event.accept()
        else:
            event.ignore()

    def center_self(self):
        qtRectangle = self.frameGeometry()
        centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        self.move(qtRectangle.topLeft())

    @classmethod
    def run(cls, dir_path):
        app = QtWidgets.QApplication(sys.argv)

        app.setStyle("Fusion")
        app.setPalette(DarkPalette())
        app.setStyleSheet("QToolTip { color: #ffffff; background-color: grey; border: 1px solid white; }")

        main = cls(dir_path)
        main.show()

        sys.exit(app.exec_())
Widget for annotator section (center) # try: #     last = list(self.current_annotations[-1]) #     last[-1] = clas #     self.current_annotations[-1] = tuple(last) #     self.background.control_panel.update_current_annotations_lw(self.current_annotations) # except IndexError: #     pass # THIS WAS REMOVED SINCE v0.2.0 AS YOU SHOULD SELECT CLASS BEFORE ANNOTATING Widget for control panel section (right) #{clas})") Widget for image browser section (left) # Switch back (when item name != item path) #ffffff; background-color: grey; border: 1px solid white; }")