diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e51700f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,34 @@ +stages: + - test-csv + - test-image_extensions + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +cache: + key: "$CI_JOB_NAME" + paths: + - .cache/pip + +before_script: + - apt-get update + - apt-get install -y python3-pyqt5 xvfb + - python3 -m venv venv + - source venv/bin/activate + - python3 -m pip install --upgrade pip + - pip3 install -r requirements.txt + - pip install pytest pytest-qt + - export PYTHONPATH=$PYTHONPATH:$CI_PROJECT_DIR/app + +test-csv: + stage: test-csv + image: python:3.8 + script: + - xvfb-run -a --server-args="-screen 0 1024x768x24" pytest test/test_generate_csv.py --tb=short -v + +test-image_extensions: + stage: test-image_extensions + image: python:3.8 + script: + - xvfb-run -a --server-args="-screen 0 1024x768x24" pytest test/test_img_paths.py --tb=short -v + diff --git a/app/.gitkeep b/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/app/main.py similarity index 96% rename from main.py rename to app/main.py index 2b16427..0e2ac0d 100644 --- a/main.py +++ b/app/main.py @@ -6,13 +6,14 @@ import numpy as np from PyQt5 import QtWidgets from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QIntValidator, QKeySequence +from PyQt5.QtGui import QPixmap, QIntValidator, QKeySequence, QImageReader from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QCheckBox, QFileDialog, QDesktopWidget, QLineEdit, \ QRadioButton, QShortcut, QScrollArea, QVBoxLayout, QGroupBox, QFormLayout from xlsxwriter.workbook import Workbook -def get_img_paths(dir, extensions=('.jpg', '.png', '.jpeg')): +def get_img_paths(dir, extensions=tuple(['.' + extension.data().decode('utf-8') for + extension in QImageReader.supportedImageFormats()])): ''' :param dir: folder with files :param extensions: tuple with file endings. e.g. ('.jpg', '.png'). Files with these endings will be added to img_paths @@ -129,7 +130,7 @@ def init_ui(self): self.next_button.clicked.connect(self.continue_app) self.next_button.setObjectName("blueButton") - # Erro message + # Error message self.error_message.setGeometry(20, 810, self.width - 20, 20) self.error_message.setAlignment(Qt.AlignCenter) self.error_message.setStyleSheet('color: red; font-weight: bold') @@ -211,7 +212,7 @@ def pick_labels_file(self): self.numLabelsInput.setText(str(len(labels))) self.generate_label_inputs() - # fill the input fileds with loaded labels + # fill the input fields with loaded labels for input, label in zip(self.label_inputs, labels): input.setText(label) @@ -250,6 +251,7 @@ def generate_label_inputs(self): self.groupBox.setLayout(self.formLayout) self.scroll.setWidget(self.groupBox) self.scroll.setWidgetResizable(True) + def centerOnScreen(self): """ Centers the window on the screen. @@ -330,6 +332,7 @@ def __init__(self, labels, input_folder, mode): self.curr_image_headline = QLabel('Current image', self) self.csv_note = QLabel('(csv will be also generated automatically after closing the app)', self) self.csv_generated_message = QLabel(self) + self.german_format_check_box = QCheckBox("Save as German format", self) self.show_next_checkbox = QCheckBox("Automatically show next image when labeled", self) self.generate_xlsx_checkbox = QCheckBox("Also generate .xlsx file", self) @@ -355,6 +358,7 @@ def init_ui(self): # "create xlsx" checkbox self.generate_xlsx_checkbox.setChecked(False) + self.german_format_check_box.setGeometry(self.img_panel_width + 140, 560, 300, 20) self.generate_xlsx_checkbox.setGeometry(self.img_panel_width + 140, 606, 300, 20) # image headline @@ -390,6 +394,8 @@ def init_ui(self): ui_line.setGeometry(20, 98, 1012, 1) ui_line.setStyleSheet('background-color: black') + # init checkbox to generate the csv in german format add sep with replacing the delimiter + # apply custom styles try: styles_path = "./styles.qss" @@ -474,7 +480,7 @@ def set_label(self, label): elif self.mode == 'move': # label was in assigned labels, so I want to remove it from label folder, # but this was the last label, so move the image to input folder. - # Don't remove it, because it it not save anywehre else + # Don't remove it, because it not save anywhere else if img_name not in self.assigned_labels.keys(): shutil.move(os.path.join(self.input_folder, label, img_name), self.input_folder) else: @@ -601,7 +607,12 @@ def generate_csv(self, out_filename): csv_file_path = os.path.join(path_to_save, out_filename) + '.csv' with open(csv_file_path, "w", newline='') as csv_file: - writer = csv.writer(csv_file, delimiter=',') + if(self.german_format_check_box.isChecked()): + delimiter = ';' + else: + delimiter = ',' + + writer = csv.writer(csv_file, delimiter=delimiter) # write header writer.writerow(['img'] + self.labels) @@ -640,7 +651,7 @@ def csv_to_xlsx(self, csv_file_path): def set_button_color(self, filename): """ changes color of button which corresponds to selected label - :filename filename of loaded image: + :filename of loaded image: """ if filename in self.assigned_labels.keys(): diff --git a/test/.gitkeep b/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/test_generate_csv.py b/test/test_generate_csv.py new file mode 100644 index 0000000..3b70cc8 --- /dev/null +++ b/test/test_generate_csv.py @@ -0,0 +1,51 @@ +import pytest +from PyQt5.QtWidgets import QApplication +from app.main import LabelerWindow + +@pytest.fixture +def app(qtbot): + test_app = LabelerWindow(labels=['landschaft', 'menschen'], input_folder='test_folder', mode='csv') + qtbot.addWidget(test_app) + return test_app + +def test_generate_csv_german(app, qtbot, tmpdir): + # Set up the environment for the test + app.german_format_check_box.setChecked(True) + app.assigned_labels = { + 'test1.jpg': ['landschaft'], + 'test2.BMP': ['landschaft'], + 'test3.jpg': ['menschen'], + 'test4.gif': ['menschen'] + } + output_csv = "test_output" + app.generate_csv(output_csv) + + with open(r"test_folder/output/" + output_csv + ".csv") as f: + lines = f.readlines() + + assert lines[0].strip() == 'img;landschaft;menschen' + assert lines[1].strip() == 'test1.jpg;1;0' + assert lines[2].strip() == 'test2.BMP;1;0' + assert lines[3].strip() == 'test3.jpg;0;1' + assert lines[4].strip() == 'test4.gif;0;1' + +def test_generate_csv_no_german_format(app, qtbot, tmpdir): + # Set up the environment for the test + app.german_format_check_box.setChecked(False) + app.assigned_labels = { + 'test1.jpg': ['landschaft'], + 'test2.BMP': ['landschaft'], + 'test3.jpg': ['menschen'], + 'test4.gif': ['menschen'] + } + output_csv = "test_output" + app.generate_csv(output_csv) + + with open(r"test_folder/output/" + output_csv + ".csv") as f: + lines = f.readlines() + + assert lines[0].strip() == 'img,landschaft,menschen' + assert lines[1].strip() == 'test1.jpg,1,0' + assert lines[2].strip() == 'test2.BMP,1,0' + assert lines[3].strip() == 'test3.jpg,0,1' + assert lines[4].strip() == 'test4.gif,0,1' \ No newline at end of file diff --git a/test/test_img_paths.py b/test/test_img_paths.py new file mode 100644 index 0000000..96a10d0 --- /dev/null +++ b/test/test_img_paths.py @@ -0,0 +1,38 @@ +import os +import pytest +from app.main import get_img_paths +from PyQt5.QtGui import QImageReader + +def test_supported_image_formats(tmpdir): + # Get supported image formats from QImageReader + # This will return a tuple of which represent the supported image file extensions (e.g., '.jpg', '.png') + expected_formats = tuple('.' + extension.data().decode('utf-8') for extension in QImageReader.supportedImageFormats()) + + # Create a temporary directory with dummy image files of each supported format + # Using the tmpdir fixture from pytest to create a temporary directory that will be automatically cleaned up + for fmt in expected_formats: + # Construct the file path for a dummy image file with the given format + file_path = os.path.join(tmpdir, f'test_image{fmt}') + # Create an empty file with the specified format + open(file_path, 'a').close() + + # Get image paths using the updated get_img_paths function + # This function should return a list of paths to the image files in the directory that match the supported formats + img_paths = get_img_paths(tmpdir) + + # Ensure all supported image formats are detected + # The number of detected image paths should match the number of supported formats + assert len(img_paths) == len(expected_formats) + # Verify that each detected image path has an extension that is in the expected formats + for path in img_paths: + assert os.path.splitext(path)[1] in expected_formats + +def test_no_images_found(tmpdir): + # Create a temporary directory with no image files + # Using the tmpdir fixture from pytest to create a temporary directory + # This time, we do not create any image files in the directory + img_paths = get_img_paths(tmpdir) + + # Ensure no images are found + # Since no image files were created in the directory, get_img_paths should return an empty list + assert img_paths == [] \ No newline at end of file diff --git a/test_folder/.gitkeep b/test_folder/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test_folder/test1.jpg b/test_folder/test1.jpg new file mode 100644 index 0000000..f04940e Binary files /dev/null and b/test_folder/test1.jpg differ diff --git a/test_folder/test2.BMP b/test_folder/test2.BMP new file mode 100644 index 0000000..3ba9f4a Binary files /dev/null and b/test_folder/test2.BMP differ diff --git a/test_folder/test3.jpg b/test_folder/test3.jpg new file mode 100644 index 0000000..54b4dca Binary files /dev/null and b/test_folder/test3.jpg differ diff --git a/test_folder/test4.gif b/test_folder/test4.gif new file mode 100644 index 0000000..074d234 Binary files /dev/null and b/test_folder/test4.gif differ