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

Merge pull request #5323 from PeterH0323/dygraph

【PPOCRLabel】4 Bugs Fixed and 2 New Feature Added 
此差异已折叠。
...@@ -8,6 +8,8 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w ...@@ -8,6 +8,8 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w
### Recent Update ### Recent Update
- 2022.01:(by [PeterH0323](https://github.com/peterh0323)
- Improve user experience: prompt for the number of files and labels, optimize interaction, and fix bugs such as only use CPU when inference
- 2021.11.17: - 2021.11.17:
- Support install and start PPOCRLabel through the whl package (by [d2623587501](https://github.com/d2623587501)) - Support install and start PPOCRLabel through the whl package (by [d2623587501](https://github.com/d2623587501))
- Dataset segmentation: Divide the annotation file into training, verification and testing parts (refer to section 3.5 below, by [MrCuiHao](https://github.com/MrCuiHao)) - Dataset segmentation: Divide the annotation file into training, verification and testing parts (refer to section 3.5 below, by [MrCuiHao](https://github.com/MrCuiHao))
...@@ -110,7 +112,7 @@ python PPOCRLabel.py ...@@ -110,7 +112,7 @@ python PPOCRLabel.py
6. Click 're-Recognition', model will rewrite ALL recognition results in ALL detection box<sup>[3]</sup>. 6. Click 're-Recognition', model will rewrite ALL recognition results in ALL detection box<sup>[3]</sup>.
7. Double click the result in 'recognition result' list to manually change inaccurate recognition results. 7. Single 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.** 8. **Click "Check", the image status will switch to "√",then the program automatically jump to the next.**
...@@ -143,15 +145,17 @@ python PPOCRLabel.py ...@@ -143,15 +145,17 @@ python PPOCRLabel.py
### 3.1 Shortcut keys ### 3.1 Shortcut keys
| Shortcut keys | Description | | Shortcut keys | Description |
|--------------------------| ------------------------------------------------ | |--------------------------|--------------------------------------------------|
| 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 | | W | Create a rect box |
| Q | Create a four-points box | | Q | Create a four-points box |
| X | Rotate the box anti-clockwise |
| C | Rotate the box clockwise |
| Ctrl + E | Edit label of the selected box | | Ctrl + E | Edit label of the selected box |
| Ctrl + R | Re-recognize the selected box | | Ctrl + R | Re-recognize the selected box |
| Ctrl + C | Copy and paste the selected box | | Ctrl + C | Copy and paste the selected box |
| Ctrl + Left Mouse Button | Multi select the label box | | Ctrl + Left Mouse Button | Multi select the label box |
| Ctrl + X | Delete the selected box | | Alt + X | Delete the selected box |
| Ctrl + V | Check image | | Ctrl + V | Check image |
| Ctrl + Shift + d | Delete image | | Ctrl + Shift + d | Delete image |
| D | Next image | | D | Next image |
......
...@@ -8,6 +8,8 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P ...@@ -8,6 +8,8 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
#### 近期更新 #### 近期更新
- 2022.01:(by [PeterH0323](https://github.com/peterh0323)
- 提升用户体验:新增文件与标记数目提示、优化交互、修复gpu使用等问题
- 2021.11.17: - 2021.11.17:
- 新增支持通过whl包安装和启动PPOCRLabel(by [d2623587501](https://github.com/d2623587501) - 新增支持通过whl包安装和启动PPOCRLabel(by [d2623587501](https://github.com/d2623587501)
- 标注数据集切分:对标注数据进行训练、验证与测试集划分(参考下方3.5节,by [MrCuiHao](https://github.com/MrCuiHao) - 标注数据集切分:对标注数据进行训练、验证与测试集划分(参考下方3.5节,by [MrCuiHao](https://github.com/MrCuiHao)
...@@ -102,7 +104,7 @@ python PPOCRLabel.py --lang ch ...@@ -102,7 +104,7 @@ python PPOCRLabel.py --lang ch
4. 手动标注:点击 “矩形标注”(推荐直接在英文模式下点击键盘中的 “W”),用户可对当前图片中模型未检出的部分进行手动绘制标记框。点击键盘Q,则使用四点标注模式(或点击“编辑” - “四点标注”),用户依次点击4个点后,双击左键表示标注完成。 4. 手动标注:点击 “矩形标注”(推荐直接在英文模式下点击键盘中的 “W”),用户可对当前图片中模型未检出的部分进行手动绘制标记框。点击键盘Q,则使用四点标注模式(或点击“编辑” - “四点标注”),用户依次点击4个点后,双击左键表示标注完成。
5. 标记框绘制完成后,用户点击 “确认”,检测框会先被预分配一个 “待识别” 标签。 5. 标记框绘制完成后,用户点击 “确认”,检测框会先被预分配一个 “待识别” 标签。
6. 重新识别:将图片中的所有检测画绘制/调整完成后,点击 “重新识别”,PPOCR模型会对当前图片中的**所有检测框**重新识别<sup>[3]</sup> 6. 重新识别:将图片中的所有检测画绘制/调整完成后,点击 “重新识别”,PPOCR模型会对当前图片中的**所有检测框**重新识别<sup>[3]</sup>
7. 内容更改:击识别结果,对不准确的识别结果进行手动更改。 7. 内容更改:击识别结果,对不准确的识别结果进行手动更改。
8. **确认标记:点击 “确认”,图片状态切换为 “√”,跳转至下一张。** 8. **确认标记:点击 “确认”,图片状态切换为 “√”,跳转至下一张。**
9. 删除:点击 “删除图像”,图片将会被删除至回收站。 9. 删除:点击 “删除图像”,图片将会被删除至回收站。
10. 导出结果:用户可以通过菜单中“文件-导出标记结果”手动导出,同时也可以点击“文件 - 自动导出标记结果”开启自动导出。手动确认过的标记将会被存放在所打开图片文件夹下的*Label.txt*中。在菜单栏点击 “文件” - "导出识别结果"后,会将此类图片的识别训练数据保存在*crop_img*文件夹下,识别标签保存在*rec_gt.txt*<sup>[4]</sup> 10. 导出结果:用户可以通过菜单中“文件-导出标记结果”手动导出,同时也可以点击“文件 - 自动导出标记结果”开启自动导出。手动确认过的标记将会被存放在所打开图片文件夹下的*Label.txt*中。在菜单栏点击 “文件” - "导出识别结果"后,会将此类图片的识别训练数据保存在*crop_img*文件夹下,识别标签保存在*rec_gt.txt*<sup>[4]</sup>
...@@ -131,23 +133,25 @@ python PPOCRLabel.py --lang ch ...@@ -131,23 +133,25 @@ python PPOCRLabel.py --lang ch
### 3.1 快捷键 ### 3.1 快捷键
| 快捷键 | 说明 | | 快捷键 | 说明 |
|------------------| ---------------------------- | |------------------|----------------|
| Ctrl + shift + R | 对当前图片的所有标记重新识别 | | Ctrl + shift + R | 对当前图片的所有标记重新识别 |
| W | 新建矩形框 | | W | 新建矩形框 |
| Q | 新建四点框 | | Q | 新建四点框 |
| Ctrl + E | 编辑所选框标签 | | X | 框逆时针旋转 |
| Ctrl + R | 重新识别所选标记 | | C | 框顺时针旋转 |
| Ctrl + E | 编辑所选框标签 |
| Ctrl + R | 重新识别所选标记 |
| Ctrl + C | 复制并粘贴选中的标记框 | | Ctrl + C | 复制并粘贴选中的标记框 |
| Ctrl + 鼠标左键 | 多选标记框 | | Ctrl + 鼠标左键 | 多选标记框 |
| Ctrl + X | 删除所选框 | | Alt + X | 删除所选框 |
| Ctrl + V | 确认本张图片标记 | | Ctrl + V | 确认本张图片标记 |
| Ctrl + Shift + d | 删除本张图片 | | Ctrl + Shift + d | 删除本张图片 |
| D | 下一张图片 | | D | 下一张图片 |
| A | 上一张图片 | | A | 上一张图片 |
| Ctrl++ | 缩小 | | Ctrl++ | 缩小 |
| Ctrl-- | 放大 | | Ctrl-- | 放大 |
| ↑→↓← | 移动标记框 | | ↑→↓← | 移动标记框 |
### 3.2 内置模型 ### 3.2 内置模型
......
# Copyright (c) <2015-Present> Tzutalin
# Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba,
# William T. Freeman. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
# associated documentation files (the "Software"), to deal in the Software without restriction, including without
# limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of
# the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import sys
try:
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox
except ImportError:
# needed for py3+qt4
# Ref:
# http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
# http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
if sys.version_info.major >= 3:
import sip
sip.setapi('QVariant', 2)
from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox
class ComboBox(QWidget):
def __init__(self, parent=None, items=[]):
super(ComboBox, self).__init__(parent)
layout = QHBoxLayout()
self.cb = QComboBox()
self.items = items
self.cb.addItems(self.items)
self.cb.currentIndexChanged.connect(parent.comboSelectionChanged)
layout.addWidget(self.cb)
self.setLayout(layout)
def update_items(self, items):
self.items = items
self.cb.clear()
self.cb.addItems(self.items)
...@@ -11,19 +11,13 @@ ...@@ -11,19 +11,13 @@
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
try: import copy
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
#from PyQt4.QtOpenGL import *
from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QPoint
from PyQt5.QtGui import QPainter, QBrush, QColor, QPixmap
from PyQt5.QtWidgets import QWidget, QMenu, QApplication
from libs.shape import Shape from libs.shape import Shape
from libs.utils import distance from libs.utils import distance
import copy
CURSOR_DEFAULT = Qt.ArrowCursor CURSOR_DEFAULT = Qt.ArrowCursor
CURSOR_POINT = Qt.PointingHandCursor CURSOR_POINT = Qt.PointingHandCursor
...@@ -31,8 +25,6 @@ CURSOR_DRAW = Qt.CrossCursor ...@@ -31,8 +25,6 @@ CURSOR_DRAW = Qt.CrossCursor
CURSOR_MOVE = Qt.ClosedHandCursor CURSOR_MOVE = Qt.ClosedHandCursor
CURSOR_GRAB = Qt.OpenHandCursor CURSOR_GRAB = Qt.OpenHandCursor
# class Canvas(QGLWidget):
class Canvas(QWidget): class Canvas(QWidget):
zoomRequest = pyqtSignal(int) zoomRequest = pyqtSignal(int)
...@@ -129,7 +121,6 @@ class Canvas(QWidget): ...@@ -129,7 +121,6 @@ class Canvas(QWidget):
def selectedVertex(self): def selectedVertex(self):
return self.hVertex is not None return self.hVertex is not None
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
"""Update line with last point and current coordinates.""" """Update line with last point and current coordinates."""
pos = self.transformPos(ev.pos()) pos = self.transformPos(ev.pos())
...@@ -333,7 +324,6 @@ class Canvas(QWidget): ...@@ -333,7 +324,6 @@ class Canvas(QWidget):
self.movingShape = False self.movingShape = False
def endMove(self, copy=False): def endMove(self, copy=False):
assert self.selectedShapes and self.selectedShapesCopy assert self.selectedShapes and self.selectedShapesCopy
assert len(self.selectedShapesCopy) == len(self.selectedShapes) assert len(self.selectedShapesCopy) == len(self.selectedShapes)
...@@ -410,7 +400,6 @@ class Canvas(QWidget): ...@@ -410,7 +400,6 @@ class Canvas(QWidget):
self.selectionChanged.emit(shapes) self.selectionChanged.emit(shapes)
self.update() self.update()
def selectShapePoint(self, point, multiple_selection_mode): def selectShapePoint(self, point, multiple_selection_mode):
"""Select the first shape created which contains this point.""" """Select the first shape created which contains this point."""
if self.selectedVertex(): # A vertex is marked for selection. if self.selectedVertex(): # A vertex is marked for selection.
...@@ -494,7 +483,6 @@ class Canvas(QWidget): ...@@ -494,7 +483,6 @@ class Canvas(QWidget):
else: else:
shape.moveVertexBy(index, shiftPos) shape.moveVertexBy(index, shiftPos)
def boundedMoveShape(self, shapes, pos): def boundedMoveShape(self, shapes, pos):
if type(shapes).__name__ != 'list': shapes = [shapes] if type(shapes).__name__ != 'list': shapes = [shapes]
if self.outOfPixmap(pos): if self.outOfPixmap(pos):
...@@ -515,6 +503,7 @@ class Canvas(QWidget): ...@@ -515,6 +503,7 @@ class Canvas(QWidget):
if dp: if dp:
for shape in shapes: for shape in shapes:
shape.moveBy(dp) shape.moveBy(dp)
shape.close()
self.prevPoint = pos self.prevPoint = pos
return True return True
return False return False
...@@ -728,6 +717,31 @@ class Canvas(QWidget): ...@@ -728,6 +717,31 @@ class Canvas(QWidget):
self.moveOnePixel('Up') self.moveOnePixel('Up')
elif key == Qt.Key_Down and self.selectedShapes: elif key == Qt.Key_Down and self.selectedShapes:
self.moveOnePixel('Down') self.moveOnePixel('Down')
elif key == Qt.Key_X and self.selectedShapes:
for i in range(len(self.selectedShapes)):
self.selectedShape = self.selectedShapes[i]
if self.rotateOutOfBound(0.01):
continue
self.selectedShape.rotate(0.01)
self.shapeMoved.emit()
self.update()
elif key == Qt.Key_C and self.selectedShapes:
for i in range(len(self.selectedShapes)):
self.selectedShape = self.selectedShapes[i]
if self.rotateOutOfBound(-0.01):
continue
self.selectedShape.rotate(-0.01)
self.shapeMoved.emit()
self.update()
def rotateOutOfBound(self, angle):
for shape in range(len(self.selectedShapes)):
self.selectedShape = self.selectedShapes[shape]
for i, p in enumerate(self.selectedShape.points):
if self.outOfPixmap(self.selectedShape.rotatePoint(p, angle)):
return True
return False
def moveOnePixel(self, direction): def moveOnePixel(self, direction):
# print(self.selectedShape.points) # print(self.selectedShape.points)
......
import sys, time # !/usr/bin/env python
from PyQt5 import QtWidgets # -*- coding: utf-8 -*-
from PyQt5.QtGui import * from PyQt5.QtCore import QModelIndex
from PyQt5.QtCore import * from PyQt5.QtWidgets import QListWidget
from PyQt5.QtWidgets import *
class EditInList(QListWidget): class EditInList(QListWidget):
def __init__(self): def __init__(self):
super(EditInList,self).__init__() super(EditInList, self).__init__()
# click to edit self.edited_item = None
self.clicked.connect(self.item_clicked)
def item_clicked(self, modelindex: QModelIndex):
try:
if self.edited_item is not None:
self.closePersistentEditor(self.edited_item)
except:
self.edited_item = self.currentItem()
def item_clicked(self, modelindex: QModelIndex) -> None: self.edited_item = self.item(modelindex.row())
self.edited_item = self.currentItem() self.openPersistentEditor(self.edited_item)
self.closePersistentEditor(self.edited_item) self.editItem(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): def mouseDoubleClickEvent(self, event):
# close edit pass
for i in range(self.count()):
self.closePersistentEditor(self.item(i))
def leaveEvent(self, event): def leaveEvent(self, event):
# close edit # close edit
for i in range(self.count()): for i in range(self.count()):
self.closePersistentEditor(self.item(i)) self.closePersistentEditor(self.item(i))
\ No newline at end of file
...@@ -10,19 +10,14 @@ ...@@ -10,19 +10,14 @@
# SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
#!/usr/bin/python # !/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import math
import sys
from PyQt5.QtCore import QPointF
try: from PyQt5.QtGui import QColor, QPen, QPainterPath, QFont
from PyQt5.QtGui import *
from PyQt5.QtCore import *
except ImportError:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from libs.utils import distance from libs.utils import distance
import sys
DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128) DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128) DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
...@@ -59,6 +54,8 @@ class Shape(object): ...@@ -59,6 +54,8 @@ class Shape(object):
self.difficult = difficult self.difficult = difficult
self.paintLabel = paintLabel self.paintLabel = paintLabel
self.locked = False self.locked = False
self.direction = 0
self.center = None
self._highlightIndex = None self._highlightIndex = None
self._highlightMode = self.NEAR_VERTEX self._highlightMode = self.NEAR_VERTEX
self._highlightSettings = { self._highlightSettings = {
...@@ -74,7 +71,24 @@ class Shape(object): ...@@ -74,7 +71,24 @@ class Shape(object):
# is used for drawing the pending line a different color. # is used for drawing the pending line a different color.
self.line_color = line_color self.line_color = line_color
def rotate(self, theta):
for i, p in enumerate(self.points):
self.points[i] = self.rotatePoint(p, theta)
self.direction -= theta
self.direction = self.direction % (2 * math.pi)
def rotatePoint(self, p, theta):
order = p - self.center
cosTheta = math.cos(theta)
sinTheta = math.sin(theta)
pResx = cosTheta * order.x() + sinTheta * order.y()
pResy = - sinTheta * order.x() + cosTheta * order.y()
pRes = QPointF(self.center.x() + pResx, self.center.y() + pResy)
return pRes
def close(self): def close(self):
self.center = QPointF((self.points[0].x() + self.points[2].x()) / 2,
(self.points[0].y() + self.points[2].y()) / 2)
self._closed = True self._closed = True
def reachMaxPoints(self): def reachMaxPoints(self):
...@@ -83,7 +97,9 @@ class Shape(object): ...@@ -83,7 +97,9 @@ class Shape(object):
return False return False
def addPoint(self, point): def addPoint(self, point):
if not self.reachMaxPoints(): # 4个点时发出close信号 if self.reachMaxPoints():
self.close()
else:
self.points.append(point) self.points.append(point)
def popPoint(self): def popPoint(self):
...@@ -112,7 +128,7 @@ class Shape(object): ...@@ -112,7 +128,7 @@ class Shape(object):
# Uncommenting the following line will draw 2 paths # Uncommenting the following line will draw 2 paths
# for the 1st vertex, and make it non-filled, which # for the 1st vertex, and make it non-filled, which
# may be desirable. # may be desirable.
#self.drawVertex(vrtx_path, 0) # self.drawVertex(vrtx_path, 0)
for i, p in enumerate(self.points): for i, p in enumerate(self.points):
line_path.lineTo(p) line_path.lineTo(p)
...@@ -136,9 +152,9 @@ class Shape(object): ...@@ -136,9 +152,9 @@ class Shape(object):
font.setPointSize(8) font.setPointSize(8)
font.setBold(True) font.setBold(True)
painter.setFont(font) painter.setFont(font)
if(self.label == None): if self.label is None:
self.label = "" self.label = ""
if(min_y < MIN_Y_LABEL): if min_y < MIN_Y_LABEL:
min_y += MIN_Y_LABEL min_y += MIN_Y_LABEL
painter.drawText(min_x, min_y, self.label) painter.drawText(min_x, min_y, self.label)
...@@ -198,6 +214,8 @@ class Shape(object): ...@@ -198,6 +214,8 @@ class Shape(object):
def copy(self): def copy(self):
shape = Shape("%s" % self.label) shape = Shape("%s" % self.label)
shape.points = [p for p in self.points] shape.points = [p for p in self.points]
shape.center = self.center
shape.direction = self.direction
shape.fill = self.fill shape.fill = self.fill
shape.selected = self.selected shape.selected = self.selected
shape._closed = self._closed shape._closed = self._closed
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册