Merge pull request #5323 from PeterH0323/dygraph
【PPOCRLabel】4 Bugs Fixed and 2 New Feature Addedpull/5358/head
commit
6cc7580dfc
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,8 @@ PPOCRLabel is a semi-automatic graphic annotation tool suitable for OCR field, w
|
|||
|
||||
### 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:
|
||||
- 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))
|
||||
|
@ -110,7 +112,7 @@ python PPOCRLabel.py
|
|||
|
||||
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.**
|
||||
|
||||
|
@ -143,15 +145,17 @@ python PPOCRLabel.py
|
|||
### 3.1 Shortcut keys
|
||||
|
||||
| Shortcut keys | Description |
|
||||
|--------------------------| ------------------------------------------------ |
|
||||
|--------------------------|--------------------------------------------------|
|
||||
| Ctrl + Shift + R | Re-recognize all the labels of the current image |
|
||||
| W | Create a rect 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 + R | Re-recognize the selected box |
|
||||
| Ctrl + C | Copy and paste the selected 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 + Shift + d | Delete image |
|
||||
| D | Next image |
|
||||
|
|
|
@ -8,6 +8,8 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
|
|||
|
||||
#### 近期更新
|
||||
|
||||
- 2022.01:(by [PeterH0323](https://github.com/peterh0323) )
|
||||
- 提升用户体验:新增文件与标记数目提示、优化交互、修复gpu使用等问题
|
||||
- 2021.11.17:
|
||||
- 新增支持通过whl包安装和启动PPOCRLabel(by [d2623587501](https://github.com/d2623587501))
|
||||
- 标注数据集切分:对标注数据进行训练、验证与测试集划分(参考下方3.5节,by [MrCuiHao](https://github.com/MrCuiHao))
|
||||
|
@ -102,7 +104,7 @@ python PPOCRLabel.py --lang ch
|
|||
4. 手动标注:点击 “矩形标注”(推荐直接在英文模式下点击键盘中的 “W”),用户可对当前图片中模型未检出的部分进行手动绘制标记框。点击键盘Q,则使用四点标注模式(或点击“编辑” - “四点标注”),用户依次点击4个点后,双击左键表示标注完成。
|
||||
5. 标记框绘制完成后,用户点击 “确认”,检测框会先被预分配一个 “待识别” 标签。
|
||||
6. 重新识别:将图片中的所有检测画绘制/调整完成后,点击 “重新识别”,PPOCR模型会对当前图片中的**所有检测框**重新识别<sup>[3]</sup>。
|
||||
7. 内容更改:双击识别结果,对不准确的识别结果进行手动更改。
|
||||
7. 内容更改:单击识别结果,对不准确的识别结果进行手动更改。
|
||||
8. **确认标记:点击 “确认”,图片状态切换为 “√”,跳转至下一张。**
|
||||
9. 删除:点击 “删除图像”,图片将会被删除至回收站。
|
||||
10. 导出结果:用户可以通过菜单中“文件-导出标记结果”手动导出,同时也可以点击“文件 - 自动导出标记结果”开启自动导出。手动确认过的标记将会被存放在所打开图片文件夹下的*Label.txt*中。在菜单栏点击 “文件” - "导出识别结果"后,会将此类图片的识别训练数据保存在*crop_img*文件夹下,识别标签保存在*rec_gt.txt*中<sup>[4]</sup>。
|
||||
|
@ -131,23 +133,25 @@ python PPOCRLabel.py --lang ch
|
|||
|
||||
### 3.1 快捷键
|
||||
|
||||
| 快捷键 | 说明 |
|
||||
|------------------| ---------------------------- |
|
||||
| Ctrl + shift + R | 对当前图片的所有标记重新识别 |
|
||||
| W | 新建矩形框 |
|
||||
| Q | 新建四点框 |
|
||||
| Ctrl + E | 编辑所选框标签 |
|
||||
| Ctrl + R | 重新识别所选标记 |
|
||||
| 快捷键 | 说明 |
|
||||
|------------------|----------------|
|
||||
| Ctrl + shift + R | 对当前图片的所有标记重新识别 |
|
||||
| W | 新建矩形框 |
|
||||
| Q | 新建四点框 |
|
||||
| X | 框逆时针旋转 |
|
||||
| C | 框顺时针旋转 |
|
||||
| Ctrl + E | 编辑所选框标签 |
|
||||
| Ctrl + R | 重新识别所选标记 |
|
||||
| Ctrl + C | 复制并粘贴选中的标记框 |
|
||||
| Ctrl + 鼠标左键 | 多选标记框 |
|
||||
| Ctrl + X | 删除所选框 |
|
||||
| Ctrl + V | 确认本张图片标记 |
|
||||
| Ctrl + Shift + d | 删除本张图片 |
|
||||
| D | 下一张图片 |
|
||||
| A | 上一张图片 |
|
||||
| Ctrl++ | 缩小 |
|
||||
| Ctrl-- | 放大 |
|
||||
| ↑→↓← | 移动标记框 |
|
||||
| Ctrl + 鼠标左键 | 多选标记框 |
|
||||
| Alt + X | 删除所选框 |
|
||||
| Ctrl + V | 确认本张图片标记 |
|
||||
| Ctrl + Shift + d | 删除本张图片 |
|
||||
| D | 下一张图片 |
|
||||
| A | 上一张图片 |
|
||||
| Ctrl++ | 缩小 |
|
||||
| Ctrl-- | 放大 |
|
||||
| ↑→↓← | 移动标记框 |
|
||||
|
||||
### 3.2 内置模型
|
||||
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# 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 @@
|
|||
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
try:
|
||||
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 *
|
||||
import copy
|
||||
|
||||
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.utils import distance
|
||||
import copy
|
||||
|
||||
CURSOR_DEFAULT = Qt.ArrowCursor
|
||||
CURSOR_POINT = Qt.PointingHandCursor
|
||||
|
@ -31,8 +25,6 @@ CURSOR_DRAW = Qt.CrossCursor
|
|||
CURSOR_MOVE = Qt.ClosedHandCursor
|
||||
CURSOR_GRAB = Qt.OpenHandCursor
|
||||
|
||||
# class Canvas(QGLWidget):
|
||||
|
||||
|
||||
class Canvas(QWidget):
|
||||
zoomRequest = pyqtSignal(int)
|
||||
|
@ -129,7 +121,6 @@ class Canvas(QWidget):
|
|||
def selectedVertex(self):
|
||||
return self.hVertex is not None
|
||||
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
"""Update line with last point and current coordinates."""
|
||||
pos = self.transformPos(ev.pos())
|
||||
|
@ -333,7 +324,6 @@ class Canvas(QWidget):
|
|||
|
||||
self.movingShape = False
|
||||
|
||||
|
||||
def endMove(self, copy=False):
|
||||
assert self.selectedShapes and self.selectedShapesCopy
|
||||
assert len(self.selectedShapesCopy) == len(self.selectedShapes)
|
||||
|
@ -410,7 +400,6 @@ class Canvas(QWidget):
|
|||
self.selectionChanged.emit(shapes)
|
||||
self.update()
|
||||
|
||||
|
||||
def selectShapePoint(self, point, multiple_selection_mode):
|
||||
"""Select the first shape created which contains this point."""
|
||||
if self.selectedVertex(): # A vertex is marked for selection.
|
||||
|
@ -494,7 +483,6 @@ class Canvas(QWidget):
|
|||
else:
|
||||
shape.moveVertexBy(index, shiftPos)
|
||||
|
||||
|
||||
def boundedMoveShape(self, shapes, pos):
|
||||
if type(shapes).__name__ != 'list': shapes = [shapes]
|
||||
if self.outOfPixmap(pos):
|
||||
|
@ -515,6 +503,7 @@ class Canvas(QWidget):
|
|||
if dp:
|
||||
for shape in shapes:
|
||||
shape.moveBy(dp)
|
||||
shape.close()
|
||||
self.prevPoint = pos
|
||||
return True
|
||||
return False
|
||||
|
@ -728,6 +717,31 @@ class Canvas(QWidget):
|
|||
self.moveOnePixel('Up')
|
||||
elif key == Qt.Key_Down and self.selectedShapes:
|
||||
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):
|
||||
# print(self.selectedShape.points)
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
import sys, time
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
# !/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from PyQt5.QtCore import QModelIndex
|
||||
from PyQt5.QtWidgets import QListWidget
|
||||
|
||||
|
||||
class EditInList(QListWidget):
|
||||
def __init__(self):
|
||||
super(EditInList,self).__init__()
|
||||
# click to edit
|
||||
self.clicked.connect(self.item_clicked)
|
||||
super(EditInList, self).__init__()
|
||||
self.edited_item = None
|
||||
|
||||
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 item_clicked(self, modelindex: QModelIndex):
|
||||
try:
|
||||
if self.edited_item is not None:
|
||||
self.closePersistentEditor(self.edited_item)
|
||||
except:
|
||||
self.edited_item = self.currentItem()
|
||||
|
||||
self.edited_item = self.item(modelindex.row())
|
||||
self.openPersistentEditor(self.edited_item)
|
||||
self.editItem(self.edited_item)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
# close edit
|
||||
for i in range(self.count()):
|
||||
self.closePersistentEditor(self.item(i))
|
||||
pass
|
||||
|
||||
def leaveEvent(self, event):
|
||||
# close edit
|
||||
for i in range(self.count()):
|
||||
self.closePersistentEditor(self.item(i))
|
||||
self.closePersistentEditor(self.item(i))
|
||||
|
|
|
@ -10,20 +10,15 @@
|
|||
# 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.
|
||||
#!/usr/bin/python
|
||||
# !/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
from libs.utils import distance
|
||||
import math
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QPointF
|
||||
from PyQt5.QtGui import QColor, QPen, QPainterPath, QFont
|
||||
from libs.utils import distance
|
||||
|
||||
DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
|
||||
DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
|
||||
DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255)
|
||||
|
@ -59,6 +54,8 @@ class Shape(object):
|
|||
self.difficult = difficult
|
||||
self.paintLabel = paintLabel
|
||||
self.locked = False
|
||||
self.direction = 0
|
||||
self.center = None
|
||||
self._highlightIndex = None
|
||||
self._highlightMode = self.NEAR_VERTEX
|
||||
self._highlightSettings = {
|
||||
|
@ -74,7 +71,24 @@ class Shape(object):
|
|||
# is used for drawing the pending line a different 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):
|
||||
self.center = QPointF((self.points[0].x() + self.points[2].x()) / 2,
|
||||
(self.points[0].y() + self.points[2].y()) / 2)
|
||||
self._closed = True
|
||||
|
||||
def reachMaxPoints(self):
|
||||
|
@ -83,7 +97,9 @@ class Shape(object):
|
|||
return False
|
||||
|
||||
def addPoint(self, point):
|
||||
if not self.reachMaxPoints(): # 4个点时发出close信号
|
||||
if self.reachMaxPoints():
|
||||
self.close()
|
||||
else:
|
||||
self.points.append(point)
|
||||
|
||||
def popPoint(self):
|
||||
|
@ -112,7 +128,7 @@ class Shape(object):
|
|||
# Uncommenting the following line will draw 2 paths
|
||||
# for the 1st vertex, and make it non-filled, which
|
||||
# may be desirable.
|
||||
#self.drawVertex(vrtx_path, 0)
|
||||
# self.drawVertex(vrtx_path, 0)
|
||||
|
||||
for i, p in enumerate(self.points):
|
||||
line_path.lineTo(p)
|
||||
|
@ -136,9 +152,9 @@ class Shape(object):
|
|||
font.setPointSize(8)
|
||||
font.setBold(True)
|
||||
painter.setFont(font)
|
||||
if(self.label == None):
|
||||
if self.label is None:
|
||||
self.label = ""
|
||||
if(min_y < MIN_Y_LABEL):
|
||||
if min_y < MIN_Y_LABEL:
|
||||
min_y += MIN_Y_LABEL
|
||||
painter.drawText(min_x, min_y, self.label)
|
||||
|
||||
|
@ -198,6 +214,8 @@ class Shape(object):
|
|||
def copy(self):
|
||||
shape = Shape("%s" % self.label)
|
||||
shape.points = [p for p in self.points]
|
||||
shape.center = self.center
|
||||
shape.direction = self.direction
|
||||
shape.fill = self.fill
|
||||
shape.selected = self.selected
|
||||
shape._closed = self._closed
|
||||
|
|
Loading…
Reference in New Issue