From 1cb21a41a90d0ff646ae62088754097915f58d81 Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sun, 30 Oct 2022 14:49:19 +0800 Subject: [PATCH 1/9] refactor: add todo list and comments in labelImg.py --- .gitignore | 3 +++ labelImg.py | 22 ++++++++++++++++++++++ tests/.gitignore | 1 + 3 files changed, 26 insertions(+) diff --git a/.gitignore b/.gitignore index 63754ecf1..ca0c9bc5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ resources/icons/.DS_Store resources.py labelImg.egg-info* +zachary_notes.txt *.pyc .*.swp build/ dist/ +uml/ +venv/ tags cscope* diff --git a/labelImg.py b/labelImg.py index efd8a2976..fcdbe5599 100755 --- a/labelImg.py +++ b/labelImg.py @@ -242,6 +242,8 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau save = action(get_str('save'), self.save_file, 'Ctrl+S', 'save', get_str('saveDetail'), enabled=False) + ######### refactor here ######### + # get_format_meta return a string that represent the file type def get_format_meta(format): """ returns a tuple containing (title, icon_name) of the selected format @@ -253,11 +255,14 @@ def get_format_meta(format): elif format == LabelFileFormat.CREATE_ML: return '&CreateML', 'format_createml' + # save_format uses the attribute label_file_format(LabelFileFormat object) + # convert to string then utilize change format save_format = action(get_format_meta(self.label_file_format)[0], self.change_format, 'Ctrl+Y', get_format_meta(self.label_file_format)[1], get_str('changeSaveFormat'), enabled=True) + # save_as action do the save operation? save_as = action(get_str('saveAs'), self.save_file_as, 'Ctrl+Shift+S', 'save-as', get_str('saveAsDetail'), enabled=False) @@ -548,7 +553,11 @@ def keyPressEvent(self, event): # Draw rectangle if Ctrl is pressed self.canvas.set_drawing_shape_to_square(True) + ########### refactor here ############ # Support Functions # + # set_format is called whenever the fileformat has changed + # it will set the UI and modify labelfileformat attribute + # last, change the suffix of output label def set_format(self, save_format): if save_format == FORMAT_PASCALVOC: self.actions.save_format.setText(FORMAT_PASCALVOC) @@ -568,6 +577,10 @@ def set_format(self, save_format): self.label_file_format = LabelFileFormat.CREATE_ML LabelFile.suffix = JSON_EXT + + # change_format is called when the fileformat buttom is toggled + # it change the LabelFileFormat attribute + # then call the set_format function def change_format(self): if self.label_file_format == LabelFileFormat.PASCAL_VOC: self.set_format(FORMAT_YOLO) @@ -876,6 +889,9 @@ def update_combo_box(self): self.combo_box.update_items(unique_text_list) + ############# refactor here ############ + # here reference the LabelFile object + # using its saving method to save virtual label into file def save_labels(self, annotation_file_path): annotation_file_path = ustr(annotation_file_path) if self.label_file is None: @@ -892,6 +908,7 @@ def format_shape(s): shapes = [format_shape(shape) for shape in self.canvas.shapes] # Can add different annotation formats here + ####### todo: move suffix operation into LabelFile object try: if self.label_file_format == LabelFileFormat.PASCAL_VOC: if annotation_file_path[-4:].lower() != ".xml": @@ -1177,6 +1194,9 @@ def counter_str(self): """ return '[{} / {}]'.format(self.cur_img_idx + 1, self.img_count) + ##### refactor here ###### + # here using the internal method of mainwindow + # to reference to corresponding reader then show the virtual labels in display def show_bounding_box_from_annotation_file(self, file_path): if self.default_save_dir is not None: basename = os.path.basename(os.path.splitext(file_path)[0]) @@ -1616,6 +1636,8 @@ def load_predefined_classes(self, predef_classes_file): else: self.label_hist.append(line) + ######### refactor here ############# + #### todo: move loading methods into LabelFile object def load_pascal_xml_by_filename(self, xml_path): if self.file_path is None: return diff --git a/tests/.gitignore b/tests/.gitignore index a6535f35d..9e34968eb 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ test.xml +tests.json From 9197482b50aef9708c591201e7764679427771b4 Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sun, 30 Oct 2022 21:56:33 +0800 Subject: [PATCH 2/9] refactor: solve long if-else statements in save_labels() --- labelImg.py | 33 ++++++------------ libs/create_ml_io.py | 2 +- libs/labelFile.py | 81 +++++++++++++++---------------------------- libs/pascal_voc_io.py | 2 +- tests/test_io.py | 2 +- zachary_dev_log.txt | 27 +++++++++++++++ 6 files changed, 67 insertions(+), 80 deletions(-) create mode 100644 zachary_dev_log.txt diff --git a/labelImg.py b/labelImg.py index fcdbe5599..8bd75ea2a 100755 --- a/labelImg.py +++ b/labelImg.py @@ -243,7 +243,7 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau 'Ctrl+S', 'save', get_str('saveDetail'), enabled=False) ######### refactor here ######### - # get_format_meta return a string that represent the file type + # get_format_meta get the path of format icon and text def get_format_meta(format): """ returns a tuple containing (title, icon_name) of the selected format @@ -256,13 +256,13 @@ def get_format_meta(format): return '&CreateML', 'format_createml' # save_format uses the attribute label_file_format(LabelFileFormat object) - # convert to string then utilize change format + # convert to string then utilize change format to toggle fileformat button save_format = action(get_format_meta(self.label_file_format)[0], self.change_format, 'Ctrl+Y', get_format_meta(self.label_file_format)[1], get_str('changeSaveFormat'), enabled=True) - # save_as action do the save operation? + # save_as acturally saved the virtual label into file save_as = action(get_str('saveAs'), self.save_file_as, 'Ctrl+Shift+S', 'save-as', get_str('saveAsDetail'), enabled=False) @@ -890,13 +890,16 @@ def update_combo_box(self): self.combo_box.update_items(unique_text_list) ############# refactor here ############ - # here reference the LabelFile object - # using its saving method to save virtual label into file + #! todo: refactor saving format if else statements + #! status: done + #! modified files: yolo/createml/pascal_io.py, LabelFile.py def save_labels(self, annotation_file_path): annotation_file_path = ustr(annotation_file_path) if self.label_file is None: self.label_file = LabelFile() self.label_file.verified = self.canvas.verified + # syncronous LabelFileFormat in mainwindow and label LabelFile object + self.label_file.label_file_format = self.label_file_format def format_shape(s): return dict(label=s.label, @@ -908,26 +911,10 @@ def format_shape(s): shapes = [format_shape(shape) for shape in self.canvas.shapes] # Can add different annotation formats here - ####### todo: move suffix operation into LabelFile object try: - if self.label_file_format == LabelFileFormat.PASCAL_VOC: - if annotation_file_path[-4:].lower() != ".xml": - annotation_file_path += XML_EXT - self.label_file.save_pascal_voc_format(annotation_file_path, shapes, self.file_path, self.image_data, - self.line_color.getRgb(), self.fill_color.getRgb()) - elif self.label_file_format == LabelFileFormat.YOLO: - if annotation_file_path[-4:].lower() != ".txt": - annotation_file_path += TXT_EXT - self.label_file.save_yolo_format(annotation_file_path, shapes, self.file_path, self.image_data, self.label_hist, - self.line_color.getRgb(), self.fill_color.getRgb()) - elif self.label_file_format == LabelFileFormat.CREATE_ML: - if annotation_file_path[-5:].lower() != ".json": - annotation_file_path += JSON_EXT - self.label_file.save_create_ml_format(annotation_file_path, shapes, self.file_path, self.image_data, + # use IOMAP to select writer + self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data, self.label_hist, self.line_color.getRgb(), self.fill_color.getRgb()) - else: - self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data, - self.line_color.getRgb(), self.fill_color.getRgb()) print('Image:{0} -> Annotation:{1}'.format(self.file_path, annotation_file_path)) return True except LabelFileError as e: diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index 3aca8d676..ad97c076e 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -22,7 +22,7 @@ def __init__(self, folder_name, filename, img_size, shapes, output_file, databas self.shapes = shapes self.output_file = output_file - def write(self): + def save(self, target_file=None, class_list=None): if os.path.isfile(self.output_file): with open(self.output_file, "r") as file: input_data = file.read() diff --git a/libs/labelFile.py b/libs/labelFile.py index 185570bcb..a511305f5 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -14,81 +14,55 @@ from libs.pascal_voc_io import XML_EXT from libs.yolo_io import YOLOWriter +# Mapping label format with corresponding writer, reader and suffix +IOMAP = { + LabelFileFormat.PASCAL_VOC: + { + suffix: ".xml" + Reader: None + Writer: PascalVocWriter + } + LabelFileFormat.YOLO: + { + suffix: ".txt" + Reader: None + Writer: YOLOWriter + } + LabelFileFormat.CREATE_ML: + { + suffix: "json" + Reader: None + Writer: CreateMLWriter + } +} class LabelFileFormat(Enum): PASCAL_VOC = 1 YOLO = 2 CREATE_ML = 3 - class LabelFileError(Exception): pass - class LabelFile(object): # It might be changed as window creates. By default, using XML ext # suffix = '.lif' suffix = XML_EXT def __init__(self, filename=None): + self.label_file_format = LabelFileFormat.PASCAL_VOC self.shapes = () self.image_path = None self.image_data = None self.verified = False - def save_create_ml_format(self, filename, shapes, image_path, image_data, class_list, line_color=None, fill_color=None, database_src=None): - img_folder_name = os.path.basename(os.path.dirname(image_path)) - img_file_name = os.path.basename(image_path) - - image = QImage() - image.load(image_path) - image_shape = [image.height(), image.width(), - 1 if image.isGrayscale() else 3] - writer = CreateMLWriter(img_folder_name, img_file_name, - image_shape, shapes, filename, local_img_path=image_path) - writer.verified = self.verified - writer.write() - return - - - def save_pascal_voc_format(self, filename, shapes, image_path, image_data, - line_color=None, fill_color=None, database_src=None): - img_folder_path = os.path.dirname(image_path) - img_folder_name = os.path.split(img_folder_path)[-1] - img_file_name = os.path.basename(image_path) - # imgFileNameWithoutExt = os.path.splitext(img_file_name)[0] - # Read from file path because self.imageData might be empty if saving to - # Pascal format - if isinstance(image_data, QImage): - image = image_data - else: - image = QImage() - image.load(image_path) - image_shape = [image.height(), image.width(), - 1 if image.isGrayscale() else 3] - writer = PascalVocWriter(img_folder_name, img_file_name, - image_shape, local_img_path=image_path) - writer.verified = self.verified - - for shape in shapes: - points = shape['points'] - label = shape['label'] - # Add Chris - difficult = int(shape['difficult']) - bnd_box = LabelFile.convert_points_to_bnd_box(points) - writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) - - writer.save(target_file=filename) - return - - def save_yolo_format(self, filename, shapes, image_path, image_data, class_list, + def save(self, filename, shapes, image_path, image_data, class_list, line_color=None, fill_color=None, database_src=None): + if filename[-4:].lower() != IOMAP[self.label_file_format][suffix]: + filename += IOMAP[self.label_file_format][EXT] img_folder_path = os.path.dirname(image_path) img_folder_name = os.path.split(img_folder_path)[-1] img_file_name = os.path.basename(image_path) - # imgFileNameWithoutExt = os.path.splitext(img_file_name)[0] - # Read from file path because self.imageData might be empty if saving to - # Pascal format if isinstance(image_data, QImage): image = image_data else: @@ -96,14 +70,13 @@ def save_yolo_format(self, filename, shapes, image_path, image_data, class_list, image.load(image_path) image_shape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = YOLOWriter(img_folder_name, img_file_name, - image_shape, local_img_path=image_path) + writer = IOMAP[self.label_file_format][Writer](img_folder_name, img_file_name, + image_shape, shapes, filename, local_img_path=image_path) writer.verified = self.verified for shape in shapes: points = shape['points'] label = shape['label'] - # Add Chris difficult = int(shape['difficult']) bnd_box = LabelFile.convert_points_to_bnd_box(points) writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index d8f7d690b..4c23e2814 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -109,7 +109,7 @@ def append_objects(self, top): y_max = SubElement(bnd_box, 'ymax') y_max.text = str(each_object['ymax']) - def save(self, target_file=None): + def save(self, target_file=None, class_list=None): root = self.gen_xml() self.append_objects(root) out_file = None diff --git a/tests/test_io.py b/tests/test_io.py index 7bc31b3af..114dd9d7b 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -52,7 +52,7 @@ def test_a_write(self): local_img_path='tests/test.512.512.bmp') writer.verified = True - writer.write() + writer.save() # check written json with open(output_file, "r") as file: diff --git a/zachary_dev_log.txt b/zachary_dev_log.txt new file mode 100644 index 000000000..2f1791c7f --- /dev/null +++ b/zachary_dev_log.txt @@ -0,0 +1,27 @@ +Todo: + refactor file reader, file writer, labelfile class + +2022/10/30 morning: + In labelImg.py: + add comments to note refactoring dependencies + +2022/10/30 afternoon: + In labelImg.py, def save_labels(): + code smell: + the function originally uses multiple if else statements to check the label format, + and then call the corresponding method in LabelFile object to write + + solving: + making a IOMAP in LabelFile.py + whenever a label needs to be save, map the label format to the corresponding reader object + so we have to allign the init method of all writers + also the save() method in each writers + for future extentions, writers must follow the format + + result: + unit test passed + + futher work: + add interface for writer that restrict the save() method and init() method + + From 17ce79cb828fef602f7469f384d5753b7bea839b Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Mon, 31 Oct 2022 23:21:08 +0800 Subject: [PATCH 3/9] refactor: refactor LabelFileFormat object in labelFile.py --- labelImg.py | 76 ++++++++++++--------------------------- libs/create_ml_io.py | 2 +- libs/labelFile.py | 85 ++++++++++++++++++++++++-------------------- zachary_dev_log.txt | 31 +++++++++++++--- 4 files changed, 97 insertions(+), 97 deletions(-) diff --git a/labelImg.py b/labelImg.py index 8bd75ea2a..79ba2017e 100755 --- a/labelImg.py +++ b/labelImg.py @@ -37,7 +37,7 @@ from libs.lightWidget import LightWidget from libs.labelDialog import LabelDialog from libs.colorDialog import ColorDialog -from libs.labelFile import LabelFile, LabelFileError, LabelFileFormat +from libs.labelFile import LabelFile, LabelFileError, PascalVoc, Yolo, CreateML #! todo: extract labelfileformat from libs.toolBar import ToolBar from libs.pascal_voc_io import PascalVocReader from libs.pascal_voc_io import XML_EXT @@ -90,7 +90,7 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau # Save as Pascal voc xml self.default_save_dir = default_save_dir - self.label_file_format = settings.get(SETTING_LABEL_FILE_FORMAT, LabelFileFormat.PASCAL_VOC) + self.label_file_format = settings.get(SETTING_LABEL_FILE_FORMAT, PascalVoc) # For loading all image under a directory self.m_img_list = [] @@ -243,23 +243,11 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau 'Ctrl+S', 'save', get_str('saveDetail'), enabled=False) ######### refactor here ######### - # get_format_meta get the path of format icon and text - def get_format_meta(format): - """ - returns a tuple containing (title, icon_name) of the selected format - """ - if format == LabelFileFormat.PASCAL_VOC: - return '&PascalVOC', 'format_voc' - elif format == LabelFileFormat.YOLO: - return '&YOLO', 'format_yolo' - elif format == LabelFileFormat.CREATE_ML: - return '&CreateML', 'format_createml' - - # save_format uses the attribute label_file_format(LabelFileFormat object) - # convert to string then utilize change format to toggle fileformat button - save_format = action(get_format_meta(self.label_file_format)[0], + #! todo: link labelformat with labelfile to replace IOmapping + #! status: done + save_format = action(self.label_file_format.meta[0], self.change_format, 'Ctrl+Y', - get_format_meta(self.label_file_format)[1], + self.label_file_format.meta[1], get_str('changeSaveFormat'), enabled=True) # save_as acturally saved the virtual label into file @@ -555,39 +543,21 @@ def keyPressEvent(self, event): ########### refactor here ############ # Support Functions # - # set_format is called whenever the fileformat has changed - # it will set the UI and modify labelfileformat attribute - # last, change the suffix of output label + #! todo: refactor LabelFileFormat object for format switching and format setting + #! status: done + #! futher work: remove if else statements in change_format() def set_format(self, save_format): - if save_format == FORMAT_PASCALVOC: - self.actions.save_format.setText(FORMAT_PASCALVOC) - self.actions.save_format.setIcon(new_icon("format_voc")) - self.label_file_format = LabelFileFormat.PASCAL_VOC - LabelFile.suffix = XML_EXT - - elif save_format == FORMAT_YOLO: - self.actions.save_format.setText(FORMAT_YOLO) - self.actions.save_format.setIcon(new_icon("format_yolo")) - self.label_file_format = LabelFileFormat.YOLO - LabelFile.suffix = TXT_EXT - - elif save_format == FORMAT_CREATEML: - self.actions.save_format.setText(FORMAT_CREATEML) - self.actions.save_format.setIcon(new_icon("format_createml")) - self.label_file_format = LabelFileFormat.CREATE_ML - LabelFile.suffix = JSON_EXT - + self.actions.save_format.setText(save_format.text) + self.actions.save_format.setIcon(new_icon(save_format.meta[1])) + self.label_file_format = save_format - # change_format is called when the fileformat buttom is toggled - # it change the LabelFileFormat attribute - # then call the set_format function def change_format(self): - if self.label_file_format == LabelFileFormat.PASCAL_VOC: - self.set_format(FORMAT_YOLO) - elif self.label_file_format == LabelFileFormat.YOLO: - self.set_format(FORMAT_CREATEML) - elif self.label_file_format == LabelFileFormat.CREATE_ML: - self.set_format(FORMAT_PASCALVOC) + if self.label_file_format == PascalVoc: + self.set_format(Yolo) + elif self.label_file_format == Yolo: + self.set_format(CreateML) + elif self.label_file_format == CreateML: + self.set_format(PascalVoc) else: raise ValueError('Unknown label file format.') self.set_dirty() @@ -1328,7 +1298,7 @@ def open_annotation_dialog(self, _value=False): path = os.path.dirname(ustr(self.file_path))\ if self.file_path else '.' - if self.label_file_format == LabelFileFormat.PASCAL_VOC: + if self.label_file_format == PascalVoc: filters = "Open Annotation XML file (%s)" % ' '.join(['*.xml']) filename = ustr(QFileDialog.getOpenFileName(self, '%s - Choose a xml file' % __appname__, path, filters)) if filename: @@ -1336,7 +1306,7 @@ def open_annotation_dialog(self, _value=False): filename = filename[0] self.load_pascal_xml_by_filename(filename) - elif self.label_file_format == LabelFileFormat.CREATE_ML: + elif self.label_file_format == CreateML: filters = "Open Annotation JSON file (%s)" % ' '.join(['*.json']) filename = ustr(QFileDialog.getOpenFileName(self, '%s - Choose a json file' % __appname__, path, filters)) @@ -1462,7 +1432,7 @@ def open_file(self, _value=False): return path = os.path.dirname(ustr(self.file_path)) if self.file_path else '.' formats = ['*.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()] - filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.suffix]) + filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.label_file_format.suffix]) filename,_ = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters) if filename: if isinstance(filename, (tuple, list)): @@ -1492,10 +1462,10 @@ def save_file_as(self, _value=False): def save_file_dialog(self, remove_ext=True): caption = '%s - Choose File' % __appname__ - filters = 'File (*%s)' % LabelFile.suffix + filters = 'File (*%s)' % LabelFile.label_file_format.suffix open_dialog_path = self.current_path() dlg = QFileDialog(self, caption, open_dialog_path, filters) - dlg.setDefaultSuffix(LabelFile.suffix[1:]) + dlg.setDefaultSuffix(LabelFile.label_file_format.suffix[1:]) dlg.setAcceptMode(QFileDialog.AcceptSave) filename_without_extension = os.path.splitext(self.file_path)[0] dlg.selectFile(filename_without_extension) diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index ad97c076e..8b1caf234 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -6,7 +6,7 @@ from libs.constants import DEFAULT_ENCODING import os -JSON_EXT = '.json' +JSON_EXT = 'json' ENCODE_METHOD = DEFAULT_ENCODING diff --git a/libs/labelFile.py b/libs/labelFile.py index a511305f5..e17a41be3 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -9,48 +9,55 @@ import os.path from enum import Enum -from libs.create_ml_io import CreateMLWriter -from libs.pascal_voc_io import PascalVocWriter -from libs.pascal_voc_io import XML_EXT -from libs.yolo_io import YOLOWriter - -# Mapping label format with corresponding writer, reader and suffix -IOMAP = { - LabelFileFormat.PASCAL_VOC: - { - suffix: ".xml" - Reader: None - Writer: PascalVocWriter - } - LabelFileFormat.YOLO: - { - suffix: ".txt" - Reader: None - Writer: YOLOWriter - } - LabelFileFormat.CREATE_ML: - { - suffix: "json" - Reader: None - Writer: CreateMLWriter - } -} - -class LabelFileFormat(Enum): - PASCAL_VOC = 1 - YOLO = 2 - CREATE_ML = 3 +#! todo: make abstract class for reader and writer +from libs.create_ml_io import CreateMLReader, CreateMLWriter, JSON_EXT +from libs.pascal_voc_io import PascalVocReader, PascalVocWriter, XML_EXT +from libs.yolo_io import YoloReader, YOLOWriter, TXT_EXT + +import abc + +# create a abstract class for label format type +class LabelFileFormat(abc.ABC): + reader = None + writer = None + suffix = None + + text = None + icon = None + meta = None + + +class PascalVoc(LabelFileFormat): + reader = PascalVocReader + writer = PascalVocWriter + suffix = XML_EXT + + text = 'PascalVOC' + meta = ['&PascalVOC', 'format_voc'] + +class Yolo(LabelFileFormat): + reader = YoloReader + writer = YOLOWriter + suffix = TXT_EXT + + text = 'Yolo' + meta = ['&yolo', 'format_yolo'] + +class CreateML(LabelFileFormat): + reader = CreateMLReader + writer = CreateMLWriter + suffix = JSON_EXT + + text = 'CreateML' + meta = ['&CreateML', 'format_createml'] class LabelFileError(Exception): pass class LabelFile(object): - # It might be changed as window creates. By default, using XML ext - # suffix = '.lif' - suffix = XML_EXT def __init__(self, filename=None): - self.label_file_format = LabelFileFormat.PASCAL_VOC + self.label_file_format = PascalVoc self.shapes = () self.image_path = None self.image_data = None @@ -58,8 +65,8 @@ def __init__(self, filename=None): def save(self, filename, shapes, image_path, image_data, class_list, line_color=None, fill_color=None, database_src=None): - if filename[-4:].lower() != IOMAP[self.label_file_format][suffix]: - filename += IOMAP[self.label_file_format][EXT] + if filename[-4:].lower() != self.label_file_format.suffix: + filename += self.label_file_format.suffix img_folder_path = os.path.dirname(image_path) img_folder_name = os.path.split(img_folder_path)[-1] img_file_name = os.path.basename(image_path) @@ -70,8 +77,8 @@ def save(self, filename, shapes, image_path, image_data, class_list, image.load(image_path) image_shape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = IOMAP[self.label_file_format][Writer](img_folder_name, img_file_name, - image_shape, shapes, filename, local_img_path=image_path) + writer = self.label_file_format.writer.write(img_folder_name, img_file_name, + image_shape, shapes, filename, local_img_path=image_path) writer.verified = self.verified for shape in shapes: diff --git a/zachary_dev_log.txt b/zachary_dev_log.txt index 2f1791c7f..ff5a4e7f9 100644 --- a/zachary_dev_log.txt +++ b/zachary_dev_log.txt @@ -1,18 +1,19 @@ Todo: - refactor file reader, file writer, labelfile class + refactor labelImg repository, mainly focus on io operations + refactoring classes: LabelFile, LabelFileFormat, LabelIO 2022/10/30 morning: In labelImg.py: - add comments to note refactoring dependencies + add comments to note some code smells for refactory 2022/10/30 afternoon: In labelImg.py, def save_labels(): code smell: the function originally uses multiple if else statements to check the label format, - and then call the corresponding method in LabelFile object to write + and then call the corresponding method in LabelFile object to write the file solving: - making a IOMAP in LabelFile.py + making a IOMAP(dictionary) in LabelFile.py whenever a label needs to be save, map the label format to the corresponding reader object so we have to allign the init method of all writers also the save() method in each writers @@ -23,5 +24,27 @@ Todo: futher work: add interface for writer that restrict the save() method and init() method + (done)link labelformat with labelfile to remove dictionary mapping + +2022/10/31: + In labelImg.py, def set_format(): + code smell: + using if else statements to set each format + + solving: + making LabelFileFomat an abstract class, contain corresponding writer/reader(io), icon, text + then implement the LabelFileFomat object with current available formats + + whenever the labelformat is specified, then the corresponding icon, text, io will be noticed, + because the abstract class force the derived class to have the corresponding attributes, + the only if-else statement remain is the toggle button + + result: + unit test passed + + futher work: + solve if-else in def change_format(), probably using a list, + or making a patch to search all derived class of LabelFileFormat + From c08d34e9b189267e60dff8f73dd2e4fe6cf61cea Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sun, 13 Nov 2022 13:18:40 +0800 Subject: [PATCH 4/9] refactor: refactor label file format object --- labelImg.py | 16 ++++++------ libs/labelFile.py | 56 ++++++----------------------------------- libs/labelFileFormat.py | 37 +++++++++++++++++++++++++++ zachary_dev_log.txt | 20 +++++++++++++-- 4 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 libs/labelFileFormat.py diff --git a/labelImg.py b/labelImg.py index 79ba2017e..cdf095110 100755 --- a/labelImg.py +++ b/labelImg.py @@ -37,7 +37,8 @@ from libs.lightWidget import LightWidget from libs.labelDialog import LabelDialog from libs.colorDialog import ColorDialog -from libs.labelFile import LabelFile, LabelFileError, PascalVoc, Yolo, CreateML #! todo: extract labelfileformat +from libs.labelFile import LabelFile, LabelFileError +from libs.labelFileFormat import PascalVoc, Yolo, CreateML from libs.toolBar import ToolBar from libs.pascal_voc_io import PascalVocReader from libs.pascal_voc_io import XML_EXT @@ -244,10 +245,11 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau ######### refactor here ######### #! todo: link labelformat with labelfile to replace IOmapping + #! understand action arguments and make them members of labelFileFormat object #! status: done - save_format = action(self.label_file_format.meta[0], + save_format = action(self.label_file_format.text, self.change_format, 'Ctrl+Y', - self.label_file_format.meta[1], + self.label_file_format.icon, get_str('changeSaveFormat'), enabled=True) # save_as acturally saved the virtual label into file @@ -548,7 +550,7 @@ def keyPressEvent(self, event): #! futher work: remove if else statements in change_format() def set_format(self, save_format): self.actions.save_format.setText(save_format.text) - self.actions.save_format.setIcon(new_icon(save_format.meta[1])) + self.actions.save_format.setIcon(new_icon(save_format.icon)) self.label_file_format = save_format def change_format(self): @@ -863,6 +865,7 @@ def update_combo_box(self): #! todo: refactor saving format if else statements #! status: done #! modified files: yolo/createml/pascal_io.py, LabelFile.py + #! futher work: reconsider sycronization of Mainwindow and labelfile def save_labels(self, annotation_file_path): annotation_file_path = ustr(annotation_file_path) if self.label_file is None: @@ -882,7 +885,6 @@ def format_shape(s): shapes = [format_shape(shape) for shape in self.canvas.shapes] # Can add different annotation formats here try: - # use IOMAP to select writer self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data, self.label_hist, self.line_color.getRgb(), self.fill_color.getRgb()) print('Image:{0} -> Annotation:{1}'.format(self.file_path, annotation_file_path)) @@ -1152,8 +1154,8 @@ def counter_str(self): return '[{} / {}]'.format(self.cur_img_idx + 1, self.img_count) ##### refactor here ###### - # here using the internal method of mainwindow - # to reference to corresponding reader then show the virtual labels in display + #! todo: use the read method of LabelFileFormat object to replace if-else statements + #! status: undone def show_bounding_box_from_annotation_file(self, file_path): if self.default_save_dir is not None: basename = os.path.basename(os.path.splitext(file_path)[0]) diff --git a/libs/labelFile.py b/libs/labelFile.py index e17a41be3..77847d840 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -7,49 +7,7 @@ from PyQt4.QtGui import QImage import os.path -from enum import Enum - -#! todo: make abstract class for reader and writer -from libs.create_ml_io import CreateMLReader, CreateMLWriter, JSON_EXT -from libs.pascal_voc_io import PascalVocReader, PascalVocWriter, XML_EXT -from libs.yolo_io import YoloReader, YOLOWriter, TXT_EXT - -import abc - -# create a abstract class for label format type -class LabelFileFormat(abc.ABC): - reader = None - writer = None - suffix = None - - text = None - icon = None - meta = None - - -class PascalVoc(LabelFileFormat): - reader = PascalVocReader - writer = PascalVocWriter - suffix = XML_EXT - - text = 'PascalVOC' - meta = ['&PascalVOC', 'format_voc'] - -class Yolo(LabelFileFormat): - reader = YoloReader - writer = YOLOWriter - suffix = TXT_EXT - - text = 'Yolo' - meta = ['&yolo', 'format_yolo'] - -class CreateML(LabelFileFormat): - reader = CreateMLReader - writer = CreateMLWriter - suffix = JSON_EXT - - text = 'CreateML' - meta = ['&CreateML', 'format_createml'] +from libs.labelFileFormat import PascalVoc, Yolo, CreateML class LabelFileError(Exception): pass @@ -64,7 +22,7 @@ def __init__(self, filename=None): self.verified = False def save(self, filename, shapes, image_path, image_data, class_list, - line_color=None, fill_color=None, database_src=None): + line_color=None, fill_color=None, database_src=None): if filename[-4:].lower() != self.label_file_format.suffix: filename += self.label_file_format.suffix img_folder_path = os.path.dirname(image_path) @@ -77,8 +35,8 @@ def save(self, filename, shapes, image_path, image_data, class_list, image.load(image_path) image_shape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = self.label_file_format.writer.write(img_folder_name, img_file_name, - image_shape, shapes, filename, local_img_path=image_path) + writer = self.label_file_format.write(img_folder_name, img_file_name, image_shape, + shapes, filename, local_img_path=image_path) writer.verified = self.verified for shape in shapes: @@ -123,10 +81,10 @@ def save(self, filename, shapes, imagePath, imageData, lineColor=None, fillColor f, ensure_ascii=True, indent=2) ''' - @staticmethod - def is_label_file(filename): + #@staticmethod + def is_label_file(self, filename): file_suffix = os.path.splitext(filename)[1].lower() - return file_suffix == LabelFile.suffix + return file_suffix == self.label_file_format.suffix @staticmethod def convert_points_to_bnd_box(points): diff --git a/libs/labelFileFormat.py b/libs/labelFileFormat.py new file mode 100644 index 000000000..c38002347 --- /dev/null +++ b/libs/labelFileFormat.py @@ -0,0 +1,37 @@ +from libs.pascal_voc_io import PascalVocReader, PascalVocWriter, XML_EXT +from libs.yolo_io import YoloReader, YOLOWriter, TXT_EXT +from libs.create_ml_io import CreateMLReader, CreateMLWriter, JSON_EXT + +class LabelFileFormat(object): + def __init__(self, reader, writer, suffix, text, icon): + self.reader = reader + self.writer = writer + self.suffix = suffix + self.text = text + self.icon = icon + + def read(*args, **kwargs): + #! todo: implement read method + pass + + def write(*args, **kwargs): + writer.write(*args, **kwargs) + + +PascalVoc = LabelFileFormat(PascalVocReader, + PascalVocWriter, + XML_EXT, + 'PascalVOC', + 'format_voc') + +Yolo = LabelFileFormat(YoloReader, + YOLOWriter, + TXT_EXT, + 'Yolo', + 'format_yolo') + +CreateML = LabelFileFormat(CreateMLReader, + CreateMLWriter, + JSON_EXT, + 'CreateML', + 'format_createml') diff --git a/zachary_dev_log.txt b/zachary_dev_log.txt index ff5a4e7f9..ab4005e11 100644 --- a/zachary_dev_log.txt +++ b/zachary_dev_log.txt @@ -2,11 +2,10 @@ Todo: refactor labelImg repository, mainly focus on io operations refactoring classes: LabelFile, LabelFileFormat, LabelIO -2022/10/30 morning: +2022/10/30: In labelImg.py: add comments to note some code smells for refactory -2022/10/30 afternoon: In labelImg.py, def save_labels(): code smell: the function originally uses multiple if else statements to check the label format, @@ -46,5 +45,22 @@ Todo: solve if-else in def change_format(), probably using a list, or making a patch to search all derived class of LabelFileFormat +2022/11/6: + In labelFile.py + code smell: + Extracting abstract class for labelFileFormat does not make sence. + Ideally, abstract class provide a set of abstract method + and concrete class must implement them. + But for the fileformat design, we only need every format to own corresponding + io as attribute. not method + + solving: + thus, I redesign the labelFileFormat class as a concrete class + Each format is a instance of this class, the init method force each instance to have + the required attributes assigned + result: + unit test passed + futher work: + explore some design pattern to apply on format/io interfaces From 53d58633958f5de3d8a06d6a700c6948ab7a9d7f Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sat, 26 Nov 2022 20:47:25 +0800 Subject: [PATCH 5/9] refactor: futher debugging for the application --- labelImg.py | 20 ++++++++++---------- libs/io_interface.py | 1 + libs/labelFile.py | 12 ++++++------ libs/labelFileFormat.py | 26 +++++++++++++++++++------- zachary_dev_log.txt | 27 +++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 libs/io_interface.py diff --git a/labelImg.py b/labelImg.py index cdf095110..4b6a98f15 100755 --- a/labelImg.py +++ b/labelImg.py @@ -38,7 +38,7 @@ from libs.labelDialog import LabelDialog from libs.colorDialog import ColorDialog from libs.labelFile import LabelFile, LabelFileError -from libs.labelFileFormat import PascalVoc, Yolo, CreateML +from libs.labelFileFormat import LabelFileFormat, PascalVoc, Yolo, CreateML from libs.toolBar import ToolBar from libs.pascal_voc_io import PascalVocReader from libs.pascal_voc_io import XML_EXT @@ -554,14 +554,14 @@ def set_format(self, save_format): self.label_file_format = save_format def change_format(self): - if self.label_file_format == PascalVoc: - self.set_format(Yolo) - elif self.label_file_format == Yolo: - self.set_format(CreateML) - elif self.label_file_format == CreateML: - self.set_format(PascalVoc) + if self.label_file_format in LabelFileFormat.formats: + index = self.label_file_format.formats.index(self.label_file_format) + self.label_file_format = LabelFileFormat.formats[index+1 if index+1 < len(self.label_file_format.formats) else 0] + self.set_format(self.label_file_format) else: raise ValueError('Unknown label file format.') + #! todo: error when only label file is change then save + #! this should not be dirty if no image is imported/ no label is plotted self.set_dirty() def no_shapes(self): @@ -1434,7 +1434,7 @@ def open_file(self, _value=False): return path = os.path.dirname(ustr(self.file_path)) if self.file_path else '.' formats = ['*.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()] - filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.label_file_format.suffix]) + filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % suffix for suffix in LabelFileFormat.suffixes]) filename,_ = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters) if filename: if isinstance(filename, (tuple, list)): @@ -1464,10 +1464,10 @@ def save_file_as(self, _value=False): def save_file_dialog(self, remove_ext=True): caption = '%s - Choose File' % __appname__ - filters = 'File (*%s)' % LabelFile.label_file_format.suffix + filters = 'File (*%s)' % self.label_file_format.suffix open_dialog_path = self.current_path() dlg = QFileDialog(self, caption, open_dialog_path, filters) - dlg.setDefaultSuffix(LabelFile.label_file_format.suffix[1:]) + dlg.setDefaultSuffix(self.label_file_format.suffix[1:]) dlg.setAcceptMode(QFileDialog.AcceptSave) filename_without_extension = os.path.splitext(self.file_path)[0] dlg.selectFile(filename_without_extension) diff --git a/libs/io_interface.py b/libs/io_interface.py new file mode 100644 index 000000000..a363ace88 --- /dev/null +++ b/libs/io_interface.py @@ -0,0 +1 @@ +#! todo: implement interface for writer and reader diff --git a/libs/labelFile.py b/libs/labelFile.py index 77847d840..78b0202b5 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -7,7 +7,7 @@ from PyQt4.QtGui import QImage import os.path -from libs.labelFileFormat import PascalVoc, Yolo, CreateML +from libs.labelFileFormat import LabelFileFormat, PascalVoc, Yolo, CreateML class LabelFileError(Exception): pass @@ -35,8 +35,8 @@ def save(self, filename, shapes, image_path, image_data, class_list, image.load(image_path) image_shape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = self.label_file_format.write(img_folder_name, img_file_name, image_shape, - shapes, filename, local_img_path=image_path) + writer = self.label_file_format.writer(img_folder_name, img_file_name, image_shape, + database_src, image_path) writer.verified = self.verified for shape in shapes: @@ -81,10 +81,10 @@ def save(self, filename, shapes, imagePath, imageData, lineColor=None, fillColor f, ensure_ascii=True, indent=2) ''' - #@staticmethod - def is_label_file(self, filename): + @staticmethod + def is_label_file(filename): file_suffix = os.path.splitext(filename)[1].lower() - return file_suffix == self.label_file_format.suffix + return (file_suffix in LabelFileFormat.suffixes) @staticmethod def convert_points_to_bnd_box(points): diff --git a/libs/labelFileFormat.py b/libs/labelFileFormat.py index c38002347..30bee8643 100644 --- a/libs/labelFileFormat.py +++ b/libs/labelFileFormat.py @@ -3,26 +3,38 @@ from libs.create_ml_io import CreateMLReader, CreateMLWriter, JSON_EXT class LabelFileFormat(object): + # here we record suffix of all file format that filter input file + formats = [] + suffixes = [] def __init__(self, reader, writer, suffix, text, icon): self.reader = reader self.writer = writer self.suffix = suffix self.text = text self.icon = icon + self.formats.append(self) + self.suffixes.append(self.suffix) - def read(*args, **kwargs): + def read(self, *args, **kwargs): #! todo: implement read method pass - def write(*args, **kwargs): - writer.write(*args, **kwargs) + def write(self, *args, **kwargs): + #! todo: define method as a single function to write label file + self.writer = self.writer(*args, **kwargs) + self.writer.save() + pass + + # define format compairson + def __eq__(self, other): + return (self.reader == other.reader and self.writer == other.writer) PascalVoc = LabelFileFormat(PascalVocReader, - PascalVocWriter, - XML_EXT, - 'PascalVOC', - 'format_voc') + PascalVocWriter, + XML_EXT, + 'PascalVOC', + 'format_voc') Yolo = LabelFileFormat(YoloReader, YOLOWriter, diff --git a/zachary_dev_log.txt b/zachary_dev_log.txt index ab4005e11..a607e4c14 100644 --- a/zachary_dev_log.txt +++ b/zachary_dev_log.txt @@ -64,3 +64,30 @@ Todo: futher work: explore some design pattern to apply on format/io interfaces + +2022/11/26: + Debugging for application: + though we passed the unit test, I found that the application will crash if some functions are used + 1. the toggle label file format button + 2. save button + 3. openfile button + + the toggle label file format button: + this error occur when we compare two LabelFileFormat objects, in python the compairson is based on pointer address + however, here we want to compare only the attributes for the identity. + Thus, a __eq__ method is written into labelFileFormat class, then the issue is solved + + save button: + for each save operator, we initialize a new writer based on the format + it is important to keep each writer consistent of input parameters + currently, we cannot fuse writer into label file format operation, this will be left to foward work + + openfile button: + this error occur when detecting if the file is available for import + we are checking validity by checking image format and label suffix + the suffix of all labels are not saved, we can only see one suffix at a time + to solve this issue, we add suffixes attribute in labelFileFormat class + thus we can now see all the suffixes of the objects + + futher work: + fuse writer into labelFileFormat object From e482182da99ee071346e988e988eda980b3bac5b Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Fri, 9 Dec 2022 16:54:41 +0800 Subject: [PATCH 6/9] refactor: solving long if-else statement in def load_label_by_filename --- zachary_dev_log.txt => dev_log.txt | 15 ++++++ labelImg.py | 83 ++++++------------------------ libs/create_ml_io.py | 25 +++++---- libs/labelFile.py | 16 +++--- libs/labelFileFormat.py | 9 ++-- libs/pascal_voc_io.py | 13 ++--- libs/yolo_io.py | 9 ++-- notes.txt | 7 +++ tests/test_io.py | 14 ++--- 9 files changed, 82 insertions(+), 109 deletions(-) rename zachary_dev_log.txt => dev_log.txt (88%) create mode 100644 notes.txt diff --git a/zachary_dev_log.txt b/dev_log.txt similarity index 88% rename from zachary_dev_log.txt rename to dev_log.txt index a607e4c14..5f670908b 100644 --- a/zachary_dev_log.txt +++ b/dev_log.txt @@ -91,3 +91,18 @@ Todo: futher work: fuse writer into labelFileFormat object + +2022/12/09: + Today's work: + 1. solving if-else statements in load_file_by_filename() + 2. redefining clear parameter for writer objects + 3. E2E debugging for labelfile io operation + + Code smell: + long if-else statements for loading label file + + Solve: + search for matching suffix in LabelFileFormat object + then use the method of LabelFileFormat.read to load any file + + futher work: abstract interface for writer and reader objects diff --git a/labelImg.py b/labelImg.py index 4b6a98f15..c7111a35d 100755 --- a/labelImg.py +++ b/labelImg.py @@ -547,7 +547,6 @@ def keyPressEvent(self, event): # Support Functions # #! todo: refactor LabelFileFormat object for format switching and format setting #! status: done - #! futher work: remove if else statements in change_format() def set_format(self, save_format): self.actions.save_format.setText(save_format.text) self.actions.save_format.setIcon(new_icon(save_format.icon)) @@ -865,13 +864,11 @@ def update_combo_box(self): #! todo: refactor saving format if else statements #! status: done #! modified files: yolo/createml/pascal_io.py, LabelFile.py - #! futher work: reconsider sycronization of Mainwindow and labelfile def save_labels(self, annotation_file_path): annotation_file_path = ustr(annotation_file_path) if self.label_file is None: self.label_file = LabelFile() self.label_file.verified = self.canvas.verified - # syncronous LabelFileFormat in mainwindow and label LabelFile object self.label_file.label_file_format = self.label_file_format def format_shape(s): @@ -886,7 +883,7 @@ def format_shape(s): # Can add different annotation formats here try: self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data, - self.label_hist, self.line_color.getRgb(), self.fill_color.getRgb()) + self.label_hist) print('Image:{0} -> Annotation:{1}'.format(self.file_path, annotation_file_path)) return True except LabelFileError as e: @@ -1159,32 +1156,13 @@ def counter_str(self): def show_bounding_box_from_annotation_file(self, file_path): if self.default_save_dir is not None: basename = os.path.basename(os.path.splitext(file_path)[0]) - xml_path = os.path.join(self.default_save_dir, basename + XML_EXT) - txt_path = os.path.join(self.default_save_dir, basename + TXT_EXT) - json_path = os.path.join(self.default_save_dir, basename + JSON_EXT) - - """Annotation file priority: - PascalXML > YOLO - """ - if os.path.isfile(xml_path): - self.load_pascal_xml_by_filename(xml_path) - elif os.path.isfile(txt_path): - self.load_yolo_txt_by_filename(txt_path) - elif os.path.isfile(json_path): - self.load_create_ml_json_by_filename(json_path, file_path) - else: - xml_path = os.path.splitext(file_path)[0] + XML_EXT - txt_path = os.path.splitext(file_path)[0] + TXT_EXT - json_path = os.path.splitext(file_path)[0] + JSON_EXT - - if os.path.isfile(xml_path): - self.load_pascal_xml_by_filename(xml_path) - elif os.path.isfile(txt_path): - self.load_yolo_txt_by_filename(txt_path) - elif os.path.isfile(json_path): - self.load_create_ml_json_by_filename(json_path, file_path) - + for suffix in LabelFileFormat.suffixes: + label_path = os.path.join(self.default_save_dir, basename + suffix) + + if os.path.isfile(label_path): + self.load_label_by_filename(label_path) + break def resizeEvent(self, event): if self.canvas and not self.image.isNull()\ @@ -1337,8 +1315,6 @@ def open_dir_dialog(self, _value=False, dir_path=None, silent=False): self.last_open_dir = target_dir_path self.import_dir_images(target_dir_path) self.default_save_dir = target_dir_path - if self.file_path: - self.show_bounding_box_from_annotation_file(file_path=self.file_path) def import_dir_images(self, dir_path): if not self.may_continue() or not dir_path: @@ -1596,45 +1572,20 @@ def load_predefined_classes(self, predef_classes_file): self.label_hist.append(line) ######### refactor here ############# - #### todo: move loading methods into LabelFile object - def load_pascal_xml_by_filename(self, xml_path): - if self.file_path is None: - return - if os.path.isfile(xml_path) is False: - return - - self.set_format(FORMAT_PASCALVOC) - - t_voc_parse_reader = PascalVocReader(xml_path) - shapes = t_voc_parse_reader.get_shapes() - self.load_labels(shapes) - self.canvas.verified = t_voc_parse_reader.verified - - def load_yolo_txt_by_filename(self, txt_path): - if self.file_path is None: - return - if os.path.isfile(txt_path) is False: - return - - self.set_format(FORMAT_YOLO) - t_yolo_parse_reader = YoloReader(txt_path, self.image) - shapes = t_yolo_parse_reader.get_shapes() - print(shapes) - self.load_labels(shapes) - self.canvas.verified = t_yolo_parse_reader.verified - - def load_create_ml_json_by_filename(self, json_path, file_path): + #! todo: move loading methods into LabelFile object + #! status: undone + def load_label_by_filename(self, label_path): if self.file_path is None: return - if os.path.isfile(json_path) is False: + if os.path.isfile(label_path) is False: return - - self.set_format(FORMAT_CREATEML) - - create_ml_parse_reader = CreateMLReader(json_path, file_path) - shapes = create_ml_parse_reader.get_shapes() + suffix = os.path.splitext(label_path)[1] + format_id = LabelFileFormat.suffixes.index(suffix) + file_format = LabelFileFormat.formats[format_id] + self.set_format(file_format) + shapes = self.label_file_format.read(label_path, self.image) self.load_labels(shapes) - self.canvas.verified = create_ml_parse_reader.verified + self.canvas.verified = self.label_file_format.file_reader.verified def copy_previous_bounding_boxes(self): current_index = self.m_img_list.index(self.file_path) diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index 8b1caf234..1744b3724 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -6,21 +6,20 @@ from libs.constants import DEFAULT_ENCODING import os -JSON_EXT = 'json' +JSON_EXT = '.json' ENCODE_METHOD = DEFAULT_ENCODING class CreateMLWriter: - def __init__(self, folder_name, filename, img_size, shapes, output_file, database_src='Unknown', local_img_path=None): - self.folder_name = folder_name - self.filename = filename - self.database_src = database_src - self.img_size = img_size + def __init__(self, img_folder_name, img_file_name, + img_shape, shapes, filename): + self.folder_name = img_folder_name + self.filename = img_file_name + self.img_size = img_shape self.box_list = [] - self.local_img_path = local_img_path self.verified = False self.shapes = shapes - self.output_file = output_file + self.output_file = filename def save(self, target_file=None, class_list=None): if os.path.isfile(self.output_file): @@ -94,11 +93,11 @@ def calculate_coordinates(self, x1, x2, y1, y2): class CreateMLReader: - def __init__(self, json_path, file_path): + def __init__(self, json_path, image): self.json_path = json_path self.shapes = [] self.verified = False - self.filename = os.path.basename(file_path) + self.filename = os.path.basename(json_path) try: self.parse_json() except ValueError: @@ -117,9 +116,9 @@ def parse_json(self): if len(self.shapes) > 0: self.shapes = [] for image in output_list: - if image["image"] == self.filename: - for shape in image["annotations"]: - self.add_shape(shape["label"], shape["coordinates"]) + #if os.path.splitext(image["image"])[0] == os.path.splitext(self.filename)[0]: + for shape in image["annotations"]: + self.add_shape(shape["label"], shape["coordinates"]) def add_shape(self, label, bnd_box): x_min = bnd_box["x"] - (bnd_box["width"] / 2) diff --git a/libs/labelFile.py b/libs/labelFile.py index 78b0202b5..859e025b5 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -21,10 +21,10 @@ def __init__(self, filename=None): self.image_data = None self.verified = False - def save(self, filename, shapes, image_path, image_data, class_list, - line_color=None, fill_color=None, database_src=None): - if filename[-4:].lower() != self.label_file_format.suffix: + def save(self, filename, shapes, image_path, image_data, class_list): + if os.path.splitext(filename)[1] != self.label_file_format.suffix: filename += self.label_file_format.suffix + label_filename = filename img_folder_path = os.path.dirname(image_path) img_folder_name = os.path.split(img_folder_path)[-1] img_file_name = os.path.basename(image_path) @@ -33,10 +33,9 @@ def save(self, filename, shapes, image_path, image_data, class_list, else: image = QImage() image.load(image_path) - image_shape = [image.height(), image.width(), + img_shape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = self.label_file_format.writer(img_folder_name, img_file_name, image_shape, - database_src, image_path) + writer = self.label_file_format.writer(img_folder_name, img_file_name, img_shape, shapes, label_filename) writer.verified = self.verified for shape in shapes: @@ -44,7 +43,10 @@ def save(self, filename, shapes, image_path, image_data, class_list, label = shape['label'] difficult = int(shape['difficult']) bnd_box = LabelFile.convert_points_to_bnd_box(points) - writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) + try: + writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) + except: + pass writer.save(target_file=filename, class_list=class_list) return diff --git a/libs/labelFileFormat.py b/libs/labelFileFormat.py index 30bee8643..49c1e6000 100644 --- a/libs/labelFileFormat.py +++ b/libs/labelFileFormat.py @@ -17,13 +17,12 @@ def __init__(self, reader, writer, suffix, text, icon): def read(self, *args, **kwargs): #! todo: implement read method - pass + self.file_reader = self.reader(*args, **kwargs) + return self.file_reader.get_shapes() def write(self, *args, **kwargs): - #! todo: define method as a single function to write label file - self.writer = self.writer(*args, **kwargs) - self.writer.save() - pass + self.file_writer = self.writer(*args, **kwargs) + self.file_writer.save() # define format compairson def __eq__(self, other): diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index 4c23e2814..f4215e072 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -14,13 +14,14 @@ class PascalVocWriter: - def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None): - self.folder_name = folder_name + def __init__(self, img_folder_name, img_file_name, + img_shape, shapes, filename): + self.folder_name = img_folder_name self.filename = filename - self.database_src = database_src - self.img_size = img_size + self.database_src = "" + self.img_size = img_shape self.box_list = [] - self.local_img_path = local_img_path + self.local_img_path = None self.verified = False def prettify(self, elem): @@ -126,7 +127,7 @@ def save(self, target_file=None, class_list=None): class PascalVocReader: - def __init__(self, file_path): + def __init__(self, file_path, image = None): # shapes type: # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] self.shapes = [] diff --git a/libs/yolo_io.py b/libs/yolo_io.py index 192e2c785..bb8f346db 100644 --- a/libs/yolo_io.py +++ b/libs/yolo_io.py @@ -10,13 +10,12 @@ class YOLOWriter: - def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None): - self.folder_name = folder_name + def __init__(self, img_folder_name, img_file_name, + img_shape, shapes, filename): + self.folder_name = img_folder_name self.filename = filename - self.database_src = database_src - self.img_size = img_size + self.img_size = img_shape self.box_list = [] - self.local_img_path = local_img_path self.verified = False def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult): diff --git a/notes.txt b/notes.txt new file mode 100644 index 000000000..444670705 --- /dev/null +++ b/notes.txt @@ -0,0 +1,7 @@ +Unit testing: + python -m unittest tests/test_xx.py + python -m unittest tests/*.py + +Env: + source venv/bin/activate + diff --git a/tests/test_io.py b/tests/test_io.py index 114dd9d7b..238c0b19a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -12,11 +12,12 @@ def test_upper(self): from pascal_voc_io import PascalVocReader # Test Write/Read - writer = PascalVocWriter('tests', 'test', (512, 512, 1), local_img_path='tests/test.512.512.bmp') - difficult = 1 - writer.add_bnd_box(60, 40, 430, 504, 'person', difficult) - writer.add_bnd_box(113, 40, 450, 403, 'face', difficult) - writer.save('tests/test.xml') + shapes = [ + ['person', [(60, 40), (430, 40), (430, 504), (60, 504)]], + ['face', [(113, 40), (450, 40), (450, 403), (113, 403)]] + ] + writer = PascalVocWriter('tests', 'test', (512, 512, 1), shapes, 'tests/test.xml') + writer.save() reader = PascalVocReader('tests/test.xml') shapes = reader.get_shapes() @@ -48,8 +49,7 @@ def test_a_write(self): shapes = [person, face] output_file = dir_name + "/tests.json" - writer = CreateMLWriter('tests', 'test.512.512.bmp', (512, 512, 1), shapes, output_file, - local_img_path='tests/test.512.512.bmp') + writer = CreateMLWriter('tests', 'test.512.512.bmp', (512, 512, 1), shapes, output_file) writer.verified = True writer.save() From 95364f3847d134d8b9c474b35c1f7fdaaa639ef8 Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Fri, 9 Dec 2022 17:06:26 +0800 Subject: [PATCH 7/9] style: removing refactoring comments and reorder indents --- labelImg.py | 21 --------------------- libs/create_ml_io.py | 2 ++ libs/labelFileFormat.py | 5 ++--- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/labelImg.py b/labelImg.py index c7111a35d..22c521b76 100755 --- a/labelImg.py +++ b/labelImg.py @@ -243,16 +243,11 @@ def __init__(self, default_filename=None, default_prefdef_class_file=None, defau save = action(get_str('save'), self.save_file, 'Ctrl+S', 'save', get_str('saveDetail'), enabled=False) - ######### refactor here ######### - #! todo: link labelformat with labelfile to replace IOmapping - #! understand action arguments and make them members of labelFileFormat object - #! status: done save_format = action(self.label_file_format.text, self.change_format, 'Ctrl+Y', self.label_file_format.icon, get_str('changeSaveFormat'), enabled=True) - # save_as acturally saved the virtual label into file save_as = action(get_str('saveAs'), self.save_file_as, 'Ctrl+Shift+S', 'save-as', get_str('saveAsDetail'), enabled=False) @@ -543,10 +538,6 @@ def keyPressEvent(self, event): # Draw rectangle if Ctrl is pressed self.canvas.set_drawing_shape_to_square(True) - ########### refactor here ############ - # Support Functions # - #! todo: refactor LabelFileFormat object for format switching and format setting - #! status: done def set_format(self, save_format): self.actions.save_format.setText(save_format.text) self.actions.save_format.setIcon(new_icon(save_format.icon)) @@ -860,10 +851,6 @@ def update_combo_box(self): self.combo_box.update_items(unique_text_list) - ############# refactor here ############ - #! todo: refactor saving format if else statements - #! status: done - #! modified files: yolo/createml/pascal_io.py, LabelFile.py def save_labels(self, annotation_file_path): annotation_file_path = ustr(annotation_file_path) if self.label_file is None: @@ -1150,16 +1137,11 @@ def counter_str(self): """ return '[{} / {}]'.format(self.cur_img_idx + 1, self.img_count) - ##### refactor here ###### - #! todo: use the read method of LabelFileFormat object to replace if-else statements - #! status: undone def show_bounding_box_from_annotation_file(self, file_path): if self.default_save_dir is not None: basename = os.path.basename(os.path.splitext(file_path)[0]) - for suffix in LabelFileFormat.suffixes: label_path = os.path.join(self.default_save_dir, basename + suffix) - if os.path.isfile(label_path): self.load_label_by_filename(label_path) break @@ -1571,9 +1553,6 @@ def load_predefined_classes(self, predef_classes_file): else: self.label_hist.append(line) - ######### refactor here ############# - #! todo: move loading methods into LabelFile object - #! status: undone def load_label_by_filename(self, label_path): if self.file_path is None: return diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index 1744b3724..2d740401f 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -11,6 +11,7 @@ class CreateMLWriter: + def __init__(self, img_folder_name, img_file_name, img_shape, shapes, filename): self.folder_name = img_folder_name @@ -93,6 +94,7 @@ def calculate_coordinates(self, x1, x2, y1, y2): class CreateMLReader: + def __init__(self, json_path, image): self.json_path = json_path self.shapes = [] diff --git a/libs/labelFileFormat.py b/libs/labelFileFormat.py index 49c1e6000..774b1658c 100644 --- a/libs/labelFileFormat.py +++ b/libs/labelFileFormat.py @@ -3,9 +3,10 @@ from libs.create_ml_io import CreateMLReader, CreateMLWriter, JSON_EXT class LabelFileFormat(object): - # here we record suffix of all file format that filter input file + formats = [] suffixes = [] + def __init__(self, reader, writer, suffix, text, icon): self.reader = reader self.writer = writer @@ -16,7 +17,6 @@ def __init__(self, reader, writer, suffix, text, icon): self.suffixes.append(self.suffix) def read(self, *args, **kwargs): - #! todo: implement read method self.file_reader = self.reader(*args, **kwargs) return self.file_reader.get_shapes() @@ -24,7 +24,6 @@ def write(self, *args, **kwargs): self.file_writer = self.writer(*args, **kwargs) self.file_writer.save() - # define format compairson def __eq__(self, other): return (self.reader == other.reader and self.writer == other.writer) From b2a776e55d3b29b31ba60d10400ed9fcad4d27f3 Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sat, 24 Dec 2022 22:47:55 +0800 Subject: [PATCH 8/9] refactor: add abstract class for io operations --- dev_log.txt | 5 +++++ libs/create_ml_io.py | 27 +++++++---------------- libs/io_abstract_class.py | 46 +++++++++++++++++++++++++++++++++++++++ libs/io_interface.py | 1 - libs/pascal_voc_io.py | 23 +++++++------------- libs/yolo_io.py | 20 +++++------------ 6 files changed, 73 insertions(+), 49 deletions(-) create mode 100644 libs/io_abstract_class.py delete mode 100644 libs/io_interface.py diff --git a/dev_log.txt b/dev_log.txt index 5f670908b..5fd0071a8 100644 --- a/dev_log.txt +++ b/dev_log.txt @@ -106,3 +106,8 @@ Todo: then use the method of LabelFileFormat.read to load any file futher work: abstract interface for writer and reader objects + +2022/12/24: + Today's work: + build abstract class for file reader and file writer + for future extension, any format should inherit the writer and reader class for implementation diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index 2d740401f..b300b252a 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -4,23 +4,19 @@ from pathlib import Path from libs.constants import DEFAULT_ENCODING +from libs.io_abstract_class import FileReader, FileWriter import os JSON_EXT = '.json' ENCODE_METHOD = DEFAULT_ENCODING -class CreateMLWriter: +class CreateMLWriter(FileWriter): def __init__(self, img_folder_name, img_file_name, img_shape, shapes, filename): - self.folder_name = img_folder_name - self.filename = img_file_name - self.img_size = img_shape - self.box_list = [] - self.verified = False - self.shapes = shapes - self.output_file = filename + super().__init__(img_folder_name, + img_file_name, img_shape, shapes, filename) def save(self, target_file=None, class_list=None): if os.path.isfile(self.output_file): @@ -93,19 +89,14 @@ def calculate_coordinates(self, x1, x2, y1, y2): return height, width, x, y -class CreateMLReader: +class CreateMLReader(FileReader): - def __init__(self, json_path, image): + def __init__(self, json_path, file_path): self.json_path = json_path - self.shapes = [] - self.verified = False self.filename = os.path.basename(json_path) - try: - self.parse_json() - except ValueError: - print("JSON decoding failed") + super().__init__(file_path) - def parse_json(self): + def parse_file(self): with open(self.json_path, "r") as file: input_data = file.read() @@ -132,5 +123,3 @@ def add_shape(self, label, bnd_box): points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] self.shapes.append((label, points, None, None, True)) - def get_shapes(self): - return self.shapes diff --git a/libs/io_abstract_class.py b/libs/io_abstract_class.py new file mode 100644 index 000000000..b55efd747 --- /dev/null +++ b/libs/io_abstract_class.py @@ -0,0 +1,46 @@ +import abc + +class FileWriter(metaclass=abc.ABCMeta): + + def __init__(self, img_folder_name, img_file_name, + img_shape, shapes, filename): + self.folder_name = img_folder_name + self.filename = img_file_name + self.img_size = img_shape + self.box_list = [] + self.verified = False + self.shapes = shapes + self.output_file = filename + + @abc.abstractmethod + def save(self): + # save labelfile format at location file path + pass + +class FileReader(metaclass=abc.ABCMeta): + + def __init__(self, file_path): + self.shapes = [] + self.file_path = file_path + self.verified = False + + try: + self.parse_file() + except: + pass + + @abc.abstractmethod + def add_shape(self): + # append the shape to self.shapes + pass + + @abc.abstractmethod + def parse_file(self): + # parse the label file then add all the shapes into self.shapes + pass + + def get_shapes(self): + # return the shapes of bounding boxes + return self.shapes + + diff --git a/libs/io_interface.py b/libs/io_interface.py deleted file mode 100644 index a363ace88..000000000 --- a/libs/io_interface.py +++ /dev/null @@ -1 +0,0 @@ -#! todo: implement interface for writer and reader diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index f4215e072..f7e74c357 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -7,22 +7,21 @@ import codecs from libs.constants import DEFAULT_ENCODING from libs.ustr import ustr +from libs.io_abstract_class import FileReader, FileWriter XML_EXT = '.xml' ENCODE_METHOD = DEFAULT_ENCODING -class PascalVocWriter: +class PascalVocWriter(FileWriter): def __init__(self, img_folder_name, img_file_name, img_shape, shapes, filename): - self.folder_name = img_folder_name - self.filename = filename + super().__init__(img_folder_name, + img_file_name, img_shape, shapes, filename) self.database_src = "" - self.img_size = img_shape self.box_list = [] self.local_img_path = None - self.verified = False def prettify(self, elem): """ @@ -125,18 +124,12 @@ def save(self, target_file=None, class_list=None): out_file.close() -class PascalVocReader: +class PascalVocReader(FileReader): - def __init__(self, file_path, image = None): + def __init__(self, file_path): # shapes type: # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] - self.shapes = [] - self.file_path = file_path - self.verified = False - try: - self.parse_xml() - except: - pass + super().__init__(file_path) def get_shapes(self): return self.shapes @@ -149,7 +142,7 @@ def add_shape(self, label, bnd_box, difficult): points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] self.shapes.append((label, points, None, None, difficult)) - def parse_xml(self): + def parse_file(self): assert self.file_path.endswith(XML_EXT), "Unsupported file format" parser = etree.XMLParser(encoding=ENCODE_METHOD) xml_tree = ElementTree.parse(self.file_path, parser=parser).getroot() diff --git a/libs/yolo_io.py b/libs/yolo_io.py index bb8f346db..ed09b1b51 100644 --- a/libs/yolo_io.py +++ b/libs/yolo_io.py @@ -4,6 +4,7 @@ import os from libs.constants import DEFAULT_ENCODING +from libs.io_abstract_class import FileReader, FileWriter TXT_EXT = '.txt' ENCODE_METHOD = DEFAULT_ENCODING @@ -12,11 +13,8 @@ class YOLOWriter: def __init__(self, img_folder_name, img_file_name, img_shape, shapes, filename): - self.folder_name = img_folder_name - self.filename = filename - self.img_size = img_shape - self.box_list = [] - self.verified = False + super().__init__(img_folder_name, + img_file_name, img_shape, shapes, filename) def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult): bnd_box = {'xmin': x_min, 'ymin': y_min, 'xmax': x_max, 'ymax': y_max} @@ -77,12 +75,11 @@ def save(self, class_list=[], target_file=None): -class YoloReader: +class YoloReader(FileReader): - def __init__(self, file_path, image, class_list_path=None): + def __init__(self, file_path, image): # shapes type: # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] - self.shapes = [] self.file_path = file_path if class_list_path is None: @@ -102,12 +99,7 @@ def __init__(self, file_path, image, class_list_path=None): 1 if image.isGrayscale() else 3] self.img_size = img_size - - self.verified = False - # try: - self.parse_yolo_format() - # except: - # pass + super().__init__(file_path) def get_shapes(self): return self.shapes From acc55d4c2f0408d6a9c28bf6fa445f2f3d8f5484 Mon Sep 17 00:00:00 2001 From: ZachKLYeh Date: Sun, 25 Dec 2022 13:42:56 +0800 Subject: [PATCH 9/9] refactor: fix pascalvoc reader init error --- libs/pascal_voc_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index f7e74c357..7711511e0 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -126,7 +126,7 @@ def save(self, target_file=None, class_list=None): class PascalVocReader(FileReader): - def __init__(self, file_path): + def __init__(self, file_path, image = None): # shapes type: # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] super().__init__(file_path)