diff --git a/.gitignore b/.gitignore index f8b73e7..5130b89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ @@ -137,4 +139,3 @@ dmypy.json # Cython debug symbols cython_debug/ - diff --git a/Bulk Image Tagger qt5.ui b/Bulk Image Tagger qt5.ui new file mode 100644 index 0000000..e74c1b9 --- /dev/null +++ b/Bulk Image Tagger qt5.ui @@ -0,0 +1,204 @@ + + + MainWindow + + + + 0 + 0 + 632 + 447 + + + + + 0 + 0 + + + + Bulk Image Tagger + + + false + + + false + + + + + + 10 + 11 + 611 + 311 + + + + + + + 10 + 370 + 181 + 25 + + + + + Premade Prompts + + + + + example1 + + + + + + + 530 + 370 + 89 + 25 + + + + Save + + + + + + 400 + 370 + 31 + 25 + + + + + + + + + + 360 + 370 + 31 + 25 + + + + + + + + + + 440 + 370 + 89 + 25 + + + + Delete + + + + + + 10 + 330 + 611 + 31 + + + + Enter prompt here + + + + + + + 0 + 0 + 632 + 22 + + + + + File + + + + + + + + + + + + + + false + + + + + Quit + + + Ctrl+Q + + + + + &Save + + + Ctrl+S + + + + + Skip + + + Ctrl+B + + + + + Open Folder + + + Ctrl+O + + + + + &Next + + + Ctrl+N + + + + + &Delete + + + Del + + + + + + diff --git a/README.md b/README.md index 27f9eb6..ca1f3c7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # bulk-image-tagger -Workflow for tagging thousands of images. \ No newline at end of file +_Workflow for tagging thousands of images_ + +**Use** +```bash +pip install PyQt5 opencv-python-headless numpy +./launch.sh +``` + +**Develop** +```bash +sudo apt install qttools5-dev-tools +pip install PyQt5 opencv-python-headless numpy +``` diff --git a/application/app.py b/application/app.py new file mode 100644 index 0000000..a44c1d9 --- /dev/null +++ b/application/app.py @@ -0,0 +1,22 @@ +import sys + +from PyQt5 import QtGui +from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QFileDialog, QMainWindow + +from main_window import Ui_MainWindow + + +class Window(QMainWindow, Ui_MainWindow): + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + + + +if __name__ == "__main__": + app = QApplication(sys.argv) + win = Window() + win.show() + sys.exit(app.exec()) diff --git a/application/main_window.py b/application/main_window.py new file mode 100644 index 0000000..486096f --- /dev/null +++ b/application/main_window.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +import os +from glob import glob +from io import BytesIO + +import cv2 +import numpy as np +from PIL import Image +from PIL.ImageQt import ImageQt +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QFileDialog, QLabel + + +# Form implementation generated from reading ui file 'Bulk Image Tagger qt5.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 + +def get_folder(folder): + glob_pattern = sorted(glob(os.path.join(folder, '*')), key=os.path.getctime) + out = [] + for i in range(len(glob_pattern)): + if os.path.splitext(os.path.basename(glob_pattern[i]))[-1] in ['.jpg', '.jpeg', '.png']: + out.append(glob_pattern[i]) + return out + + +def resizeAndPad(img, size, padColor=0): + h, w = img.shape[:2] + sh, sw = size + + # interpolation method + if h > sh or w > sw: # shrinking image + interp = cv2.INTER_AREA + else: # stretching image + interp = cv2.INTER_CUBIC + + # aspect ratio of image + aspect = w / h # if on Python 2, you might need to cast as a float: float(w)/h + + # compute scaling and pad sizing + if aspect > 1: # horizontal image + new_w = sw + new_h = np.round(new_w / aspect).astype(int) + pad_vert = (sh - new_h) / 2 + pad_top, pad_bot = np.floor(pad_vert).astype(int), np.ceil(pad_vert).astype(int) + pad_left, pad_right = 0, 0 + elif aspect < 1: # vertical image + new_h = sh + new_w = np.round(new_h * aspect).astype(int) + pad_horz = (sw - new_w) / 2 + pad_left, pad_right = np.floor(pad_horz).astype(int), np.ceil(pad_horz).astype(int) + pad_top, pad_bot = 0, 0 + else: # square image + new_h, new_w = sh, sw + pad_left, pad_right, pad_top, pad_bot = 0, 0, 0, 0 + + # set pad color + if len(img.shape) == 3 and not isinstance(padColor, (list, tuple, np.ndarray)): # color image but only one color provided + padColor = [padColor] * 3 + + # scale and pad + scaled_img = cv2.resize(img, (new_w, new_h), interpolation=interp) + scaled_img = cv2.copyMakeBorder(scaled_img, pad_top, pad_bot, pad_left, pad_right, borderType=cv2.BORDER_CONSTANT, value=padColor) + + return scaled_img + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + self.folder_contents = None + self.folder_contents_i = None + + MainWindow.setObjectName("MainWindow") + MainWindow.setFixedSize(632, 447) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) + MainWindow.setSizePolicy(sizePolicy) + MainWindow.setAnimated(False) + MainWindow.setDocumentMode(False) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + # self.graphicsView = QtWidgets.QGraphicsView(self.centralwidget) + # self.graphicsView.setGeometry(QtCore.QRect(10, 11, 611, 311)) + # self.graphicsView.setObjectName("graphicsView") + + self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit.setGeometry(QtCore.QRect(10, 330, 611, 31)) + self.lineEdit.setObjectName("lineEdit") + self.lineEdit.returnPressed.connect(self.write_txtbox) + self.lineEdit.setEnabled(False) + + self.comboBox = QtWidgets.QComboBox(self.centralwidget) + self.comboBox.setGeometry(QtCore.QRect(10, 370, 181, 25)) + self.comboBox.setObjectName("comboBox") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.setEnabled(False) + + self.save_button = QtWidgets.QPushButton(self.centralwidget) + self.save_button.setGeometry(QtCore.QRect(530, 370, 89, 25)) + self.save_button.setObjectName("save_button") + self.save_button.clicked.connect(self.write_txtbox) + self.save_button.setEnabled(False) + + self.next_button = QtWidgets.QPushButton(self.centralwidget) + self.next_button.setGeometry(QtCore.QRect(400, 370, 31, 25)) + self.next_button.setObjectName("next_button") + self.next_button.clicked.connect(self.next_image) + self.next_button.setEnabled(False) + + self.last_button = QtWidgets.QPushButton(self.centralwidget) + self.last_button.setGeometry(QtCore.QRect(360, 370, 31, 25)) + self.last_button.setObjectName("last_button") + self.last_button.clicked.connect(self.prev_image) + self.last_button.setEnabled(False) + + self.delete_button = QtWidgets.QPushButton(self.centralwidget) + self.delete_button.setGeometry(QtCore.QRect(440, 370, 89, 25)) + self.delete_button.setObjectName("delete_button") + self.delete_button.clicked.connect(self.delete_image) + self.delete_button.setEnabled(False) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 632, 22)) + self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(self.menubar) + self.actionQuit = QtWidgets.QAction(MainWindow) + MainWindow.setMenuBar(self.menubar) + self.statusBar = QtWidgets.QStatusBar(MainWindow) + self.statusBar.setSizeGripEnabled(False) + self.statusBar.setObjectName("statusBar") + MainWindow.setStatusBar(self.statusBar) + self.actionQuit.setObjectName("actionQuit") + self.actionQuit.triggered.connect(self.close) + + self.actionSave = QtWidgets.QAction(MainWindow) + self.actionSave.setObjectName("actionSave") + self.actionSave.triggered.connect(self.write_txtbox) + + self.actionNext = QtWidgets.QAction(MainWindow) + self.actionNext.setObjectName("actionNext") + self.actionNext.triggered.connect(self.prev_image) + + self.actionOpen = QtWidgets.QAction(MainWindow) + self.actionOpen.setObjectName("actionOpen") + self.actionOpen.triggered.connect(self.folder_open) + + self.actionNext = QtWidgets.QAction(MainWindow) + self.actionNext.setObjectName("actionNext") + self.actionNext.triggered.connect(self.next_image) + + self.actionDelete = QtWidgets.QAction(MainWindow) + self.actionDelete.setObjectName("actionDelete") + self.actionDelete.triggered.connect(self.delete_image) + + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionQuit) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionNext) + self.menuFile.addAction(self.actionNext) + self.menuFile.addAction(self.actionDelete) + self.menuFile.addAction(self.actionSave) + self.menubar.addAction(self.menuFile.menuAction()) + + self.label = QLabel(self) + self.label.setGeometry(QtCore.QRect(10, 30, 611, 311)) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Bulk Image Tagger")) + self.lineEdit.setPlaceholderText(_translate("MainWindow", "Enter prompt here")) + self.comboBox.setItemText(0, _translate("MainWindow", "Premade Prompts")) + self.comboBox.setItemText(1, _translate("MainWindow", "example1")) + self.save_button.setText(_translate("MainWindow", "Save")) + self.next_button.setText(_translate("MainWindow", "→")) + self.last_button.setText(_translate("MainWindow", "←")) + self.delete_button.setText(_translate("MainWindow", "Delete")) + self.menuFile.setTitle(_translate("MainWindow", "File")) + self.actionQuit.setText(_translate("MainWindow", "Quit")) + self.actionQuit.setShortcut(_translate("MainWindow", "Ctrl+Q")) + self.actionSave.setText(_translate("MainWindow", "&Save")) + self.actionSave.setShortcut(_translate("MainWindow", "Ctrl+S")) + self.actionNext.setText(_translate("MainWindow", "Previous")) + self.actionNext.setShortcut(_translate("MainWindow", "Ctrl+B")) + self.actionOpen.setText(_translate("MainWindow", "Open Folder")) + self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O")) + self.actionNext.setText(_translate("MainWindow", "&Next")) + self.actionNext.setShortcut(_translate("MainWindow", "Ctrl+N")) + self.actionDelete.setText(_translate("MainWindow", "&Delete")) + self.actionDelete.setShortcut(_translate("MainWindow", "Ctrl+D")) + + def folder_open(self): + self.user_dir = QFileDialog.getExistingDirectory( + self, "Open Directory", + "", + QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks, + ) + self.folder_contents = get_folder(self.user_dir) + self.folder_contents_i = 0 + self.last_button.setEnabled(False) + if len(self.folder_contents) > 0: + self.load_image(self.folder_contents[self.folder_contents_i]) + self.enable(True) + else: + self.statusBar.showMessage('No images found!') + + def load_image(self, image_path): + self.lineEdit.clear() + if os.path.isfile(image_path): + im = cv2.imread(image_path) # square image + im_resize = resizeAndPad(im, (311, 311), 311) + is_success, im_buf_arr = cv2.imencode(".png", im_resize) + im = Image.open(BytesIO(im_buf_arr.tobytes())) + im.tobytes('raw', 'RGB') + qim = ImageQt(im) + pixmap = QPixmap.fromImage(qim) + self.label.setPixmap(pixmap) + self.label.show() + self.statusBar.showMessage(os.path.split(self.folder_contents[self.folder_contents_i])[-1]) + + def next_image(self): + if self.folder_contents is not None: + if self.folder_contents_i + 1 < len(self.folder_contents): + self.folder_contents_i += 1 + self.load_image(self.folder_contents[self.folder_contents_i]) + else: + self.next_button.setEnabled(False) + if self.folder_contents_i - 1 < len(self.folder_contents): + self.last_button.setEnabled(True) + + def prev_image(self): + if self.folder_contents is not None: + if self.folder_contents_i - 1 >= 0: + self.folder_contents_i -= 1 + self.load_image(self.folder_contents[self.folder_contents_i]) + elif self.folder_contents_i - 1 < len(self.folder_contents): + self.last_button.setEnabled(False) + if self.folder_contents_i + 1 < len(self.folder_contents): + self.next_button.setEnabled(True) + + def write_txtbox(self): + if self.folder_contents is not None: + textbox_content = self.lineEdit.text() + if len(textbox_content) > 0: + f = open(f'{self.folder_contents[self.folder_contents_i]}.txt', 'w') + f.write(textbox_content) + f.close() + self.lineEdit.clear() + self.next_image() + + def delete_image(self): + if self.folder_contents is not None and len(self.folder_contents) > 0: + self.label.hide() + try: + os.remove(self.folder_contents[self.folder_contents_i]) + except: + print(self.folder_contents, self.folder_contents_i) + if os.path.exists(f'{self.folder_contents[self.folder_contents_i]}.txt'): + os.remove(f'{self.folder_contents[self.folder_contents_i]}.txt') + self.statusBar.showMessage(f'Deleted {os.path.split(self.folder_contents[self.folder_contents_i])[-1]}') + del self.folder_contents[self.folder_contents_i] + self.folder_contents_i -= 1 + if len(self.folder_contents) > 0: + self.next_image() + else: + self.enable(False) + + def enable(self, action): + self.save_button.setEnabled(action) + self.next_button.setEnabled(action) + self.last_button.setEnabled(action) + self.delete_button.setEnabled(action) + self.lineEdit.setEnabled(action) + self.comboBox.setEnabled(action) diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..a5db9f8 --- /dev/null +++ b/launch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 application/app.py \ No newline at end of file diff --git a/main_window.py b/main_window.py new file mode 100644 index 0000000..2350f5d --- /dev/null +++ b/main_window.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'Bulk Image Tagger qt5.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(632, 447) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) + MainWindow.setSizePolicy(sizePolicy) + MainWindow.setAnimated(False) + MainWindow.setDocumentMode(False) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.graphicsView = QtWidgets.QGraphicsView(self.centralwidget) + self.graphicsView.setGeometry(QtCore.QRect(10, 11, 611, 311)) + self.graphicsView.setObjectName("graphicsView") + self.comboBox = QtWidgets.QComboBox(self.centralwidget) + self.comboBox.setGeometry(QtCore.QRect(10, 370, 181, 25)) + self.comboBox.setObjectName("comboBox") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.pushButton = QtWidgets.QPushButton(self.centralwidget) + self.pushButton.setGeometry(QtCore.QRect(530, 370, 89, 25)) + self.pushButton.setObjectName("pushButton") + self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) + self.pushButton_2.setGeometry(QtCore.QRect(400, 370, 31, 25)) + self.pushButton_2.setObjectName("pushButton_2") + self.pushButton_3 = QtWidgets.QPushButton(self.centralwidget) + self.pushButton_3.setGeometry(QtCore.QRect(360, 370, 31, 25)) + self.pushButton_3.setObjectName("pushButton_3") + self.pushButton_4 = QtWidgets.QPushButton(self.centralwidget) + self.pushButton_4.setGeometry(QtCore.QRect(440, 370, 89, 25)) + self.pushButton_4.setObjectName("pushButton_4") + self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit.setGeometry(QtCore.QRect(10, 330, 611, 31)) + self.lineEdit.setObjectName("lineEdit") + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 632, 22)) + self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName("menuFile") + MainWindow.setMenuBar(self.menubar) + self.statusBar = QtWidgets.QStatusBar(MainWindow) + self.statusBar.setSizeGripEnabled(False) + self.statusBar.setObjectName("statusBar") + MainWindow.setStatusBar(self.statusBar) + self.actionQuit = QtWidgets.QAction(MainWindow) + self.actionQuit.setObjectName("actionQuit") + self.actionSave = QtWidgets.QAction(MainWindow) + self.actionSave.setObjectName("actionSave") + self.actionSkip = QtWidgets.QAction(MainWindow) + self.actionSkip.setObjectName("actionSkip") + self.actionOpen = QtWidgets.QAction(MainWindow) + self.actionOpen.setObjectName("actionOpen") + self.actionNext = QtWidgets.QAction(MainWindow) + self.actionNext.setObjectName("actionNext") + self.actionDelete = QtWidgets.QAction(MainWindow) + self.actionDelete.setObjectName("actionDelete") + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionQuit) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionSkip) + self.menuFile.addAction(self.actionNext) + self.menuFile.addAction(self.actionDelete) + self.menuFile.addAction(self.actionSave) + self.menubar.addAction(self.menuFile.menuAction()) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Bulk Image Tagger")) + self.comboBox.setItemText(0, _translate("MainWindow", "Premade Prompts")) + self.comboBox.setItemText(1, _translate("MainWindow", "example1")) + self.pushButton.setText(_translate("MainWindow", "Save")) + self.pushButton_2.setText(_translate("MainWindow", "→")) + self.pushButton_3.setText(_translate("MainWindow", "←")) + self.pushButton_4.setText(_translate("MainWindow", "Delete")) + self.lineEdit.setPlaceholderText(_translate("MainWindow", "Enter prompt here")) + self.menuFile.setTitle(_translate("MainWindow", "File")) + self.actionQuit.setText(_translate("MainWindow", "Quit")) + self.actionQuit.setShortcut(_translate("MainWindow", "Ctrl+Q")) + self.actionSave.setText(_translate("MainWindow", "&Save")) + self.actionSave.setShortcut(_translate("MainWindow", "Ctrl+S")) + self.actionSkip.setText(_translate("MainWindow", "Skip")) + self.actionSkip.setShortcut(_translate("MainWindow", "Ctrl+B")) + self.actionOpen.setText(_translate("MainWindow", "Open Folder")) + self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O")) + self.actionNext.setText(_translate("MainWindow", "&Next")) + self.actionNext.setShortcut(_translate("MainWindow", "Ctrl+N")) + self.actionDelete.setText(_translate("MainWindow", "&Delete")) + self.actionDelete.setShortcut(_translate("MainWindow", "Ctrl+D"))