From 7cbe68536e6a43f13bc437c3b41590f239fc66aa Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Tue, 16 Feb 2021 15:45:09 +0900 Subject: [PATCH 1/6] add shortcut c instead of ctrl+v --- README.rst | 13 +++++++++++++ activate.bat | 2 ++ build.bat | 6 ++++++ labelImg.py | 8 ++++++++ 4 files changed, 29 insertions(+) create mode 100644 activate.bat create mode 100644 build.bat diff --git a/README.rst b/README.rst index 637b64bf9..ee6264cf2 100644 --- a/README.rst +++ b/README.rst @@ -119,6 +119,17 @@ Open the Anaconda Prompt and go to the `labelImg <#labelimg>`__ directory python labelImg.py python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] +.. code:: + + conda create -n labelimg python=3.5 + pip install PyQt5==5.10.1 lxml pyinstaller + # You may delete C:~/.labelImgSettings.pkl + pyrcc5 -o libs/resources.py resources.qrc + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + pyinstaller --hidden-import=xml --hidden-import=xml.etree --hidden-import=xml.etree.ElementTree --hidden-import=lxml.etree --hidden-import=PyQt5.sip -D -F -n labelImg -c "./labelImg.py" -p ./libs -p ./ --paths C:\Users\Administrator\Anaconda3\envs\labelimg\Lib\site-packages\PyQt5\Qt\bin + + Get from PyPI but only python3.0 or above ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is the simplest (one-command) install method on modern Linux distributions such as Ubuntu and Fedora. @@ -219,6 +230,8 @@ Hotkeys +--------------------+--------------------------------------------+ | a | Previous image | +--------------------+--------------------------------------------+ +| c | Copy Previous label | ++--------------------+--------------------------------------------+ | del | Delete the selected rect box | +--------------------+--------------------------------------------+ | Ctrl++ | Zoom in | diff --git a/activate.bat b/activate.bat new file mode 100644 index 000000000..1009e0d87 --- /dev/null +++ b/activate.bat @@ -0,0 +1,2 @@ +pushd \\Mac\Home\Documents\programs\software\labelImg_copyfunc +%windir%\System32\cmd.exe "/K" C:\Users\Administrator\Anaconda3\Scripts\activate.bat C:\Users\Administrator\Anaconda3\envs\labelimg diff --git a/build.bat b/build.bat new file mode 100644 index 000000000..42889fee2 --- /dev/null +++ b/build.bat @@ -0,0 +1,6 @@ +rd /s /q dist build +del labelImg.spec + +pyinstaller --hidden-import=xml --hidden-import=xml.etree --hidden-import=xml.etree.ElementTree --hidden-import=lxml.etree --hidden-import=PyQt5.sip -D -F -n labelImg -c "./labelImg.py" -p ./libs -p ./ --paths C:\Users\Administrator\Anaconda3\envs\labelimg\Lib\site-packages\PyQt5\Qt\bin +mkdir dist\data +xcopy data dist\data /s /e /y /i \ No newline at end of file diff --git a/labelImg.py b/labelImg.py index 739c7f092..a1dda0251 100755 --- a/labelImg.py +++ b/labelImg.py @@ -135,6 +135,14 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa self.editButton = QToolButton() self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + # * + # * dhzs 2017-12-2 add copy button + # * + self.copy_prev_button = QPushButton('Copy the previous label') + self.copy_prev_button.setShortcut('c') + self.copy_prev_button.clicked.connect(self.copyPreviousBoundingBoxes) + listLayout.addWidget(self.copy_prev_button) + # Add some of widgets to listLayout listLayout.addWidget(self.editButton) listLayout.addWidget(self.diffcButton) From 8baba7e5baa1b19f6034b884b18929ccd71fe582 Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Fri, 26 Feb 2021 17:24:06 +0900 Subject: [PATCH 2/6] add truncated flag --- labelImg.py | 74 +++++++++++++++++++--------- labelImg.spec | 33 +++++++++++++ libs/create_ml_io.py | 2 +- libs/labelFile.py | 12 ++--- libs/pascal_voc_io.py | 31 ++++++------ libs/shape.py | 43 ++++++++++++++-- libs/yolo_io.py | 14 +++--- resources/strings/strings.properties | 1 + 8 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 labelImg.spec diff --git a/labelImg.py b/labelImg.py index a1dda0251..861320fb9 100755 --- a/labelImg.py +++ b/labelImg.py @@ -128,13 +128,27 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa useDefaultLabelContainer = QWidget() useDefaultLabelContainer.setLayout(useDefaultLabelQHBoxLayout) - # Create a widget for edit and diffc button - self.diffcButton = QCheckBox(getStr('useDifficult')) - self.diffcButton.setChecked(False) - self.diffcButton.stateChanged.connect(self.btnstate) self.editButton = QToolButton() self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + # Create Flag checkboxes list + self.flagGroupBox = QGroupBox(self) + self.flagGroupBox.setTitle('Flags') + flagslayout = QVBoxLayout(self) + self.flagGroupBox.setLayout(flagslayout) + self.flagButtons = [] + + self.diffcButton = QCheckBox(getStr('useDifficult')) + self.diffcButton.setChecked(False) + # calling stateChanged is inappropriate! + self.diffcButton.clicked.connect(self.btnstate) + self.flagButtons.append(self.diffcButton) + + self.truncButton = QCheckBox(getStr('useTruncated')) + self.truncButton.setChecked(False) + self.truncButton.clicked.connect(self.btnstate) + self.flagButtons.append(self.truncButton) + # * # * dhzs 2017-12-2 add copy button # * @@ -143,9 +157,13 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa self.copy_prev_button.clicked.connect(self.copyPreviousBoundingBoxes) listLayout.addWidget(self.copy_prev_button) + # add flag buttons to flagsGroupBox + for flagbtn in self.flagButtons: + flagslayout.addWidget(flagbtn) + # Add some of widgets to listLayout listLayout.addWidget(self.editButton) - listLayout.addWidget(self.diffcButton) + listLayout.addWidget(self.flagGroupBox) listLayout.addWidget(useDefaultLabelContainer) # Create and add combobox for showing unique labels in group @@ -446,8 +464,6 @@ def getFormatMeta(format): self.fillColor = None self.zoom_level = 100 self.fit_window = False - # Add Chris - self.difficult = False ## Fix the compatible issue for qt4 and qt5. Convert the QStringList to python list if settings.get(SETTING_RECENT_FILES): @@ -479,8 +495,7 @@ def getFormatMeta(format): Shape.line_color = self.lineColor = QColor(settings.get(SETTING_LINE_COLOR, DEFAULT_LINE_COLOR)) Shape.fill_color = self.fillColor = QColor(settings.get(SETTING_FILL_COLOR, DEFAULT_FILL_COLOR)) self.canvas.setDrawingColor(self.lineColor) - # Add chris - Shape.difficult = self.difficult + Shape.flags = self.flags def xbool(x): if isinstance(x, QVariant): @@ -730,6 +745,20 @@ def fileitemDoubleClicked(self, item=None): if filename: self.loadFile(filename) + @property + def flags(self): + """ + Returns + ------- + dict + key is a flag name + value is a Bool + """ + return {flagbtn.text(): flagbtn.isChecked() for flagbtn in self.flagButtons} + + def setFlagsChecked(self, flags): + pass + # Add chris def btnstate(self, item= None): """ Function to handle difficult examples @@ -741,7 +770,7 @@ def btnstate(self, item= None): if not item: # If not selected Item, take the first one item = self.labelList.item(self.labelList.count()-1) - difficult = self.diffcButton.isChecked() + flags = self.flags try: shape = self.itemsToShapes[item] @@ -749,8 +778,7 @@ def btnstate(self, item= None): pass # Checked and Update try: - if difficult != shape.difficult: - shape.difficult = difficult + if shape.setChangedFlags(flags): self.setDirty() else: # User probably changed item visibility self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) @@ -798,7 +826,7 @@ def remLabel(self, shape): def loadLabels(self, shapes): s = [] - for label, points, line_color, fill_color, difficult in shapes: + for label, points, line_color, fill_color, flags in shapes: shape = Shape(label=label) for x, y in points: @@ -808,7 +836,7 @@ def loadLabels(self, shapes): self.setDirty() shape.addPoint(QPointF(x, y)) - shape.difficult = difficult + shape.flags = flags shape.close() s.append(shape) @@ -848,11 +876,10 @@ def format_shape(s): line_color=s.line_color.getRgb(), fill_color=s.fill_color.getRgb(), points=[(p.x(), p.y()) for p in s.points], - # add chris - difficult = s.difficult) + flags=s.flags) shapes = [format_shape(shape) for shape in self.canvas.shapes] - # Can add differrent annotation formats here + # Can add different annotation formats here try: if self.labelFileFormat == LabelFileFormat.PASCAL_VOC: if annotationFilePath[-4:].lower() != ".xml": @@ -899,9 +926,11 @@ def labelSelectionChanged(self): self._noSelectionSlot = True self.canvas.selectShape(self.itemsToShapes[item]) shape = self.itemsToShapes[item] - # Add Chris - self.diffcButton.setChecked(shape.difficult) - + for flagbtn in self.flagButtons: + if hasattr(shape, flagbtn.text()): + flagbtn.setChecked(getattr(shape, flagbtn.text())) + else: + flagbtn.setChecked(False) def labelItemChanged(self, item): shape = self.itemsToShapes[item] label = item.text() @@ -932,8 +961,8 @@ def newShape(self): else: text = self.defaultLabelTextLine.text() - # Add Chris - self.diffcButton.setChecked(False) + for flagbtn in self.flagButtons: + flagbtn.setChecked(False) if text is not None: self.prevLabelText = text generate_color = generateColorByText(text) @@ -1283,7 +1312,6 @@ def importDirImages(self, dirpath): self.lastOpenDir = dirpath self.dirname = dirpath self.filePath = None - self.fileListWidget.clear() self.mImgList = self.scanAllImages(dirpath) self.openNextImg() for imgPath in self.mImgList: diff --git a/labelImg.spec b/labelImg.spec new file mode 100644 index 000000000..b52374272 --- /dev/null +++ b/labelImg.spec @@ -0,0 +1,33 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['labelImg.py'], + pathex=['./libs', './', 'C:\\Users\\Administrator\\Anaconda3\\envs\\labelimg\\Lib\\site-packages\\PyQt5\\Qt\\bin', 'O:\\Documents\\programs\\software\\labelImg_copyfunc'], + binaries=[], + datas=[], + hiddenimports=['xml', 'xml.etree', 'xml.etree.ElementTree', 'lxml.etree', 'PyQt5.sip'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='labelImg', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True ) diff --git a/libs/create_ml_io.py b/libs/create_ml_io.py index 0d0781464..9d95b94ff 100644 --- a/libs/create_ml_io.py +++ b/libs/create_ml_io.py @@ -125,7 +125,7 @@ def add_shape(self, label, bndbox): ymax = bndbox["y"] + (bndbox["height"] / 2) points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] - self.shapes.append((label, points, None, None, True)) + self.shapes.append((label, points, None, None, {'difficult': False, 'truncated': False})) def get_shapes(self): return self.shapes diff --git a/libs/labelFile.py b/libs/labelFile.py index b366d45e5..b1fe9eaa2 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -77,10 +77,10 @@ def savePascalVocFormat(self, filename, shapes, imagePath, imageData, for shape in shapes: points = shape['points'] label = shape['label'] - # Add Chris - difficult = int(shape['difficult']) + + flags = shape['flags'] bndbox = LabelFile.convertPoints2BndBox(points) - writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult) + writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, flags) writer.save(targetFile=filename) return @@ -107,10 +107,10 @@ def saveYoloFormat(self, filename, shapes, imagePath, imageData, classList, for shape in shapes: points = shape['points'] label = shape['label'] - # Add Chris - difficult = int(shape['difficult']) + + flags = shape['flags'] bndbox = LabelFile.convertPoints2BndBox(points) - writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult) + writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, flags) writer.save(targetFile=filename, classList=classList) return diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index 627e315b4..652c8baa4 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -77,10 +77,10 @@ def genXML(self): segmented.text = '0' return top - def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult): + def addBndBox(self, xmin, ymin, xmax, ymax, name, flags): bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax} bndbox['name'] = name - bndbox['difficult'] = difficult + bndbox['flags'] = flags self.boxlist.append(bndbox) def appendObjects(self, top): @@ -90,15 +90,11 @@ def appendObjects(self, top): name.text = ustr(each_object['name']) pose = SubElement(object_item, 'pose') pose.text = "Unspecified" - truncated = SubElement(object_item, 'truncated') - if int(float(each_object['ymax'])) == int(float(self.imgSize[0])) or (int(float(each_object['ymin']))== 1): - truncated.text = "1" # max == height or min - elif (int(float(each_object['xmax']))==int(float(self.imgSize[1]))) or (int(float(each_object['xmin']))== 1): - truncated.text = "1" # max == width or min - else: - truncated.text = "0" - difficult = SubElement(object_item, 'difficult') - difficult.text = str( bool(each_object['difficult']) & 1 ) + + for key, val in each_object['flags'].items(): + element = SubElement(object_item, key) + element.text = str( bool(val) & 1 ) + bndbox = SubElement(object_item, 'bndbox') xmin = SubElement(bndbox, 'xmin') xmin.text = str(each_object['xmin']) @@ -128,7 +124,7 @@ class PascalVocReader: def __init__(self, filepath): # shapes type: - # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] + # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, flags] self.shapes = [] self.filepath = filepath self.verified = False @@ -140,13 +136,13 @@ def __init__(self, filepath): def getShapes(self): return self.shapes - def addShape(self, label, bndbox, difficult): + def addShape(self, label, bndbox, flags): xmin = int(float(bndbox.find('xmin').text)) ymin = int(float(bndbox.find('ymin').text)) xmax = int(float(bndbox.find('xmax').text)) ymax = int(float(bndbox.find('ymax').text)) points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] - self.shapes.append((label, points, None, None, difficult)) + self.shapes.append((label, points, None, None, flags)) def parseXML(self): assert self.filepath.endswith(XML_EXT), "Unsupport file format" @@ -163,9 +159,12 @@ def parseXML(self): for object_iter in xmltree.findall('object'): bndbox = object_iter.find("bndbox") label = object_iter.find('name').text - # Add chris + difficult = False if object_iter.find('difficult') is not None: difficult = bool(int(object_iter.find('difficult').text)) - self.addShape(label, bndbox, difficult) + truncated = False + if object_iter.find('truncated') is not None: + truncated = bool(int(object_iter.find('truncated').text)) + self.addShape(label, bndbox, flags={'difficult': difficult, 'truncated': truncated}) return True diff --git a/libs/shape.py b/libs/shape.py index 466e046dc..316966047 100644 --- a/libs/shape.py +++ b/libs/shape.py @@ -38,12 +38,12 @@ class Shape(object): scale = 1.0 labelFontSize = 8 - def __init__(self, label=None, line_color=None, difficult=False, paintLabel=False): + def __init__(self, label=None, line_color=None, flags={'difficult': False, 'truncated': False}, paintLabel=False): self.label = label self.points = [] self.fill = False self.selected = False - self.difficult = difficult + self.flags = flags self.paintLabel = paintLabel self._highlightIndex = None @@ -193,9 +193,30 @@ def copy(self): shape.line_color = self.line_color if self.fill_color != Shape.fill_color: shape.fill_color = self.fill_color - shape.difficult = self.difficult + shape.flags = self.flags return shape + + def setChangedFlags(self, newflags): + """ + Set changed flags and return the boolean representing whether to have changed flags + Returns + ------- + Bool + Whether to have changed flags + """ + isChanged = False + for newkey, newval in newflags.items(): + if newkey not in self.flags.keys() or self.flags[newkey] != newval: + isChanged = True + break + + if isChanged: + self.flags = newflags + + return isChanged + + def __len__(self): return len(self.points) @@ -204,3 +225,19 @@ def __getitem__(self, key): def __setitem__(self, key, value): self.points[key] = value + + # allow to access the flag's key as Shape's attribute + def __getattr__(self, item): + if item in self.flags.keys(): + return self.flags[item] + else: + raise AttributeError('Shape has no attribute \'{}\''.format(item)) + + def __setattr__(self, key, value): + try: + super().__setattr__(key, value) + except AttributeError: + if key in self.flags.keys(): + self.flags[key] = value + else: + raise AttributeError('Shape has no attribute \'{}\''.format(key)) \ No newline at end of file diff --git a/libs/yolo_io.py b/libs/yolo_io.py index 216fba388..f8aac6a95 100644 --- a/libs/yolo_io.py +++ b/libs/yolo_io.py @@ -22,10 +22,10 @@ def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localIm self.localImgPath = localImgPath self.verified = False - def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult): + def addBndBox(self, xmin, ymin, xmax, ymax, name, flags): bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax} bndbox['name'] = name - bndbox['difficult'] = difficult + bndbox['flags'] = flags self.boxlist.append(bndbox) def BndBox2YoloLine(self, box, classList=[]): @@ -85,7 +85,7 @@ class YoloReader: def __init__(self, filepath, image, classListPath=None): # shapes type: - # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] + # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, flags] self.shapes = [] self.filepath = filepath @@ -116,10 +116,10 @@ def __init__(self, filepath, image, classListPath=None): def getShapes(self): return self.shapes - def addShape(self, label, xmin, ymin, xmax, ymax, difficult): + def addShape(self, label, xmin, ymin, xmax, ymax, flags): points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] - self.shapes.append((label, points, None, None, difficult)) + self.shapes.append((label, points, None, None, flags)) def yoloLine2Shape(self, classIndex, xcen, ycen, w, h): label = self.classes[int(classIndex)] @@ -142,5 +142,5 @@ def parseYoloFormat(self): classIndex, xcen, ycen, w, h = bndBox.strip().split(' ') label, xmin, ymin, xmax, ymax = self.yoloLine2Shape(classIndex, xcen, ycen, w, h) - # Caveat: difficult flag is discarded when saved as yolo format. - self.addShape(label, xmin, ymin, xmax, ymax, False) + # Caveat: flags are discarded when saved as yolo format. + self.addShape(label, xmin, ymin, xmax, ymax, {'difficult': False, 'truncated': False}) diff --git a/resources/strings/strings.properties b/resources/strings/strings.properties index d684e4ada..896c677fb 100644 --- a/resources/strings/strings.properties +++ b/resources/strings/strings.properties @@ -55,6 +55,7 @@ shapeFillColorDetail=Change the fill color for this specific shape showHide=Show/Hide Label Panel useDefaultLabel=Use default label useDifficult=difficult +useTruncated=truncated boxLabelText=Box Labels labels=Labels autoSaveMode=Auto Save mode From dd6f703df657b141af955fe945291a5e477f494d Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Fri, 26 Feb 2021 19:05:05 +0900 Subject: [PATCH 3/6] custom flags --- activate.bat | 2 - build.bat | 6 --- labelImg.py | 104 ++++++++++++++++++++++++++++++++++-------- labelImg.spec | 33 -------------- libs/pascal_voc_io.py | 17 ++++--- 5 files changed, 95 insertions(+), 67 deletions(-) delete mode 100644 activate.bat delete mode 100644 build.bat delete mode 100644 labelImg.spec diff --git a/activate.bat b/activate.bat deleted file mode 100644 index 1009e0d87..000000000 --- a/activate.bat +++ /dev/null @@ -1,2 +0,0 @@ -pushd \\Mac\Home\Documents\programs\software\labelImg_copyfunc -%windir%\System32\cmd.exe "/K" C:\Users\Administrator\Anaconda3\Scripts\activate.bat C:\Users\Administrator\Anaconda3\envs\labelimg diff --git a/build.bat b/build.bat deleted file mode 100644 index 42889fee2..000000000 --- a/build.bat +++ /dev/null @@ -1,6 +0,0 @@ -rd /s /q dist build -del labelImg.spec - -pyinstaller --hidden-import=xml --hidden-import=xml.etree --hidden-import=xml.etree.ElementTree --hidden-import=lxml.etree --hidden-import=PyQt5.sip -D -F -n labelImg -c "./labelImg.py" -p ./libs -p ./ --paths C:\Users\Administrator\Anaconda3\envs\labelimg\Lib\site-packages\PyQt5\Qt\bin -mkdir dist\data -xcopy data dist\data /s /e /y /i \ No newline at end of file diff --git a/labelImg.py b/labelImg.py index 861320fb9..90a4d5ff8 100755 --- a/labelImg.py +++ b/labelImg.py @@ -134,20 +134,28 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa # Create Flag checkboxes list self.flagGroupBox = QGroupBox(self) self.flagGroupBox.setTitle('Flags') - flagslayout = QVBoxLayout(self) - self.flagGroupBox.setLayout(flagslayout) - self.flagButtons = [] - self.diffcButton = QCheckBox(getStr('useDifficult')) - self.diffcButton.setChecked(False) - # calling stateChanged is inappropriate! - self.diffcButton.clicked.connect(self.btnstate) - self.flagButtons.append(self.diffcButton) + vbox = QVBoxLayout(self) + self.flagslayout = QGridLayout(self) + vbox.addLayout(self.flagslayout) + + addflagLayout = QHBoxLayout(self) + self.flaglineedit = QLineEdit() + self.flaglineedit.setPlaceholderText('Flag name') + self.flaglineedit.returnPressed.connect(self.addflag_button_pushed) + addflagLayout.addWidget(self.flaglineedit, 9) + self.addflag_button = QPushButton('↑') + self.addflag_button.clicked.connect(self.addflag_button_pushed) + addflagLayout.addWidget(self.addflag_button, 1) + vbox.addLayout(addflagLayout) - self.truncButton = QCheckBox(getStr('useTruncated')) - self.truncButton.setChecked(False) - self.truncButton.clicked.connect(self.btnstate) - self.flagButtons.append(self.truncButton) + self.flagGroupBox.setLayout(vbox) + # list of tuple(checkbox, x button) + self.flagWidgets = [] + + # add difficult and truncated flag by default + self.addFlags(getStr('useDifficult')) + self.addFlags(getStr('useTruncated')) # * # * dhzs 2017-12-2 add copy button @@ -157,13 +165,9 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa self.copy_prev_button.clicked.connect(self.copyPreviousBoundingBoxes) listLayout.addWidget(self.copy_prev_button) - # add flag buttons to flagsGroupBox - for flagbtn in self.flagButtons: - flagslayout.addWidget(flagbtn) - # Add some of widgets to listLayout - listLayout.addWidget(self.editButton) listLayout.addWidget(self.flagGroupBox) + listLayout.addWidget(self.editButton) listLayout.addWidget(useDefaultLabelContainer) # Create and add combobox for showing unique labels in group @@ -756,8 +760,64 @@ def flags(self): """ return {flagbtn.text(): flagbtn.isChecked() for flagbtn in self.flagButtons} - def setFlagsChecked(self, flags): - pass + def addflag_button_pushed(self): + # check whether the name is valid or not + flagname = self.flaglineedit.text() + if flagname == '': + self.errorMessage('Invalid flag name', 'Input any texts!') + return + + if flagname in [c.text() for c in self.flagButtons]: + self.errorMessage('Invalid flag name', 'Already exist!') + return + + self.addFlags(flagname) + + def addFlags(self, name): + column = len(self.flagWidgets) + + newbtn = QCheckBox(name) + newbtn.setChecked(False) + # calling stateChanged is inappropriate! + newbtn.clicked.connect(self.btnstate) + self.flagslayout.addWidget(newbtn, *(column, 0)) + self.flagslayout.setColumnStretch(0, 9) + + removebtn = QPushButton('X') + widgets = (newbtn, removebtn) + index = len(self.flagWidgets) + removebtn.clicked.connect(lambda : self.removeFlags(index)) + self.flagslayout.addWidget(removebtn, *(column, 1)) + self.flagslayout.setColumnStretch(1, 1) + self.flagWidgets.append(widgets) + + + def removeFlags(self, index): + # remove checkbox + btn = self.flagWidgets[index][0] + self.flagslayout.removeWidget(btn) + + # remove x button + removebtn = self.flagWidgets[index][1] + self.flagslayout.removeWidget(removebtn) + + btn.deleteLater() + removebtn.deleteLater() + del self.flagWidgets[index] + + # update argument of removeFlags + for i, removeBtn in enumerate(self.flagRemoveButtons): + removebtn.clicked.connect(lambda : self.removeFlags(i)) + + @property + def flagButtons(self): + for widgets in self.flagWidgets: + yield widgets[0] + + @property + def flagRemoveButtons(self): + for widgets in self.flagWidgets: + yield widgets[1] # Add chris def btnstate(self, item= None): @@ -826,6 +886,7 @@ def remLabel(self, shape): def loadLabels(self, shapes): s = [] + flaglist = [btn.text() for btn in self.flagButtons] for label, points, line_color, fill_color, flags in shapes: shape = Shape(label=label) for x, y in points: @@ -850,6 +911,11 @@ def loadLabels(self, shapes): else: shape.fill_color = generateColorByText(label) + # add flag if it doesn't exist + for flagname in shape.flags.keys(): + if flagname not in flaglist: + self.addFlags(flagname) + self.addLabel(shape) self.updateComboBox() self.canvas.loadShapes(s) diff --git a/labelImg.spec b/labelImg.spec deleted file mode 100644 index b52374272..000000000 --- a/labelImg.spec +++ /dev/null @@ -1,33 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - - -a = Analysis(['labelImg.py'], - pathex=['./libs', './', 'C:\\Users\\Administrator\\Anaconda3\\envs\\labelimg\\Lib\\site-packages\\PyQt5\\Qt\\bin', 'O:\\Documents\\programs\\software\\labelImg_copyfunc'], - binaries=[], - datas=[], - hiddenimports=['xml', 'xml.etree', 'xml.etree.ElementTree', 'lxml.etree', 'PyQt5.sip'], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='labelImg', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True ) diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index 652c8baa4..f8c334e75 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -156,15 +156,18 @@ def parseXML(self): except KeyError: self.verified = False + noflag_list = ['name', 'pose', 'bndbox'] for object_iter in xmltree.findall('object'): bndbox = object_iter.find("bndbox") label = object_iter.find('name').text - difficult = False - if object_iter.find('difficult') is not None: - difficult = bool(int(object_iter.find('difficult').text)) - truncated = False - if object_iter.find('truncated') is not None: - truncated = bool(int(object_iter.find('truncated').text)) - self.addShape(label, bndbox, flags={'difficult': difficult, 'truncated': truncated}) + flags = {'difficult': False, 'truncated': False} + + for obj_node in object_iter: + if obj_node.tag in noflag_list: + continue + val = bool(int(obj_node.text)) + flags[obj_node.tag] = val + + self.addShape(label, bndbox, flags=flags) return True From 7b67c2cbf0d2f71aaf0f471e0eeb79e444d1e214 Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Fri, 26 Feb 2021 19:28:43 +0900 Subject: [PATCH 4/6] shortcut c --- README.rst | 11 ----------- labelImg.py | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/README.rst b/README.rst index ee6264cf2..c1f940987 100644 --- a/README.rst +++ b/README.rst @@ -119,17 +119,6 @@ Open the Anaconda Prompt and go to the `labelImg <#labelimg>`__ directory python labelImg.py python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] -.. code:: - - conda create -n labelimg python=3.5 - pip install PyQt5==5.10.1 lxml pyinstaller - # You may delete C:~/.labelImgSettings.pkl - pyrcc5 -o libs/resources.py resources.qrc - python labelImg.py - python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] - pyinstaller --hidden-import=xml --hidden-import=xml.etree --hidden-import=xml.etree.ElementTree --hidden-import=lxml.etree --hidden-import=PyQt5.sip -D -F -n labelImg -c "./labelImg.py" -p ./libs -p ./ --paths C:\Users\Administrator\Anaconda3\envs\labelimg\Lib\site-packages\PyQt5\Qt\bin - - Get from PyPI but only python3.0 or above ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is the simplest (one-command) install method on modern Linux distributions such as Ubuntu and Fedora. diff --git a/labelImg.py b/labelImg.py index 90a4d5ff8..f252056b0 100755 --- a/labelImg.py +++ b/labelImg.py @@ -157,14 +157,6 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa self.addFlags(getStr('useDifficult')) self.addFlags(getStr('useTruncated')) - # * - # * dhzs 2017-12-2 add copy button - # * - self.copy_prev_button = QPushButton('Copy the previous label') - self.copy_prev_button.setShortcut('c') - self.copy_prev_button.clicked.connect(self.copyPreviousBoundingBoxes) - listLayout.addWidget(self.copy_prev_button) - # Add some of widgets to listLayout listLayout.addWidget(self.flagGroupBox) listLayout.addWidget(self.editButton) @@ -244,7 +236,7 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa 'Ctrl+u', 'open', getStr('openDir')) copyPrevBounding = action(getStr('copyPrevBounding'), self.copyPreviousBoundingBoxes, - 'Ctrl+v', 'paste', getStr('copyPrevBounding')) + 'c', 'paste', getStr('copyPrevBounding')) changeSavedir = action(getStr('changeSaveDir'), self.changeSavedirDialog, 'Ctrl+r', 'open', getStr('changeSavedAnnotationDir')) From cd958284c5ef7d226e207d85fd3d44a7b432bfb6 Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Mon, 1 Mar 2021 18:20:25 +0900 Subject: [PATCH 5/6] add flag setting, saving bug fixed --- labelImg.py | 25 ++++++++++++++++++------- libs/constants.py | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/labelImg.py b/labelImg.py index f252056b0..993e72809 100755 --- a/labelImg.py +++ b/labelImg.py @@ -154,8 +154,9 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa self.flagWidgets = [] # add difficult and truncated flag by default - self.addFlags(getStr('useDifficult')) - self.addFlags(getStr('useTruncated')) + flaglist = settings.get(SETTING_FLAGS_INFO, [getStr('useDifficult'), getStr('useTruncated')]) + for flagname in flaglist: + self.addFlags(flagname) # Add some of widgets to listLayout listLayout.addWidget(self.flagGroupBox) @@ -878,7 +879,7 @@ def remLabel(self, shape): def loadLabels(self, shapes): s = [] - flaglist = [btn.text() for btn in self.flagButtons] + flagset = set([btn.text() for btn in self.flagButtons]) for label, points, line_color, fill_color, flags in shapes: shape = Shape(label=label) for x, y in points: @@ -903,10 +904,17 @@ def loadLabels(self, shapes): else: shape.fill_color = generateColorByText(label) - # add flag if it doesn't exist - for flagname in shape.flags.keys(): - if flagname not in flaglist: - self.addFlags(flagname) + shape_flagset = set(shape.flags.keys()) + # setDirty if the xml file's flag doesn't exist + for flagname in (flagset - shape_flagset): + shape.flags[flagname] = False + self.setDirty() + + # add flag to rightdock if it doesn't exist + for flagname in (shape_flagset - flagset): + flagset.add(flagname) + self.addFlags(flagname) + self.setDirty() self.addLabel(shape) self.updateComboBox() @@ -1293,6 +1301,9 @@ def closeEvent(self, event): settings[SETTING_PAINT_LABEL] = self.displayLabelOption.isChecked() settings[SETTING_DRAW_SQUARE] = self.drawSquaresOption.isChecked() settings[SETTING_LABEL_FILE_FORMAT] = self.labelFileFormat + + # save flag info + settings[SETTING_FLAGS_INFO] = list(self.flags.keys()) settings.save() def loadRecent(self, filename): diff --git a/libs/constants.py b/libs/constants.py index 1efda037c..eb389367b 100644 --- a/libs/constants.py +++ b/libs/constants.py @@ -18,3 +18,4 @@ SETTING_DRAW_SQUARE = 'draw/square' SETTING_LABEL_FILE_FORMAT= 'labelFileFormat' DEFAULT_ENCODING = 'utf-8' +SETTING_FLAGS_INFO='flagsinfo' From 795e993f5875253030aba31ea83a1d6abd998916 Mon Sep 17 00:00:00 2001 From: jjjkkkjjj-mizuno Date: Tue, 2 Mar 2021 12:49:23 +0900 Subject: [PATCH 6/6] add discard unnecessary flags function --- labelImg.py | 53 +++++++++++++++++++--------- libs/constants.py | 1 + libs/shape.py | 4 +++ resources/strings/strings.properties | 1 + 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/labelImg.py b/labelImg.py index 993e72809..4cfbdd3f8 100755 --- a/labelImg.py +++ b/labelImg.py @@ -149,6 +149,11 @@ def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSa addflagLayout.addWidget(self.addflag_button, 1) vbox.addLayout(addflagLayout) + self.discardUnnecessaryFlagsCheckbox = QCheckBox(getStr('discardFlags')) + self.discardUnnecessaryFlagsCheckbox.setChecked(settings.get(SETTING_DISCARD_FLAGS, False)) + self.discardUnnecessaryFlagsCheckbox.clicked.connect(self.btnstate) + vbox.addWidget(self.discardUnnecessaryFlagsCheckbox) + self.flagGroupBox.setLayout(vbox) # list of tuple(checkbox, x button) self.flagWidgets = [] @@ -767,40 +772,51 @@ def addflag_button_pushed(self): self.addFlags(flagname) def addFlags(self, name): - column = len(self.flagWidgets) + index = len(self.flagWidgets) newbtn = QCheckBox(name) newbtn.setChecked(False) # calling stateChanged is inappropriate! newbtn.clicked.connect(self.btnstate) - self.flagslayout.addWidget(newbtn, *(column, 0)) + newbtn.setProperty('colpos', index) + self.flagslayout.addWidget(newbtn, *(index, 0)) self.flagslayout.setColumnStretch(0, 9) removebtn = QPushButton('X') widgets = (newbtn, removebtn) - index = len(self.flagWidgets) - removebtn.clicked.connect(lambda : self.removeFlags(index)) - self.flagslayout.addWidget(removebtn, *(column, 1)) + removebtn.setProperty('colpos', index) + removebtn.clicked.connect(lambda : self.removeFlags(removebtn.property('colpos'))) + self.flagslayout.addWidget(removebtn, *(index, 1)) self.flagslayout.setColumnStretch(1, 1) self.flagWidgets.append(widgets) def removeFlags(self, index): + # remove widget by index totally + flaglineedit, removebtn = self.flagWidgets[index] # remove checkbox - btn = self.flagWidgets[index][0] - self.flagslayout.removeWidget(btn) + self.flagslayout.removeWidget(flaglineedit) + flaglineedit.setParent(None) # remove x button - removebtn = self.flagWidgets[index][1] self.flagslayout.removeWidget(removebtn) + removebtn.setParent(None) - btn.deleteLater() + flaglineedit.deleteLater() removebtn.deleteLater() del self.flagWidgets[index] - # update argument of removeFlags - for i, removeBtn in enumerate(self.flagRemoveButtons): - removebtn.clicked.connect(lambda : self.removeFlags(i)) + # update position + for i, (flaglineedit, removebtn) in enumerate(self.flagWidgets): + # update property + flaglineedit.setProperty('colpos', i) + removebtn.setProperty('colpos', i) + + # update flaglayout position + self.flagslayout.addWidget(flaglineedit, *(i, 0)) + self.flagslayout.addWidget(removebtn, *(i, 1)) + + self.setDirty() @property def flagButtons(self): @@ -911,10 +927,14 @@ def loadLabels(self, shapes): self.setDirty() # add flag to rightdock if it doesn't exist - for flagname in (shape_flagset - flagset): - flagset.add(flagname) - self.addFlags(flagname) - self.setDirty() + if self.discardUnnecessaryFlagsCheckbox.isChecked(): + for flagname in (shape_flagset - flagset): + del shape.flags[flagname] + self.setDirty() + else: + for flagname in (shape_flagset - flagset): + flagset.add(flagname) + self.addFlags(flagname) self.addLabel(shape) self.updateComboBox() @@ -1304,6 +1324,7 @@ def closeEvent(self, event): # save flag info settings[SETTING_FLAGS_INFO] = list(self.flags.keys()) + settings[SETTING_DISCARD_FLAGS] = self.discardUnnecessaryFlagsCheckbox.isChecked() settings.save() def loadRecent(self, filename): diff --git a/libs/constants.py b/libs/constants.py index eb389367b..7a6c82432 100644 --- a/libs/constants.py +++ b/libs/constants.py @@ -19,3 +19,4 @@ SETTING_LABEL_FILE_FORMAT= 'labelFileFormat' DEFAULT_ENCODING = 'utf-8' SETTING_FLAGS_INFO='flagsinfo' +SETTING_DISCARD_FLAGS='discardflags' diff --git a/libs/shape.py b/libs/shape.py index 316966047..a0d039e0f 100644 --- a/libs/shape.py +++ b/libs/shape.py @@ -210,6 +210,10 @@ def setChangedFlags(self, newflags): if newkey not in self.flags.keys() or self.flags[newkey] != newval: isChanged = True break + # check to discard unnecessary flags + newflags_set = set(newflags.keys()) + origflags_set = set(self.flags.keys()) + isChanged = isChanged or len(newflags_set.symmetric_difference(origflags_set)) > 0 if isChanged: self.flags = newflags diff --git a/resources/strings/strings.properties b/resources/strings/strings.properties index 896c677fb..fe31ea47b 100644 --- a/resources/strings/strings.properties +++ b/resources/strings/strings.properties @@ -56,6 +56,7 @@ showHide=Show/Hide Label Panel useDefaultLabel=Use default label useDifficult=difficult useTruncated=truncated +discardFlags=Discard the unnecessary flags boxLabelText=Box Labels labels=Labels autoSaveMode=Auto Save mode