Merge pull request #1975 from Evezerest/dy1
Add new batch processing and undo functions
This commit is contained in:
commit
e7ef5ee49e
|
@ -206,7 +206,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.labelList = EditInList()
|
||||
labelListContainer = QWidget()
|
||||
labelListContainer.setLayout(listLayout)
|
||||
self.labelList.itemActivated.connect(self.labelSelectionChanged)
|
||||
#self.labelList.itemActivated.connect(self.labelSelectionChanged)
|
||||
self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)
|
||||
self.labelList.clicked.connect(self.labelList.item_clicked)
|
||||
# Connect to itemChanged to detect checkbox changes.
|
||||
|
@ -219,7 +219,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
################## detection box ####################
|
||||
self.BoxList = QListWidget()
|
||||
|
||||
self.BoxList.itemActivated.connect(self.boxSelectionChanged)
|
||||
#self.BoxList.itemActivated.connect(self.boxSelectionChanged)
|
||||
self.BoxList.itemSelectionChanged.connect(self.boxSelectionChanged)
|
||||
self.BoxList.itemDoubleClicked.connect(self.editBox)
|
||||
# Connect to itemChanged to detect checkbox changes.
|
||||
|
@ -435,7 +435,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
|
||||
######## New actions #######
|
||||
AutoRec = action(getStr('autoRecognition'), self.autoRecognition,
|
||||
'Ctrl+Shift+A', 'Auto', getStr('autoRecognition'), enabled=False)
|
||||
'', 'Auto', getStr('autoRecognition'), enabled=False)
|
||||
|
||||
reRec = action(getStr('reRecognition'), self.reRecognition,
|
||||
'Ctrl+Shift+R', 'reRec', getStr('reRecognition'), enabled=False)
|
||||
|
@ -444,7 +444,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
'Ctrl+R', 'reRec', getStr('singleRe'), enabled=False)
|
||||
|
||||
createpoly = action(getStr('creatPolygon'), self.createPolygon,
|
||||
'q', 'new', 'Creat Polygon', enabled=True)
|
||||
'q', 'new', getStr('creatPolygon'), enabled=True)
|
||||
|
||||
saveRec = action(getStr('saveRec'), self.saveRecResult,
|
||||
'', 'save', getStr('saveRec'), enabled=False)
|
||||
|
@ -452,6 +452,12 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
saveLabel = action(getStr('saveLabel'), self.saveLabelFile, #
|
||||
'Ctrl+S', 'save', getStr('saveLabel'), enabled=False)
|
||||
|
||||
undoLastPoint = action(getStr("undoLastPoint"), self.canvas.undoLastPoint,
|
||||
'Ctrl+Z', "undo", getStr("undoLastPoint"), enabled=False)
|
||||
|
||||
undo = action(getStr("undo"), self.undoShapeEdit,
|
||||
'Ctrl+Z', "undo", getStr("undo"), enabled=False)
|
||||
|
||||
self.editButton.setDefaultAction(edit)
|
||||
self.newButton.setDefaultAction(create)
|
||||
self.DelButton.setDefaultAction(deleteImg)
|
||||
|
@ -512,10 +518,11 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg,
|
||||
fitWindow=fitWindow, fitWidth=fitWidth,
|
||||
zoomActions=zoomActions, saveLabel=saveLabel,
|
||||
undo=undo, undoLastPoint=undoLastPoint,
|
||||
fileMenuActions=(
|
||||
opendir, saveLabel, resetAll, quit),
|
||||
beginner=(), advanced=(),
|
||||
editMenu=(createpoly, edit, copy, delete,singleRere,
|
||||
editMenu=(createpoly, edit, copy, delete,singleRere,None, undo, undoLastPoint,
|
||||
None, color1, self.drawSquaresOption),
|
||||
beginnerContext=(create, edit, copy, delete, singleRere),
|
||||
advancedContext=(createMode, editMode, edit, copy,
|
||||
|
@ -549,8 +556,13 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.labelDialogOption.setChecked(settings.get(SETTING_PAINT_LABEL, False))
|
||||
self.labelDialogOption.triggered.connect(self.speedChoose)
|
||||
|
||||
self.autoSaveOption = QAction(getStr('autoSaveMode'), self)
|
||||
self.autoSaveOption.setCheckable(True)
|
||||
self.autoSaveOption.setChecked(settings.get(SETTING_PAINT_LABEL, False))
|
||||
self.autoSaveOption.triggered.connect(self.autoSaveFunc)
|
||||
|
||||
addActions(self.menus.file,
|
||||
(opendir, None, saveLabel, saveRec, None, resetAll, deleteImg, quit))
|
||||
(opendir, None, saveLabel, saveRec, self.autoSaveOption, None, resetAll, deleteImg, quit))
|
||||
|
||||
addActions(self.menus.help, (showSteps, showInfo))
|
||||
addActions(self.menus.view, (
|
||||
|
@ -566,9 +578,9 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
|
||||
# Custom context menu for the canvas widget:
|
||||
addActions(self.canvas.menus[0], self.actions.beginnerContext)
|
||||
addActions(self.canvas.menus[1], (
|
||||
action('&Copy here', self.copyShape),
|
||||
action('&Move here', self.moveShape)))
|
||||
#addActions(self.canvas.menus[1], (
|
||||
# action('&Copy here', self.copyShape),
|
||||
# action('&Move here', self.moveShape)))
|
||||
|
||||
|
||||
self.statusBar().showMessage('%s started.' % __appname__)
|
||||
|
@ -758,6 +770,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.canvas.setEditing(False)
|
||||
self.canvas.fourpoint = True
|
||||
self.actions.create.setEnabled(False)
|
||||
self.actions.undoLastPoint.setEnabled(True)
|
||||
|
||||
def toggleDrawingSensitive(self, drawing=True):
|
||||
"""In the middle of drawing, toggling between modes should be disabled."""
|
||||
|
@ -866,10 +879,11 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.updateComboBox()
|
||||
|
||||
def updateBoxlist(self):
|
||||
shape = self.canvas.selectedShape
|
||||
for shape in self.canvas.selectedShapes+[self.canvas.hShape]:
|
||||
item = self.shapesToItemsbox[shape] # listitem
|
||||
text = [(int(p.x()), int(p.y())) for p in shape.points]
|
||||
item.setText(str(text))
|
||||
self.actions.undo.setEnabled(True)
|
||||
self.setDirty()
|
||||
|
||||
def indexTo5Files(self, currIndex):
|
||||
|
@ -902,23 +916,27 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
if len(self.mImgList) > 0:
|
||||
self.zoomWidget.setValue(self.zoomWidgetValue + self.imgsplider.value())
|
||||
|
||||
# React to canvas signals.
|
||||
def shapeSelectionChanged(self, selected=False):
|
||||
if self._noSelectionSlot:
|
||||
self._noSelectionSlot = False
|
||||
else:
|
||||
shape = self.canvas.selectedShape
|
||||
if shape:
|
||||
self.shapesToItems[shape].setSelected(True)
|
||||
self.shapesToItemsbox[shape].setSelected(True) # ADD
|
||||
else:
|
||||
|
||||
def shapeSelectionChanged(self, selected_shapes):
|
||||
self._noSelectionSlot = True
|
||||
for shape in self.canvas.selectedShapes:
|
||||
shape.selected = False
|
||||
self.labelList.clearSelection()
|
||||
self.actions.delete.setEnabled(selected)
|
||||
self.actions.copy.setEnabled(selected)
|
||||
self.actions.edit.setEnabled(selected)
|
||||
self.actions.shapeLineColor.setEnabled(selected)
|
||||
self.actions.shapeFillColor.setEnabled(selected)
|
||||
self.actions.singleRere.setEnabled(selected)
|
||||
self.canvas.selectedShapes = selected_shapes
|
||||
for shape in self.canvas.selectedShapes:
|
||||
shape.selected = True
|
||||
self.shapesToItems[shape].setSelected(True)
|
||||
self.shapesToItemsbox[shape].setSelected(True)
|
||||
|
||||
self.labelList.scrollToItem(self.currentItem()) # QAbstractItemView.EnsureVisible
|
||||
self.BoxList.scrollToItem(self.currentBox())
|
||||
|
||||
self._noSelectionSlot = False
|
||||
n_selected = len(selected_shapes)
|
||||
self.actions.singleRere.setEnabled(n_selected)
|
||||
self.actions.delete.setEnabled(n_selected)
|
||||
self.actions.copy.setEnabled(n_selected)
|
||||
self.actions.edit.setEnabled(n_selected == 1)
|
||||
|
||||
def addLabel(self, shape):
|
||||
shape.paintLabel = self.displayLabelOption.isChecked()
|
||||
|
@ -941,10 +959,11 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
action.setEnabled(True)
|
||||
self.updateComboBox()
|
||||
|
||||
def remLabel(self, shape):
|
||||
if shape is None:
|
||||
def remLabels(self, shapes):
|
||||
if shapes is None:
|
||||
# print('rm empty label')
|
||||
return
|
||||
for shape in shapes:
|
||||
item = self.shapesToItems[shape]
|
||||
self.labelList.takeItem(self.labelList.row(item))
|
||||
del self.shapesToItems[shape]
|
||||
|
@ -1001,7 +1020,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
item.setText(str([(int(p.x()), int(p.y())) for p in shape.points]))
|
||||
self.updateComboBox()
|
||||
|
||||
def updateComboBox(self):
|
||||
def updateComboBox(self): # TODO:貌似没用
|
||||
# Get the unique labels and add them to the Combobox.
|
||||
itemsTextList = [str(self.labelList.item(i).text()) for i in range(self.labelList.count())]
|
||||
|
||||
|
@ -1054,26 +1073,38 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
return False
|
||||
|
||||
def copySelectedShape(self):
|
||||
self.addLabel(self.canvas.copySelectedShape())
|
||||
for shape in self.canvas.copySelectedShape():
|
||||
self.addLabel(shape)
|
||||
# fix copy and delete
|
||||
self.shapeSelectionChanged(True)
|
||||
#self.shapeSelectionChanged(True)
|
||||
|
||||
|
||||
def labelSelectionChanged(self):
|
||||
item = self.currentItem()
|
||||
self.labelList.scrollToItem(item, QAbstractItemView.EnsureVisible)
|
||||
if item and self.canvas.editing():
|
||||
self._noSelectionSlot = True
|
||||
self.canvas.selectShape(self.itemsToShapes[item])
|
||||
shape = self.itemsToShapes[item]
|
||||
if self._noSelectionSlot:
|
||||
return
|
||||
if self.canvas.editing():
|
||||
selected_shapes = []
|
||||
for item in self.labelList.selectedItems():
|
||||
selected_shapes.append(self.itemsToShapes[item])
|
||||
if selected_shapes:
|
||||
self.canvas.selectShapes(selected_shapes)
|
||||
else:
|
||||
self.canvas.deSelectShape()
|
||||
|
||||
|
||||
def boxSelectionChanged(self):
|
||||
item = self.currentBox()
|
||||
self.BoxList.scrollToItem(item, QAbstractItemView.EnsureVisible)
|
||||
if item and self.canvas.editing():
|
||||
self._noSelectionSlot = True
|
||||
self.canvas.selectShape(self.itemsToShapesbox[item])
|
||||
shape = self.itemsToShapesbox[item]
|
||||
if self._noSelectionSlot:
|
||||
#self.BoxList.scrollToItem(self.currentBox(), QAbstractItemView.PositionAtCenter)
|
||||
return
|
||||
if self.canvas.editing():
|
||||
selected_shapes = []
|
||||
for item in self.BoxList.selectedItems():
|
||||
selected_shapes.append(self.itemsToShapesbox[item])
|
||||
if selected_shapes:
|
||||
self.canvas.selectShapes(selected_shapes)
|
||||
else:
|
||||
self.canvas.deSelectShape()
|
||||
|
||||
|
||||
def labelItemChanged(self, item):
|
||||
shape = self.itemsToShapes[item]
|
||||
|
@ -1113,6 +1144,8 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
if self.beginner(): # Switch to edit mode.
|
||||
self.canvas.setEditing(True)
|
||||
self.actions.create.setEnabled(True)
|
||||
self.actions.undoLastPoint.setEnabled(False)
|
||||
self.actions.undo.setEnabled(True)
|
||||
else:
|
||||
self.actions.editMode.setEnabled(True)
|
||||
self.setDirty()
|
||||
|
@ -1548,6 +1581,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.fileListWidget.insertItem(int(currIndex), item)
|
||||
self.openNextImg()
|
||||
self.actions.saveRec.setEnabled(True)
|
||||
self.actions.saveLabel.setEnabled(True)
|
||||
|
||||
elif mode == 'Auto':
|
||||
if annotationFilePath and self.saveLabels(annotationFilePath, mode=mode):
|
||||
|
@ -1643,7 +1677,8 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.setDirty()
|
||||
|
||||
def deleteSelectedShape(self):
|
||||
self.remLabel(self.canvas.deleteSelected())
|
||||
self.remLabels(self.canvas.deleteSelected())
|
||||
self.actions.undo.setEnabled(True)
|
||||
self.setDirty()
|
||||
if self.noShapes():
|
||||
for action in self.actions.onShapesPresent:
|
||||
|
@ -1653,7 +1688,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
|
||||
default=DEFAULT_LINE_COLOR)
|
||||
if color:
|
||||
self.canvas.selectedShape.line_color = color
|
||||
for shape in self.canvas.selectedShapes: shape.line_color = color
|
||||
self.canvas.update()
|
||||
self.setDirty()
|
||||
|
||||
|
@ -1661,7 +1696,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
color = self.colorDialog.getColor(self.fillColor, u'Choose fill color',
|
||||
default=DEFAULT_FILL_COLOR)
|
||||
if color:
|
||||
self.canvas.selectedShape.fill_color = color
|
||||
for shape in self.canvas.selectedShapes: shape.fill_color = color
|
||||
self.canvas.update()
|
||||
self.setDirty()
|
||||
|
||||
|
@ -1785,7 +1820,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
|
||||
def singleRerecognition(self):
|
||||
img = cv2.imread(self.filePath)
|
||||
shape = self.canvas.selectedShape
|
||||
for shape in self.canvas.selectedShapes:
|
||||
box = [[int(p.x()), int(p.y())] for p in shape.points]
|
||||
assert len(box) == 4
|
||||
img_crop = get_rotate_crop_image(img, np.array(box, np.float32))
|
||||
|
@ -1914,8 +1949,8 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.savePPlabel()
|
||||
|
||||
def saveRecResult(self):
|
||||
if None in [self.PPlabelpath, self.PPlabel, self.fileStatedict]:
|
||||
QMessageBox.information(self, "Information", "Save file first")
|
||||
if {} in [self.PPlabelpath, self.PPlabel, self.fileStatedict]:
|
||||
QMessageBox.information(self, "Information", "Check the image first")
|
||||
return
|
||||
|
||||
rec_gt_dir = os.path.dirname(self.PPlabelpath) + '/rec_gt.txt'
|
||||
|
@ -1953,6 +1988,33 @@ class MainWindow(QMainWindow, WindowMixin):
|
|||
self.canvas.newShape.disconnect()
|
||||
self.canvas.newShape.connect(partial(self.newShape, False))
|
||||
|
||||
def autoSaveFunc(self):
|
||||
if self.autoSaveOption.isChecked():
|
||||
self.autoSaveNum = 1 # Real auto_Save
|
||||
try:
|
||||
self.saveLabelFile()
|
||||
except:
|
||||
pass
|
||||
print('The program will automatically save once after confirming an image')
|
||||
else:
|
||||
self.autoSaveNum = 5 # Used for backup
|
||||
print('The program will automatically save once after confirming 5 images (default)')
|
||||
|
||||
def undoShapeEdit(self):
|
||||
self.canvas.restoreShape()
|
||||
self.labelList.clear()
|
||||
self.BoxList.clear()
|
||||
self.loadShapes(self.canvas.shapes)
|
||||
self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
|
||||
|
||||
def loadShapes(self, shapes, replace=True):
|
||||
self._noSelectionSlot = True
|
||||
for shape in shapes:
|
||||
self.addLabel(shape)
|
||||
self.labelList.clearSelection()
|
||||
self._noSelectionSlot = False
|
||||
self.canvas.loadShapes(shapes, replace=replace)
|
||||
|
||||
|
||||
def inverted(color):
|
||||
return QColor(*[255 - v for v in color.getRgb()])
|
||||
|
|
|
@ -8,6 +8,10 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w
|
|||
|
||||
### Recent Update
|
||||
|
||||
- 2021.2.5: New batch processing and undo functions (by [Evezerest](https://github.com/Evezerest)):
|
||||
- Batch processing function: Press and hold the Ctrl key to select the box, you can move, copy, and delete in batches.
|
||||
- Undo function: In the process of drawing a four-point label box or after editing the box, press Ctrl+Z to undo the previous operation.
|
||||
- Fix image rotation and size problems, optimize the process of editing the mark frame (by [ninetailskim](https://github.com/ninetailskim)、 [edencfc](https://github.com/edencfc)).
|
||||
- 2021.1.11: Optimize the labeling experience (by [edencfc](https://github.com/edencfc)),
|
||||
- Users can choose whether to pop up the label input dialog after drawing the detection box in "View - Pop-up Label Input Dialog".
|
||||
- The recognition result scrolls synchronously when users click related detection box.
|
||||
|
@ -16,7 +20,6 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w
|
|||
|
||||
### TODO:
|
||||
- Lock box mode: For the same scene data, the size and position of the locked detection box can be transferred between different pictures.
|
||||
- Experience optimization: Add undo, batch operation include move, copy, delete and so on, optimize the annotation process.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -76,12 +79,11 @@ python3 PPOCRLabel.py
|
|||
|
||||
7. Double click the result in 'recognition result' list to manually change inaccurate recognition results.
|
||||
|
||||
8. Click "Check", the image status will switch to "√",then the program automatically jump to the next(The results will not be written directly to the file at this time).
|
||||
8. Click "Check", the image status will switch to "√",then the program automatically jump to the next.
|
||||
|
||||
9. Click "Delete Image" and the image will be deleted to the recycle bin.
|
||||
|
||||
10. Labeling result: the user can save manually through the menu "File - Save Label", while the program will also save automatically after every 5 images confirmed by the user.the manually checked label will be stored in *Label.txt* under the opened picture folder.
|
||||
Click "PaddleOCR"-"Save Recognition Results" in the menu bar, the recognition training data of such pictures will be saved in the *crop_img* folder, and the recognition label will be saved in *rec_gt.txt*<sup>[4]</sup>.
|
||||
10. Labeling result: the user can save manually through the menu "File - Save Label", while the program will also save automatically if "File - Auto Save Label Mode" is selected. The manually checked label will be stored in *Label.txt* under the opened picture folder. Click "PaddleOCR"-"Save Recognition Results" in the menu bar, the recognition training data of such pictures will be saved in the *crop_img* folder, and the recognition label will be saved in *rec_gt.txt*<sup>[4]</sup>.
|
||||
|
||||
### Note
|
||||
|
||||
|
@ -89,8 +91,7 @@ python3 PPOCRLabel.py
|
|||
|
||||
[2] The image status indicates whether the user has saved the image manually. If it has not been saved manually it is "X", otherwise it is "√", PPOCRLabel will not relabel pictures with a status of "√".
|
||||
|
||||
[3] After clicking "Re-recognize", the model will overwrite ALL recognition results in the picture.
|
||||
Therefore, if the recognition result has been manually changed before, it may change after re-recognition.
|
||||
[3] After clicking "Re-recognize", the model will overwrite ALL recognition results in the picture. Therefore, if the recognition result has been manually changed before, it may change after re-recognition.
|
||||
|
||||
[4] The files produced by PPOCRLabel can be found under the opened picture folder including the following, please do not manually change the contents, otherwise it will cause the program to be abnormal.
|
||||
|
||||
|
@ -107,13 +108,14 @@ Therefore, if the recognition result has been manually changed before, it may ch
|
|||
### Shortcut keys
|
||||
|
||||
| Shortcut keys | Description |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| Ctrl + shift + A | Automatically label all unchecked images |
|
||||
| Ctrl + shift + R | Re-recognize all the labels of the current image |
|
||||
| ------------------------ | ------------------------------------------------ |
|
||||
| Ctrl + Shift + R | Re-recognize all the labels of the current image |
|
||||
| W | Create a rect box |
|
||||
| Q | Create a four-points box |
|
||||
| Ctrl + E | Edit label of the selected box |
|
||||
| Ctrl + R | Re-recognize the selected box |
|
||||
| Ctrl + C | Copy and paste the selected box |
|
||||
| Ctrl + Left Mouse Button | Multi select the label box |
|
||||
| Backspace | Delete the selected box |
|
||||
| Ctrl + V | Check image |
|
||||
| Ctrl + Shift + d | Delete image |
|
||||
|
@ -136,7 +138,7 @@ Therefore, if the recognition result has been manually changed before, it may ch
|
|||
|
||||
PPOCRLabel supports three ways to save Label.txt
|
||||
|
||||
- Automatically save: When it detects that the user has manually checked 5 pictures, the program automatically writes the annotations into Label.txt. The user can change the value of ``self.autoSaveNum`` in ``PPOCRLabel.py`` to set the number of images to be automatically saved after confirmation.
|
||||
- Automatically save: After selecting "File - Auto Save Label Mode", the program will automatically write the annotations into Label.txt every time the user confirms an image. If this option is not turned on, it will be automatically saved after detecting that the user has manually checked 5 images.
|
||||
- Manual save: Click "File-Save Marking Results" to manually save the label.
|
||||
- Close application save
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
|
|||
|
||||
#### 近期更新
|
||||
|
||||
- 2021.2.5:新增批处理与撤销功能(by [Evezerest](https://github.com/Evezerest))
|
||||
- 批处理功能:按住Ctrl键选择标记框后可批量移动、复制、删除。
|
||||
- 撤销功能:在绘制四点标注框过程中或对框进行编辑操作后,按下Ctrl+Z可撤销上一部操作。
|
||||
- 修复图像旋转和尺寸问题、优化编辑标记框过程(by [ninetailskim](https://github.com/ninetailskim)、 [edencfc](https://github.com/edencfc))
|
||||
- 2021.1.11:优化标注体验(by [edencfc](https://github.com/edencfc)):
|
||||
- 用户可在“视图 - 弹出标记输入框”选择在画完检测框后标记输入框是否弹出。
|
||||
- 识别结果与检测框同步滚动。
|
||||
|
@ -17,9 +21,8 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
|
|||
#### 尽请期待
|
||||
|
||||
- 锁定框模式:针对同一场景数据,被锁定的检测框的大小与位置能在不同图片之间传递。
|
||||
- 体验优化:增加撤销操作,批量移动、复制、删除等功能。优化标注流程。
|
||||
|
||||
如果您对以上内容感兴趣或对完善工具有不一样的想法,欢迎加入我们的队伍与我们共同开发
|
||||
如果您对以上内容感兴趣或对完善工具有不一样的想法,欢迎加入我们的SIG队伍与我们共同开发。可以在[此处](https://github.com/PaddlePaddle/PaddleOCR/issues/1728)完成问卷和前置任务,经过我们确认相关内容后即可正式加入,享受SIG福利,共同为OCR开源事业贡献(特别说明:针对PPOCRLabel的改进也属于PaddleOCR前置任务)
|
||||
|
||||
|
||||
## 安装
|
||||
|
@ -65,9 +68,9 @@ python3 PPOCRLabel.py --lang ch
|
|||
5. 标记框绘制完成后,用户点击 “确认”,检测框会先被预分配一个 “待识别” 标签。
|
||||
6. 重新识别:将图片中的所有检测画绘制/调整完成后,点击 “重新识别”,PPOCR模型会对当前图片中的**所有检测框**重新识别<sup>[3]</sup>。
|
||||
7. 内容更改:双击识别结果,对不准确的识别结果进行手动更改。
|
||||
8. 确认标记:点击 “确认”,图片状态切换为 “√”,跳转至下一张(此时不会直接将结果写入文件)。
|
||||
8. **确认标记**:点击 “确认”,图片状态切换为 “√”,跳转至下一张。
|
||||
9. 删除:点击 “删除图像”,图片将会被删除至回收站。
|
||||
10. 保存结果:用户可以通过菜单中“文件-保存标记结果”手动保存,同时程序也会在用户每确认5张图片后自动保存一次。手动确认过的标记将会被存放在所打开图片文件夹下的*Label.txt*中。在菜单栏点击 “文件” - "保存识别结果"后,会将此类图片的识别训练数据保存在*crop_img*文件夹下,识别标签保存在*rec_gt.txt*中<sup>[4]</sup>。
|
||||
10. 保存结果:用户可以通过菜单中“文件-保存标记结果”手动保存,同时也可以点击“文件 - 自动保存标记结果”开启自动保存。手动确认过的标记将会被存放在所打开图片文件夹下的*Label.txt*中。在菜单栏点击 “文件” - "保存识别结果"后,会将此类图片的识别训练数据保存在*crop_img*文件夹下,识别标签保存在*rec_gt.txt*中<sup>[4]</sup>。
|
||||
|
||||
### 注意
|
||||
|
||||
|
@ -93,12 +96,13 @@ python3 PPOCRLabel.py --lang ch
|
|||
|
||||
| 快捷键 | 说明 |
|
||||
| ---------------- | ---------------------------- |
|
||||
| Ctrl + shift + A | 自动标注所有未确认过的图片 |
|
||||
| Ctrl + shift + R | 对当前图片的所有标记重新识别 |
|
||||
| W | 新建矩形框 |
|
||||
| Q | 新建四点框 |
|
||||
| Ctrl + E | 编辑所选框标签 |
|
||||
| Ctrl + R | 重新识别所选标记 |
|
||||
| Ctrl + C | 复制并粘贴选中的标记框 |
|
||||
| Ctrl + 鼠标左键 | 多选标记框 |
|
||||
| Backspace | 删除所选框 |
|
||||
| Ctrl + V | 确认本张图片标记 |
|
||||
| Ctrl + Shift + d | 删除本张图片 |
|
||||
|
@ -120,7 +124,7 @@ python3 PPOCRLabel.py --lang ch
|
|||
|
||||
PPOCRLabel支持三种保存方式:
|
||||
|
||||
- 程序自动保存:当检测到用户手动确认过5张图片后,程序自动将标记结果写入Label.txt中。其中用户可通过更改```PPOCRLabel.py```中的```self.autoSaveNum```的数值设置确认几张图片后进行自动保存。
|
||||
- 自动保存:点击“文件 - 自动保存标记结果”后,用户每确认过一张图片,程序自动将标记结果写入Label.txt中。若未开启此选项,则检测到用户手动确认过5张图片后进行自动保存。
|
||||
- 手动保存:点击“文件 - 保存标记结果”手动保存标记。
|
||||
- 关闭应用程序保存
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ class Canvas(QWidget):
|
|||
zoomRequest = pyqtSignal(int)
|
||||
scrollRequest = pyqtSignal(int, int)
|
||||
newShape = pyqtSignal()
|
||||
selectionChanged = pyqtSignal(bool)
|
||||
# selectionChanged = pyqtSignal(bool)
|
||||
selectionChanged = pyqtSignal(list)
|
||||
shapeMoved = pyqtSignal()
|
||||
drawingPolygon = pyqtSignal(bool)
|
||||
|
||||
|
@ -51,9 +52,11 @@ class Canvas(QWidget):
|
|||
# Initialise local state.
|
||||
self.mode = self.EDIT
|
||||
self.shapes = []
|
||||
self.shapesBackups = []
|
||||
self.current = None
|
||||
self.selectedShapes = []
|
||||
self.selectedShape = None # save the selected shape here
|
||||
self.selectedShapeCopy = None
|
||||
self.selectedShapesCopy = []
|
||||
self.drawingLineColor = QColor(0, 0, 255)
|
||||
self.drawingRectColor = QColor(0, 0, 255)
|
||||
self.line = Shape(line_color=self.drawingLineColor)
|
||||
|
@ -77,6 +80,7 @@ class Canvas(QWidget):
|
|||
self.drawSquare = False
|
||||
self.fourpoint = True # ADD
|
||||
self.pointnum = 0
|
||||
self.movingShape = False
|
||||
|
||||
#initialisation for panning
|
||||
self.pan_initial_pos = QPoint()
|
||||
|
@ -149,37 +153,20 @@ class Canvas(QWidget):
|
|||
clipped_x = min(max(0, pos.x()), size.width())
|
||||
clipped_y = min(max(0, pos.y()), size.height())
|
||||
pos = QPointF(clipped_x, clipped_y)
|
||||
elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]) and not self.fourpoint:
|
||||
|
||||
elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
|
||||
# Attract line to starting point and colorise to alert the
|
||||
# user:
|
||||
pos = self.current[0]
|
||||
color = self.current.line_color
|
||||
self.overrideCursor(CURSOR_POINT)
|
||||
self.current.highlightVertex(0, Shape.NEAR_VERTEX)
|
||||
elif ( # ADD
|
||||
len(self.current) > 1
|
||||
and self.fourpoint
|
||||
and self.closeEnough(pos, self.current[0])
|
||||
):
|
||||
# Attract line to starting point and
|
||||
# colorise to alert the user.
|
||||
pos = self.current[0]
|
||||
self.overrideCursor(CURSOR_POINT)
|
||||
self.current.highlightVertex(0, Shape.NEAR_VERTEX)
|
||||
|
||||
|
||||
if self.drawSquare:
|
||||
initPos = self.current[0]
|
||||
minX = initPos.x()
|
||||
minY = initPos.y()
|
||||
min_size = min(abs(pos.x() - minX), abs(pos.y() - minY))
|
||||
directionX = -1 if pos.x() - minX < 0 else 1
|
||||
directionY = -1 if pos.y() - minY < 0 else 1
|
||||
self.line[1] = QPointF(minX + directionX * min_size, minY + directionY * min_size)
|
||||
self.line.points = [self.current[0], pos]
|
||||
self.line.close()
|
||||
|
||||
elif self.fourpoint:
|
||||
# self.line[self.pointnum] = pos # OLD
|
||||
|
||||
self.line[0] = self.current[-1]
|
||||
self.line[1] = pos
|
||||
|
||||
|
@ -196,12 +183,14 @@ class Canvas(QWidget):
|
|||
|
||||
# Polygon copy moving.
|
||||
if Qt.RightButton & ev.buttons():
|
||||
if self.selectedShapeCopy and self.prevPoint:
|
||||
if self.selectedShapesCopy and self.prevPoint:
|
||||
self.overrideCursor(CURSOR_MOVE)
|
||||
self.boundedMoveShape(self.selectedShapeCopy, pos)
|
||||
self.boundedMoveShape(self.selectedShapesCopy, pos)
|
||||
self.repaint()
|
||||
elif self.selectedShape:
|
||||
self.selectedShapeCopy = self.selectedShape.copy()
|
||||
elif self.selectedShapes:
|
||||
self.selectedShapesCopy = [
|
||||
s.copy() for s in self.selectedShapes
|
||||
]
|
||||
self.repaint()
|
||||
return
|
||||
|
||||
|
@ -211,11 +200,13 @@ class Canvas(QWidget):
|
|||
self.boundedMoveVertex(pos)
|
||||
self.shapeMoved.emit()
|
||||
self.repaint()
|
||||
elif self.selectedShape and self.prevPoint:
|
||||
self.movingShape = True
|
||||
elif self.selectedShapes and self.prevPoint:
|
||||
self.overrideCursor(CURSOR_MOVE)
|
||||
self.boundedMoveShape(self.selectedShape, pos)
|
||||
self.boundedMoveShape(self.selectedShapes, pos)
|
||||
self.shapeMoved.emit()
|
||||
self.repaint()
|
||||
self.movingShape = True
|
||||
else:
|
||||
#pan
|
||||
delta_x = pos.x() - self.pan_initial_pos.x()
|
||||
|
@ -263,65 +254,60 @@ class Canvas(QWidget):
|
|||
|
||||
def mousePressEvent(self, ev):
|
||||
pos = self.transformPos(ev.pos())
|
||||
|
||||
if ev.button() == Qt.LeftButton:
|
||||
if self.drawing():
|
||||
# self.handleDrawing(pos) # OLD
|
||||
|
||||
|
||||
if self.current and self.fourpoint: # ADD IF
|
||||
if self.current:
|
||||
if self.fourpoint: # ADD IF
|
||||
# Add point to existing shape.
|
||||
print('Adding points in mousePressEvent is ', self.line[1])
|
||||
# print('Adding points in mousePressEvent is ', self.line[1])
|
||||
self.current.addPoint(self.line[1])
|
||||
self.line[0] = self.current[-1]
|
||||
if self.current.isClosed():
|
||||
# print('1111')
|
||||
self.finalise()
|
||||
elif self.drawSquare: # 增加
|
||||
assert len(self.current.points) == 1
|
||||
self.current.points = self.line.points
|
||||
self.finalise()
|
||||
elif not self.outOfPixmap(pos):
|
||||
# Create new shape.
|
||||
self.current = Shape()# self.current = Shape(shape_type=self.createMode)
|
||||
self.current = Shape()
|
||||
self.current.addPoint(pos)
|
||||
# if self.createMode == "point":
|
||||
# self.finalise()
|
||||
# else:
|
||||
# if self.createMode == "circle":
|
||||
# self.current.shape_type = "circle"
|
||||
self.line.points = [pos, pos]
|
||||
self.setHiding()
|
||||
self.drawingPolygon.emit(True)
|
||||
self.update()
|
||||
|
||||
|
||||
else:
|
||||
selection = self.selectShapePoint(pos)
|
||||
group_mode = int(ev.modifiers()) == Qt.ControlModifier
|
||||
self.selectShapePoint(pos, multiple_selection_mode=group_mode)
|
||||
self.prevPoint = pos
|
||||
|
||||
if selection is None:
|
||||
#pan
|
||||
QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor))
|
||||
self.pan_initial_pos = pos
|
||||
|
||||
elif ev.button() == Qt.RightButton and self.editing():
|
||||
self.selectShapePoint(pos)
|
||||
group_mode = int(ev.modifiers()) == Qt.ControlModifier
|
||||
self.selectShapePoint(pos, multiple_selection_mode=group_mode)
|
||||
self.prevPoint = pos
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
if ev.button() == Qt.RightButton:
|
||||
menu = self.menus[bool(self.selectedShapeCopy)]
|
||||
menu = self.menus[bool(self.selectedShapesCopy)]
|
||||
self.restoreCursor()
|
||||
if not menu.exec_(self.mapToGlobal(ev.pos()))\
|
||||
and self.selectedShapeCopy:
|
||||
and self.selectedShapesCopy:
|
||||
# Cancel the move by deleting the shadow copy.
|
||||
self.selectedShapeCopy = None
|
||||
# self.selectedShapeCopy = None
|
||||
self.selectedShapesCopy = []
|
||||
self.repaint()
|
||||
elif ev.button() == Qt.LeftButton and self.selectedShape: # OLD
|
||||
|
||||
elif ev.button() == Qt.LeftButton and self.selectedShapes:
|
||||
if self.selectedVertex():
|
||||
self.overrideCursor(CURSOR_POINT)
|
||||
else:
|
||||
self.overrideCursor(CURSOR_GRAB)
|
||||
|
||||
|
||||
elif ev.button() == Qt.LeftButton and not self.fourpoint:
|
||||
pos = self.transformPos(ev.pos())
|
||||
if self.drawing():
|
||||
|
@ -330,24 +316,37 @@ class Canvas(QWidget):
|
|||
#pan
|
||||
QApplication.restoreOverrideCursor() # ?
|
||||
|
||||
if self.movingShape and self.hShape:
|
||||
index = self.shapes.index(self.hShape)
|
||||
if (
|
||||
self.shapesBackups[-1][index].points
|
||||
!= self.shapes[index].points
|
||||
):
|
||||
self.storeShapes()
|
||||
self.shapeMoved.emit() # connect to updateBoxlist in PPOCRLabel.py
|
||||
|
||||
self.movingShape = False
|
||||
|
||||
|
||||
def endMove(self, copy=False):
|
||||
assert self.selectedShape and self.selectedShapeCopy
|
||||
shape = self.selectedShapeCopy
|
||||
#del shape.fill_color
|
||||
#del shape.line_color
|
||||
assert self.selectedShapes and self.selectedShapesCopy
|
||||
assert len(self.selectedShapesCopy) == len(self.selectedShapes)
|
||||
if copy:
|
||||
for i, shape in enumerate(self.selectedShapesCopy):
|
||||
self.shapes.append(shape)
|
||||
self.selectedShape.selected = False
|
||||
self.selectedShape = shape
|
||||
self.repaint()
|
||||
self.selectedShapes[i].selected = False
|
||||
self.selectedShapes[i] = shape
|
||||
else:
|
||||
self.selectedShape.points = [p for p in shape.points]
|
||||
self.selectedShapeCopy = None
|
||||
for i, shape in enumerate(self.selectedShapesCopy):
|
||||
self.selectedShapes[i].points = shape.points
|
||||
self.selectedShapesCopy = []
|
||||
self.repaint()
|
||||
self.storeShapes()
|
||||
return True
|
||||
|
||||
def hideBackroundShapes(self, value):
|
||||
self.hideBackround = value
|
||||
if self.selectedShape:
|
||||
if self.selectedShapes:
|
||||
# Only hide other shapes if there is a current selection.
|
||||
# Otherwise the user will not be able to select a shape.
|
||||
self.setHiding(True)
|
||||
|
@ -363,7 +362,7 @@ class Canvas(QWidget):
|
|||
if self.pointnum == 3:
|
||||
self.finalise()
|
||||
|
||||
else: # 按住送掉后跳到这里
|
||||
else:
|
||||
initPos = self.current[0]
|
||||
print('initPos', self.current[0])
|
||||
minX = initPos.x()
|
||||
|
@ -399,28 +398,33 @@ class Canvas(QWidget):
|
|||
self.current.popPoint()
|
||||
self.finalise()
|
||||
|
||||
def selectShape(self, shape):
|
||||
self.deSelectShape()
|
||||
shape.selected = True
|
||||
self.selectedShape = shape
|
||||
def selectShapes(self, shapes):
|
||||
for s in shapes: s.seleted = True
|
||||
self.setHiding()
|
||||
self.selectionChanged.emit(True)
|
||||
self.selectionChanged.emit(shapes)
|
||||
self.update()
|
||||
|
||||
def selectShapePoint(self, point):
|
||||
|
||||
def selectShapePoint(self, point, multiple_selection_mode):
|
||||
"""Select the first shape created which contains this point."""
|
||||
self.deSelectShape()
|
||||
if self.selectedVertex(): # A vertex is marked for selection.
|
||||
index, shape = self.hVertex, self.hShape
|
||||
shape.highlightVertex(index, shape.MOVE_VERTEX)
|
||||
self.selectShape(shape)
|
||||
return self.hVertex
|
||||
else:
|
||||
for shape in reversed(self.shapes):
|
||||
if self.isVisible(shape) and shape.containsPoint(point):
|
||||
self.selectShape(shape)
|
||||
self.calculateOffsets(shape, point)
|
||||
return self.selectedShape
|
||||
return None
|
||||
self.setHiding()
|
||||
if multiple_selection_mode:
|
||||
if shape not in self.selectedShapes: # list
|
||||
self.selectionChanged.emit(
|
||||
self.selectedShapes + [shape]
|
||||
)
|
||||
else:
|
||||
self.selectionChanged.emit([shape])
|
||||
return
|
||||
self.deSelectShape()
|
||||
|
||||
def calculateOffsets(self, shape, point):
|
||||
rect = shape.boundingRect()
|
||||
|
@ -465,8 +469,9 @@ class Canvas(QWidget):
|
|||
else:
|
||||
shiftPos = pos - point
|
||||
|
||||
if [shape[0].x(), shape[0].y(), shape[2].x(), shape[2].y()] \
|
||||
== [shape[3].x(),shape[1].y(),shape[1].x(),shape[3].y()]:
|
||||
shape.moveVertexBy(index, shiftPos)
|
||||
|
||||
lindex = (index + 1) % 4
|
||||
rindex = (index + 3) % 4
|
||||
lshift = None
|
||||
|
@ -480,7 +485,12 @@ class Canvas(QWidget):
|
|||
shape.moveVertexBy(rindex, rshift)
|
||||
shape.moveVertexBy(lindex, lshift)
|
||||
|
||||
def boundedMoveShape(self, shape, pos):
|
||||
else:
|
||||
shape.moveVertexBy(index, shiftPos)
|
||||
|
||||
|
||||
def boundedMoveShape(self, shapes, pos):
|
||||
if type(shapes).__name__ != 'list': shapes = [shapes]
|
||||
if self.outOfPixmap(pos):
|
||||
return False # No need to move
|
||||
o1 = pos + self.offsets[0]
|
||||
|
@ -497,40 +507,49 @@ class Canvas(QWidget):
|
|||
#self.calculateOffsets(self.selectedShape, pos)
|
||||
dp = pos - self.prevPoint
|
||||
if dp:
|
||||
for shape in shapes:
|
||||
shape.moveBy(dp)
|
||||
self.prevPoint = pos
|
||||
return True
|
||||
return False
|
||||
|
||||
def deSelectShape(self):
|
||||
if self.selectedShape:
|
||||
self.selectedShape.selected = False
|
||||
self.selectedShape = None
|
||||
if self.selectedShapes:
|
||||
for shape in self.selectedShapes: shape.selected=False
|
||||
self.setHiding(False)
|
||||
self.selectionChanged.emit(False)
|
||||
self.selectionChanged.emit([])
|
||||
self.update()
|
||||
|
||||
def deleteSelected(self):
|
||||
if self.selectedShape:
|
||||
shape = self.selectedShape
|
||||
self.shapes.remove(self.selectedShape)
|
||||
self.selectedShape = None
|
||||
deleted_shapes = []
|
||||
if self.selectedShapes:
|
||||
for shape in self.selectedShapes:
|
||||
self.shapes.remove(shape)
|
||||
deleted_shapes.append(shape)
|
||||
self.storeShapes()
|
||||
self.selectedShapes = []
|
||||
self.update()
|
||||
return shape
|
||||
return deleted_shapes
|
||||
|
||||
def storeShapes(self):
|
||||
shapesBackup = []
|
||||
for shape in self.shapes:
|
||||
shapesBackup.append(shape.copy())
|
||||
if len(self.shapesBackups) >= 10:
|
||||
self.shapesBackups = self.shapesBackups[-9:]
|
||||
self.shapesBackups.append(shapesBackup)
|
||||
|
||||
def copySelectedShape(self):
|
||||
if self.selectedShape:
|
||||
shape = self.selectedShape.copy()
|
||||
self.deSelectShape()
|
||||
self.shapes.append(shape)
|
||||
shape.selected = True
|
||||
self.selectedShape = shape
|
||||
self.boundedShiftShape(shape)
|
||||
return shape
|
||||
if self.selectedShapes:
|
||||
self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]
|
||||
self.boundedShiftShapes(self.selectedShapesCopy)
|
||||
self.endMove(copy=True)
|
||||
return self.selectedShapes
|
||||
|
||||
def boundedShiftShape(self, shape):
|
||||
def boundedShiftShapes(self, shapes):
|
||||
# Try to move in one direction, and if it fails in another.
|
||||
# Give up if both fail.
|
||||
for shape in shapes:
|
||||
point = shape[0]
|
||||
offset = QPointF(2.0, 2.0)
|
||||
self.calculateOffsets(shape, point)
|
||||
|
@ -560,8 +579,9 @@ class Canvas(QWidget):
|
|||
if self.current:
|
||||
self.current.paint(p)
|
||||
self.line.paint(p)
|
||||
if self.selectedShapeCopy:
|
||||
self.selectedShapeCopy.paint(p)
|
||||
if self.selectedShapesCopy:
|
||||
for s in self.selectedShapesCopy:
|
||||
s.paint(p)
|
||||
|
||||
# Paint rect
|
||||
if self.current is not None and len(self.line) == 2 and not self.fourpoint:
|
||||
|
@ -739,6 +759,7 @@ class Canvas(QWidget):
|
|||
|
||||
if fill_color:
|
||||
self.shapes[-1].fill_color = fill_color
|
||||
self.storeShapes()
|
||||
|
||||
return self.shapes[-1]
|
||||
|
||||
|
@ -749,6 +770,17 @@ class Canvas(QWidget):
|
|||
self.line.points = [self.current[-1], self.current[0]]
|
||||
self.drawingPolygon.emit(True)
|
||||
|
||||
def undoLastPoint(self):
|
||||
if not self.current or self.current.isClosed():
|
||||
return
|
||||
self.current.popPoint()
|
||||
if len(self.current) > 0:
|
||||
self.line[0] = self.current[-1]
|
||||
else:
|
||||
self.current = None
|
||||
self.drawingPolygon.emit(False)
|
||||
self.repaint()
|
||||
|
||||
def resetAllLines(self):
|
||||
assert self.shapes
|
||||
self.current = self.shapes.pop()
|
||||
|
@ -762,11 +794,18 @@ class Canvas(QWidget):
|
|||
def loadPixmap(self, pixmap):
|
||||
self.pixmap = pixmap
|
||||
self.shapes = []
|
||||
self.repaint() # 这函数在哪
|
||||
self.repaint()
|
||||
|
||||
def loadShapes(self, shapes):
|
||||
def loadShapes(self, shapes, replace=True):
|
||||
if replace:
|
||||
self.shapes = list(shapes)
|
||||
else:
|
||||
self.shapes.extend(shapes)
|
||||
self.current = None
|
||||
self.hShape = None
|
||||
self.hVertex = None
|
||||
# self.hEdge = None
|
||||
self.storeShapes()
|
||||
self.repaint()
|
||||
|
||||
def setShapeVisible(self, shape, value):
|
||||
|
@ -793,6 +832,24 @@ class Canvas(QWidget):
|
|||
self.restoreCursor()
|
||||
self.pixmap = None
|
||||
self.update()
|
||||
self.shapesBackups = []
|
||||
|
||||
def setDrawingShapeToSquare(self, status):
|
||||
self.drawSquare = status
|
||||
|
||||
def restoreShape(self):
|
||||
if not self.isShapeRestorable:
|
||||
return
|
||||
self.shapesBackups.pop() # latest
|
||||
shapesBackup = self.shapesBackups.pop()
|
||||
self.shapes = shapesBackup
|
||||
self.selectedShapes = []
|
||||
for shape in self.shapes:
|
||||
shape.selected = False
|
||||
self.repaint()
|
||||
|
||||
@property
|
||||
def isShapeRestorable(self):
|
||||
if len(self.shapesBackups) < 2:
|
||||
return False
|
||||
return True
|
File diff suppressed because it is too large
Load Diff
|
@ -82,7 +82,7 @@ class Shape(object):
|
|||
return False
|
||||
|
||||
def addPoint(self, point):
|
||||
if not self.reachMaxPoints():
|
||||
if not self.reachMaxPoints(): # 4个点时发出close信号
|
||||
self.points.append(point)
|
||||
|
||||
def popPoint(self):
|
||||
|
|
|
@ -97,3 +97,6 @@ showBox=显示所有标注
|
|||
saveLabel=保存标记结果
|
||||
singleRe=重识别此区块
|
||||
labelDialogOption=弹出标记输入框
|
||||
undo=撤销
|
||||
undoLastPoint=撤销上个点
|
||||
autoSaveMode=自动保存标记结果
|
|
@ -97,3 +97,6 @@ showBox=Show All Box
|
|||
saveLabel=Save Label
|
||||
singleRe=Re-recognition RectBox
|
||||
labelDialogOption=Pop-up Label Input Dialog
|
||||
undo=Undo
|
||||
undoLastPoint=Undo Last Point
|
||||
autoSaveMode=Auto Save Label Mode
|
BIN
doc/joinus.PNG
BIN
doc/joinus.PNG
Binary file not shown.
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 109 KiB |
Loading…
Reference in New Issue