未验证 提交 e4ea2211 编写于 作者: E Evezerest 提交者: GitHub

Optimize the labeling experience (#1709)

* Update readme

* Optimize the labeling experience
上级 e1b93296
...@@ -64,6 +64,7 @@ from libs.colorDialog import ColorDialog ...@@ -64,6 +64,7 @@ from libs.colorDialog import ColorDialog
from libs.toolBar import ToolBar from libs.toolBar import ToolBar
from libs.ustr import ustr from libs.ustr import ustr
from libs.hashableQListWidgetItem import HashableQListWidgetItem from libs.hashableQListWidgetItem import HashableQListWidgetItem
from libs.editinlist import EditInList
__appname__ = 'PPOCRLabel' __appname__ = 'PPOCRLabel'
...@@ -201,12 +202,12 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -201,12 +202,12 @@ class MainWindow(QMainWindow, WindowMixin):
################## label list #################### ################## label list ####################
# Create and add a widget for showing current label items # Create and add a widget for showing current label items
self.labelList = QListWidget() self.labelList = EditInList()
labelListContainer = QWidget() labelListContainer = QWidget()
labelListContainer.setLayout(listLayout) labelListContainer.setLayout(listLayout)
self.labelList.itemActivated.connect(self.labelSelectionChanged) self.labelList.itemActivated.connect(self.labelSelectionChanged)
self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged) self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)
self.labelList.itemDoubleClicked.connect(self.editLabel) self.labelList.clicked.connect(self.labelList.item_clicked)
# Connect to itemChanged to detect checkbox changes. # Connect to itemChanged to detect checkbox changes.
self.labelList.itemChanged.connect(self.labelItemChanged) self.labelList.itemChanged.connect(self.labelItemChanged)
self.labelListDock = QDockWidget(getStr('recognitionResult'),self) self.labelListDock = QDockWidget(getStr('recognitionResult'),self)
...@@ -316,7 +317,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -316,7 +317,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.scrollArea = scroll self.scrollArea = scroll
self.canvas.scrollRequest.connect(self.scrollRequest) self.canvas.scrollRequest.connect(self.scrollRequest)
self.canvas.newShape.connect(self.newShape) self.canvas.newShape.connect(partial(self.newShape, False))
self.canvas.shapeMoved.connect(self.updateBoxlist) # self.setDirty self.canvas.shapeMoved.connect(self.updateBoxlist) # self.setDirty
self.canvas.selectionChanged.connect(self.shapeSelectionChanged) self.canvas.selectionChanged.connect(self.shapeSelectionChanged)
self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive) self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive)
...@@ -354,13 +355,9 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -354,13 +355,9 @@ class MainWindow(QMainWindow, WindowMixin):
quit = action(getStr('quit'), self.close, quit = action(getStr('quit'), self.close,
'Ctrl+Q', 'quit', getStr('quitApp')) 'Ctrl+Q', 'quit', getStr('quitApp'))
open = action(getStr('openFile'), self.openFile,
'Ctrl+O', 'open', getStr('openFileDetail'))
opendir = action(getStr('openDir'), self.openDirDialog, opendir = action(getStr('openDir'), self.openDirDialog,
'Ctrl+u', 'open', getStr('openDir')) 'Ctrl+u', 'open', getStr('openDir'))
save = action(getStr('save'), self.saveFile, save = action(getStr('save'), self.saveFile,
'Ctrl+V', 'verify', getStr('saveDetail'), enabled=False) 'Ctrl+V', 'verify', getStr('saveDetail'), enabled=False)
...@@ -506,7 +503,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -506,7 +503,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.drawSquaresOption.triggered.connect(self.toogleDrawSquare) self.drawSquaresOption.triggered.connect(self.toogleDrawSquare)
# Store actions for further handling. # Store actions for further handling.
self.actions = struct(save=save, open=open, resetAll=resetAll, deleteImg=deleteImg, self.actions = struct(save=save, resetAll=resetAll, deleteImg=deleteImg,
lineColor=color1, create=create, delete=delete, edit=edit, copy=copy, lineColor=color1, create=create, delete=delete, edit=edit, copy=copy,
saveRec=saveRec, singleRere=singleRere,AutoRec=AutoRec,reRec=reRec, saveRec=saveRec, singleRere=singleRere,AutoRec=AutoRec,reRec=reRec,
createMode=createMode, editMode=editMode, createMode=createMode, editMode=editMode,
...@@ -515,7 +512,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -515,7 +512,7 @@ class MainWindow(QMainWindow, WindowMixin):
fitWindow=fitWindow, fitWidth=fitWidth, fitWindow=fitWindow, fitWidth=fitWidth,
zoomActions=zoomActions, saveLabel=saveLabel, zoomActions=zoomActions, saveLabel=saveLabel,
fileMenuActions=( fileMenuActions=(
open, opendir, saveLabel, resetAll, quit), opendir, saveLabel, resetAll, quit),
beginner=(), advanced=(), beginner=(), advanced=(),
editMenu=(createpoly, edit, copy, delete,singleRere, editMenu=(createpoly, edit, copy, delete,singleRere,
None, color1, self.drawSquaresOption), None, color1, self.drawSquaresOption),
...@@ -537,11 +534,6 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -537,11 +534,6 @@ class MainWindow(QMainWindow, WindowMixin):
labelList=labelMenu) labelList=labelMenu)
# Sync single class mode from PR#106
self.singleClassMode = QAction(getStr('singleClsMode'), self)
self.singleClassMode.setShortcut("Ctrl+Shift+S")
self.singleClassMode.setCheckable(True)
self.singleClassMode.setChecked(settings.get(SETTING_SINGLE_CLASS, False))
self.lastLabel = None self.lastLabel = None
# Add option to enable/disable labels being displayed at the top of bounding boxes # Add option to enable/disable labels being displayed at the top of bounding boxes
self.displayLabelOption = QAction(getStr('displayLabel'), self) self.displayLabelOption = QAction(getStr('displayLabel'), self)
...@@ -550,12 +542,18 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -550,12 +542,18 @@ class MainWindow(QMainWindow, WindowMixin):
self.displayLabelOption.setChecked(settings.get(SETTING_PAINT_LABEL, False)) self.displayLabelOption.setChecked(settings.get(SETTING_PAINT_LABEL, False))
self.displayLabelOption.triggered.connect(self.togglePaintLabelsOption) self.displayLabelOption.triggered.connect(self.togglePaintLabelsOption)
self.labelDialogOption = QAction(getStr('labelDialogOption'), self)
self.labelDialogOption.setShortcut("Ctrl+Shift+L")
self.labelDialogOption.setCheckable(True)
self.labelDialogOption.setChecked(settings.get(SETTING_PAINT_LABEL, False))
self.labelDialogOption.triggered.connect(self.speedChoose)
addActions(self.menus.file, addActions(self.menus.file,
(opendir, None, saveLabel, saveRec, None, resetAll, deleteImg, quit)) (opendir, None, saveLabel, saveRec, None, resetAll, deleteImg, quit))
addActions(self.menus.help, (showSteps, showInfo)) addActions(self.menus.help, (showSteps, showInfo))
addActions(self.menus.view, ( addActions(self.menus.view, (
self.displayLabelOption, # labels, self.displayLabelOption, self.labelDialogOption,
None, None,
hideAll, showAll, None, hideAll, showAll, None,
zoomIn, zoomOut, zoomOrg, None, zoomIn, zoomOut, zoomOrg, None,
...@@ -1062,6 +1060,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1062,6 +1060,7 @@ class MainWindow(QMainWindow, WindowMixin):
def labelSelectionChanged(self): def labelSelectionChanged(self):
item = self.currentItem() item = self.currentItem()
self.labelList.scrollToItem(item, QAbstractItemView.EnsureVisible)
if item and self.canvas.editing(): if item and self.canvas.editing():
self._noSelectionSlot = True self._noSelectionSlot = True
self.canvas.selectShape(self.itemsToShapes[item]) self.canvas.selectShape(self.itemsToShapes[item])
...@@ -1069,6 +1068,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1069,6 +1068,7 @@ class MainWindow(QMainWindow, WindowMixin):
def boxSelectionChanged(self): def boxSelectionChanged(self):
item = self.currentBox() item = self.currentBox()
self.BoxList.scrollToItem(item, QAbstractItemView.EnsureVisible)
if item and self.canvas.editing(): if item and self.canvas.editing():
self._noSelectionSlot = True self._noSelectionSlot = True
self.canvas.selectShape(self.itemsToShapesbox[item]) self.canvas.selectShape(self.itemsToShapesbox[item])
...@@ -1089,7 +1089,7 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1089,7 +1089,7 @@ class MainWindow(QMainWindow, WindowMixin):
# self.actions.save.setEnabled(True) # self.actions.save.setEnabled(True)
# Callback functions: # Callback functions:
def newShape(self): def newShape(self, value=True):
"""Pop-up and give focus to the label editor. """Pop-up and give focus to the label editor.
position MUST be in global coordinates. position MUST be in global coordinates.
...@@ -1098,12 +1098,11 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1098,12 +1098,11 @@ class MainWindow(QMainWindow, WindowMixin):
self.labelDialog = LabelDialog( self.labelDialog = LabelDialog(
parent=self, listItem=self.labelHist) parent=self, listItem=self.labelHist)
# Sync single class mode from PR#106 if value:
if self.singleClassMode.isChecked() and self.lastLabel:
text = self.lastLabel
else:
text = self.labelDialog.popUp(text=self.prevLabelText) text = self.labelDialog.popUp(text=self.prevLabelText)
self.lastLabel = text self.lastLabel = text
else:
text = self.prevLabelText
if text is not None: if text is not None:
self.prevLabelText = self.stringBundle.getString('tempLabel') self.prevLabelText = self.stringBundle.getString('tempLabel')
...@@ -1364,7 +1363,6 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1364,7 +1363,6 @@ class MainWindow(QMainWindow, WindowMixin):
else: else:
settings[SETTING_LAST_OPEN_DIR] = '' settings[SETTING_LAST_OPEN_DIR] = ''
settings[SETTING_SINGLE_CLASS] = self.singleClassMode.isChecked()
settings[SETTING_PAINT_LABEL] = self.displayLabelOption.isChecked() settings[SETTING_PAINT_LABEL] = self.displayLabelOption.isChecked()
settings[SETTING_DRAW_SQUARE] = self.drawSquaresOption.isChecked() settings[SETTING_DRAW_SQUARE] = self.drawSquaresOption.isChecked()
settings.save() settings.save()
...@@ -1497,35 +1495,6 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1497,35 +1495,6 @@ class MainWindow(QMainWindow, WindowMixin):
print('file name in openNext is ',filename) print('file name in openNext is ',filename)
self.loadFile(filename) self.loadFile(filename)
def openFile(self, _value=False):
if not self.mayContinue():
return
path = os.path.dirname(ustr(self.filePath)) if self.filePath else '.'
formats = ['*.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()]
filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.suffix])
filename = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters)
if filename:
if isinstance(filename, (tuple, list)):
filename = filename[0]
self.loadFile(filename)
# print('filename in openfile is ', self.filePath)
self.filePath = None
self.fileListWidget.clear()
self.iconlist.clear()
self.mImgList = [filename]
self.openNextImg()
if self.validFilestate(filename) is True:
item = QListWidgetItem(newIcon('done'), filename)
self.setClean()
elif self.validFilestate(filename) is None:
item = QListWidgetItem(newIcon('close'), filename)
else:
item = QListWidgetItem(newIcon('close'), filename)
self.setDirty()
self.fileListWidget.addItem(filename)
self.additems5(None)
print('opened image is', filename)
def updateFileListIcon(self, filename): def updateFileListIcon(self, filename):
pass pass
...@@ -1963,6 +1932,16 @@ class MainWindow(QMainWindow, WindowMixin): ...@@ -1963,6 +1932,16 @@ class MainWindow(QMainWindow, WindowMixin):
QMessageBox.information(self, "Information", "Cropped images has been saved in "+str(crop_img_dir)) QMessageBox.information(self, "Information", "Cropped images has been saved in "+str(crop_img_dir))
def speedChoose(self):
if self.labelDialogOption.isChecked():
self.canvas.newShape.disconnect()
self.canvas.newShape.connect(partial(self.newShape, True))
else:
self.canvas.newShape.disconnect()
self.canvas.newShape.connect(partial(self.newShape, False))
def inverted(color): def inverted(color):
return QColor(*[255 - v for v in color.getRgb()]) 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 ...@@ -8,6 +8,10 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w
### Recent Update ### Recent Update
- 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.
- Click to modify the recognition result.(If you can't change the result, please switch to the system default input method, or switch back to the original input method again)
- 2020.12.18: Support re-recognition of a single label box (by [ninetailskim](https://github.com/ninetailskim) ), perfect shortcut keys. - 2020.12.18: Support re-recognition of a single label box (by [ninetailskim](https://github.com/ninetailskim) ), perfect shortcut keys.
### TODO: ### TODO:
......
...@@ -8,7 +8,11 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P ...@@ -8,7 +8,11 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
#### 近期更新 #### 近期更新
- 2020.12.18: 支持对单个标记框进行重新识别(by [ninetailskim](https://github.com/ninetailskim)),完善快捷键。 - 2021.1.11:优化标注体验(by [edencfc](https://github.com/edencfc)):
- 用户可在“视图 - 弹出标记输入框”选择在画完检测框后标记输入框是否弹出。
- 识别结果与检测框同步滚动。
- 识别结果更改为单击修改。(如果无法修改,请切换为系统自带输入法,或再次切回原输入法)
- 2020.12.18: 支持对单个标记框进行重新识别(by [ninetailskim](https://github.com/ninetailskim)),完善快捷键。
#### 尽请期待 #### 尽请期待
......
import sys, time
from PyQt5 import QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class EditInList(QListWidget):
def __init__(self):
super(EditInList,self).__init__()
# click to edit
self.clicked.connect(self.item_clicked)
def item_clicked(self, modelindex: QModelIndex) -> None:
self.edited_item = self.currentItem()
self.closePersistentEditor(self.edited_item)
item = self.item(modelindex.row())
# time.sleep(0.2)
self.edited_item = item
self.openPersistentEditor(item)
# time.sleep(0.2)
self.editItem(item)
def mouseDoubleClickEvent(self, event):
# close edit
for i in range(self.count()):
self.closePersistentEditor(self.item(i))
def leaveEvent(self, event):
# close edit
for i in range(self.count()):
self.closePersistentEditor(self.item(i))
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
...@@ -42,7 +42,7 @@ zoomin=放大画面 ...@@ -42,7 +42,7 @@ zoomin=放大画面
info=信息 info=信息
openAnnotation=开启标签 openAnnotation=开启标签
prevImgDetail=上一个图像 prevImgDetail=上一个图像
fitWidth=缩放到跟当前画面一样宽 fitWidth=缩放到当前画面宽度
zoomout=缩小画面 zoomout=缩小画面
changeSavedAnnotationDir=更改保存标签文件的预设目录 changeSavedAnnotationDir=更改保存标签文件的预设目录
nextImgDetail=下一个图像 nextImgDetail=下一个图像
...@@ -96,3 +96,4 @@ hideBox=隐藏所有标注 ...@@ -96,3 +96,4 @@ hideBox=隐藏所有标注
showBox=显示所有标注 showBox=显示所有标注
saveLabel=保存标记结果 saveLabel=保存标记结果
singleRe=重识别此区块 singleRe=重识别此区块
labelDialogOption=弹出标记输入框
\ No newline at end of file
...@@ -96,3 +96,4 @@ hideBox=Hide All Box ...@@ -96,3 +96,4 @@ hideBox=Hide All Box
showBox=Show All Box showBox=Show All Box
saveLabel=Save Label saveLabel=Save Label
singleRe=Re-recognition RectBox singleRe=Re-recognition RectBox
labelDialogOption=Pop-up Label Input Dialog
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册