Skip to content

Commit 476b4db

Browse files
yes-githubGreatV
andauthored
add bbox auto zoom center and vertex moves independently (#87)
* add bbox auto zoom center and vertex moves independently * code optimization * fix code style --------- Co-authored-by: aboutibm@163.com <7p=e763wN3A6k+[C> Co-authored-by: Wang Xin <xinwang614@gmail.com>
1 parent 3201d08 commit 476b4db

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

PPOCRLabel.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
newIcon,
101101
rebuild_html_from_ppstructure_label,
102102
stepsInfo,
103+
polygon_bounding_box_center_and_area,
104+
map_value,
103105
struct,
104106
)
105107
from libs.labelColor import label_colormap
@@ -130,6 +132,7 @@ def __init__(
130132
lang="ch",
131133
gpu=False,
132134
img_list_natural_sort=True,
135+
bbox_auto_zoom_center=False,
133136
kie_mode=False,
134137
default_filename=None,
135138
default_predefined_class_file=None,
@@ -151,6 +154,7 @@ def __init__(
151154
self.lang = lang
152155
self.gpu = gpu
153156
self.img_list_natural_sort = img_list_natural_sort
157+
self.bbox_auto_zoom_center = bbox_auto_zoom_center
154158

155159
# Load string bundle for i18n
156160
if lang not in ["ch", "en"]:
@@ -1943,7 +1947,7 @@ def addZoom(self, increment=10):
19431947
int(self.zoomWidget.value() + increment)
19441948
) # set zoom slider value
19451949

1946-
def zoomRequest(self, delta):
1950+
def zoomRequest(self, delta, pos: QPoint = None):
19471951
# get the current scrollbar positions
19481952
# calculate the percentages ~ coordinates
19491953
h_bar = self.scrollBars[Qt.Horizontal]
@@ -1958,8 +1962,10 @@ def zoomRequest(self, delta):
19581962
# where 0 = move left
19591963
# 1 = move right
19601964
# up and down analogous
1961-
cursor = QCursor()
1962-
pos = cursor.pos()
1965+
if pos is None:
1966+
cursor = QCursor()
1967+
pos = cursor.pos()
1968+
19631969
relative_pos = QWidget.mapFromGlobal(self, pos)
19641970

19651971
cursor_x = relative_pos.x()
@@ -2013,6 +2019,7 @@ def togglePolygons(self, value):
20132019

20142020
def loadFile(self, filePath=None, isAdjustScale=True):
20152021
"""Load the specified file, or the last opened file if None."""
2022+
self.canvas.shape_move_index = None
20162023
if self.dirty:
20172024
self.mayContinue()
20182025
self.resetState()
@@ -2117,6 +2124,20 @@ def loadFile(self, filePath=None, isAdjustScale=True):
21172124
)
21182125

21192126
self.canvas.setFocus(True)
2127+
2128+
if self.bbox_auto_zoom_center:
2129+
if len(self.canvas.shapes) > 0:
2130+
(
2131+
center_x,
2132+
center_y,
2133+
shape_area,
2134+
) = polygon_bounding_box_center_and_area(
2135+
self.canvas.shapes[0].points
2136+
)
2137+
if shape_area < 30000:
2138+
zoom_value = 120 * map_value(shape_area, 100, 30000, 20, 0)
2139+
self.zoomRequest(zoom_value, QPoint(center_x, center_y))
2140+
# print(" =========> ", shape_area, " ==> ", zoom_value)
21202141
return True
21212142
return False
21222143

@@ -3548,6 +3569,9 @@ def get_main_app(argv=[]):
35483569
arg_parser.add_argument("--rec_model_dir", type=str, default=None, nargs="?")
35493570
arg_parser.add_argument("--rec_char_dict_path", type=str, default=None, nargs="?")
35503571
arg_parser.add_argument("--cls_model_dir", type=str, default=None, nargs="?")
3572+
arg_parser.add_argument(
3573+
"--bbox_auto_zoom_center", type=str2bool, default=False, nargs="?"
3574+
)
35513575

35523576
args = arg_parser.parse_args(argv[1:])
35533577

@@ -3561,6 +3585,7 @@ def get_main_app(argv=[]):
35613585
rec_model_dir=args.rec_model_dir,
35623586
rec_char_dict_path=args.rec_char_dict_path,
35633587
cls_model_dir=args.cls_model_dir,
3588+
bbox_auto_zoom_center=args.bbox_auto_zoom_center,
35643589
)
35653590
win.show()
35663591
return app, win

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ PPOCRLabelv2 is a semi-automatic graphic annotation tool suitable for OCR field,
2424
- `rec_model_dir`: Path to the recognition model directory
2525
- `rec_char_dict_path`: Path to the recognition model dictionary file
2626
- `cls_model_dir`: Path to the classification model directory
27+
- Added the `--bbox_auto_zoom_center` parameter, which can be enabled when there is only one bounding box in the image, automatically centering and zooming in on the bounding box.
28+
- Added 5 shortcut keys `z`, `x`, `c`, `v`, `b` for controlling the 4 vertices of the bounding box. For usage details, see the '11. Additional Functionality Description' in "2.1 Operating Procedures" below.
2729
- 2022.05: Add table annotations, follow `2.2 Table Annotations` for more information (by [whjdark](https://github.com/peterh0323); [Evezerest](https://github.com/Evezerest))
2830
- 2022.02: (by [PeterH0323](https://github.com/peterh0323))
2931
- Add KIE Mode by using `--kie`, for [detection + identification + keyword extraction] labeling.
@@ -166,6 +168,12 @@ PPOCRLabel.exe --lang ch
166168
11. Additional Feature Description
167169
- `File` -> `Re-recognition`: After checking, the newly annotated box content will automatically trigger the `Re-recognition` function of the current annotation box, eliminating the need to click the Re-identify button. This is suitable for scenarios where you do not want to use Automatic Annotation but prefer manual annotation, such as license plate recognition. In a single image with only one license plate, using Automatic Annotation would require deleting many additional recognized text boxes, which is less efficient than directly re-annotating.
168170
- `File` -> `Auto Save Unsaved changes`: By default, you need to press the `Check` button to complete the marking confirmation for the current box, which can be cumbersome. After checking, when switching to the next image (by pressing the shortcut key `D`), a prompt box asking to confirm whether to save unconfirmed markings will no longer appear. The current markings will be automatically saved and the next image will be switched, making it convenient for quick marking.
171+
- After selecting the bounding box, there are 5 shortcut keys available to individually control the movement of the four vertices of the bounding box, suitable for scenarios that require precise control over the positions of the bounding box vertices:
172+
- `z`: After pressing, the up, down, left, and right arrow keys will move the 1st vertex individually.
173+
- `x`: After pressing, the up, down, left, and right arrow keys will move the 2nd vertex individually.
174+
- `c`: After pressing, the up, down, left, and right arrow keys will move the 3rd vertex individually.
175+
- `v`: After pressing, the up, down, left, and right arrow keys will move the 4th vertex individually.
176+
- `b`: After pressing, the up, down, left, and right arrow keys will revert to the default action of moving the entire bounding box.
169177

170178
### 2.2 Table Annotation
171179

@@ -217,8 +225,6 @@ labeling in the Excel file, the recommended steps are:
217225
| Ctrl + Shift + R | Re-recognize all the labels of the current image |
218226
| W | Create a rect box |
219227
| Q or Home | Create a multi-points box |
220-
| X | Rotate the box anti-clockwise |
221-
| C | Rotate the box clockwise |
222228
| Ctrl + E | Edit label of the selected box |
223229
| Ctrl + X | Change key class of the box when enable `--kie` |
224230
| Ctrl + R | Re-recognize the selected box |
@@ -232,6 +238,7 @@ labeling in the Excel file, the recommended steps are:
232238
| Ctrl++ | Zoom in |
233239
| Ctrl-- | Zoom out |
234240
| ↑→↓← | Move selected box |
241+
| Z, X, C, V, B | Move the four vertices of the selected bounding box individually|
235242

236243
### 3.2 Built-in Model
237244

README_ch.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ PPOCRLabel是一款适用于OCR领域的半自动化图形标注工具,内置P
2424
- `rec_model_dir` :识别模型目录路径
2525
- `rec_char_dict_path` :识别模型字典文件路径
2626
- `cls_model_dir` :分类模型目录路径
27+
- 新增`--bbox_auto_zoom_center`参数,当图片只有一个标记框的时候,可以开启,会自动将标记框居中放大
28+
- 新增5个控制标记框4个顶点的快捷键`z``x``c``v``b`,使用方法详见下方`2.1 操作步骤``11. 补充功能说明`
2729
- 2022.05:**新增表格标注**,使用方法见下方`2.2 表格标注`(by [whjdark](https://github.com/peterh0323); [Evezerest](https://github.com/Evezerest)
2830
- 2022.02:**新增关键信息标注**、优化标注体验(by [PeterH0323](https://github.com/peterh0323)
2931
- 新增:使用 `--kie` 进入 KIE 功能,用于打【检测+识别+关键字提取】的标签
@@ -152,6 +154,12 @@ PPOCRLabel.exe --lang ch
152154
11. 补充功能说明
153155
- `文件` -> `自动重新识别` : 勾选后,对于新标注的框内容会自动触发当前标注框的重新识别功能,不需要再去点击`重新识别`按钮,适合各种原因不想使用`自动标注`只想手动标注的场景,例如车牌识别,一张图里只有一个车牌,如果使用`自动标注`,需要删除很多额外识别出来的文字框,不如直接重新标注
154156
- `文件` -> `自动保存未提交变更` : 默认是按`确认`按钮完成当前框的标记确认,有点繁琐,勾选后,切换下一张图(按快捷键`D`)的时候,不再弹出提示框确认是否保存未确认的标记,自动保存当前标记并切换下一张图,方便快速标记
157+
- 选中标记框后,5个可以控制标记框四个顶点单独移动的快捷键,适合需要精确控制标记框四个顶点位置的场景
158+
- `z` :按下后,此时使用键盘的上下左右按键将单独移动第1个顶点
159+
- `x` :按下后,此时使用键盘的上下左右按键将单独移动第2个顶点
160+
- `c` :按下后,此时使用键盘的上下左右按键将单独移动第3个顶点
161+
- `v` :按下后,此时使用键盘的上下左右按键将单独移动第4个顶点
162+
- `b` :按下后,此时使用键盘的上下左右按键将恢复默认的整体移动整个标记框
155163

156164
### 2.2 表格标注([视频演示](https://www.bilibili.com/video/BV1wR4y1v7JE/?share_source=copy_web&vd_source=cf1f9d24648d49636e3d109c9f9a377d&t=1998)
157165

@@ -196,8 +204,6 @@ PPOCRLabel.exe --lang ch
196204
| Ctrl + shift + R | 对当前图片的所有标记重新识别 |
197205
| W | 新建矩形框 |
198206
| Q 或 Home | 新建多点框 |
199-
| X | 框逆时针旋转 |
200-
| C | 框顺时针旋转 |
201207
| Ctrl + E | 编辑所选框标签 |
202208
| Ctrl + X | `--kie` 模式下,修改 Box 的关键字种类 |
203209
| Ctrl + R | 重新识别所选标记 |
@@ -211,6 +217,7 @@ PPOCRLabel.exe --lang ch
211217
| Ctrl++ | 缩小 |
212218
| Ctrl-- | 放大 |
213219
| ↑→↓← | 移动标记框 |
220+
| Z、X、C、V、B | 对选中的标记框,单独移动四个顶点 |
214221

215222
### 3.2 内置模型
216223

libs/canvas.py

+48-16
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class Canvas(QWidget):
4040

4141
epsilon = 5.0
4242

43+
shape_move_index = None
44+
4345
def __init__(self, *args, **kwargs):
4446
super(Canvas, self).__init__(*args, **kwargs)
4547
# Initialise local state.
@@ -754,6 +756,39 @@ def keyPressEvent(self, ev):
754756
self.moveOnePixel("Up")
755757
elif key == Qt.Key_Down and self.selectedShapes:
756758
self.moveOnePixel("Down")
759+
elif key == Qt.Key_Z and self.selectedShapes:
760+
self.shape_move_index = 0
761+
select_shape = self.selectedShapes[0]
762+
select_shape.highlightVertex(
763+
self.shape_move_index, select_shape.MOVE_VERTEX
764+
)
765+
self.update()
766+
elif key == Qt.Key_X and self.selectedShapes:
767+
self.shape_move_index = 1
768+
select_shape = self.selectedShapes[0]
769+
select_shape.highlightVertex(
770+
self.shape_move_index, select_shape.MOVE_VERTEX
771+
)
772+
self.update()
773+
elif key == Qt.Key_C and self.selectedShapes:
774+
self.shape_move_index = 2
775+
select_shape = self.selectedShapes[0]
776+
select_shape.highlightVertex(
777+
self.shape_move_index, select_shape.MOVE_VERTEX
778+
)
779+
self.update()
780+
elif key == Qt.Key_V and self.selectedShapes:
781+
self.shape_move_index = 3
782+
select_shape = self.selectedShapes[0]
783+
select_shape.highlightVertex(
784+
self.shape_move_index, select_shape.MOVE_VERTEX
785+
)
786+
self.update()
787+
elif key == Qt.Key_B and self.selectedShapes:
788+
self.shape_move_index = None
789+
select_shape = self.selectedShapes[0]
790+
select_shape.highlightClear()
791+
self.update()
757792
elif key == Qt.Key_X and self.selectedShapes:
758793
for i in range(len(self.selectedShapes)):
759794
self.selectedShape = self.selectedShapes[i]
@@ -788,34 +823,31 @@ def moveOnePixel(self, direction):
788823
self.selectedShape = self.selectedShapes[i]
789824
if direction == "Left" and not self.moveOutOfBound(QPointF(-1.0, 0)):
790825
# print("move Left one pixel")
791-
self.selectedShape.points[0] += QPointF(-1.0, 0)
792-
self.selectedShape.points[1] += QPointF(-1.0, 0)
793-
self.selectedShape.points[2] += QPointF(-1.0, 0)
794-
self.selectedShape.points[3] += QPointF(-1.0, 0)
826+
self.move_points(QPointF(-1.0, 0))
795827
elif direction == "Right" and not self.moveOutOfBound(QPointF(1.0, 0)):
796828
# print("move Right one pixel")
797-
self.selectedShape.points[0] += QPointF(1.0, 0)
798-
self.selectedShape.points[1] += QPointF(1.0, 0)
799-
self.selectedShape.points[2] += QPointF(1.0, 0)
800-
self.selectedShape.points[3] += QPointF(1.0, 0)
829+
self.move_points(QPointF(1.0, 0))
801830
elif direction == "Up" and not self.moveOutOfBound(QPointF(0, -1.0)):
802831
# print("move Up one pixel")
803-
self.selectedShape.points[0] += QPointF(0, -1.0)
804-
self.selectedShape.points[1] += QPointF(0, -1.0)
805-
self.selectedShape.points[2] += QPointF(0, -1.0)
806-
self.selectedShape.points[3] += QPointF(0, -1.0)
832+
self.move_points(QPointF(0, -1.0))
807833
elif direction == "Down" and not self.moveOutOfBound(QPointF(0, 1.0)):
808834
# print("move Down one pixel")
809-
self.selectedShape.points[0] += QPointF(0, 1.0)
810-
self.selectedShape.points[1] += QPointF(0, 1.0)
811-
self.selectedShape.points[2] += QPointF(0, 1.0)
812-
self.selectedShape.points[3] += QPointF(0, 1.0)
835+
self.move_points(QPointF(0, 1.0))
813836
shapesBackup = []
814837
shapesBackup = copy.deepcopy(self.shapes)
815838
self.shapesBackups.append(shapesBackup)
816839
self.shapeMoved.emit()
817840
self.repaint()
818841

842+
def move_points(self, p: QPointF):
843+
if self.shape_move_index is None:
844+
self.selectedShape.points[0] += p
845+
self.selectedShape.points[1] += p
846+
self.selectedShape.points[2] += p
847+
self.selectedShape.points[3] += p
848+
else:
849+
self.selectedShape.points[self.shape_move_index] += p
850+
819851
def moveOutOfBound(self, step):
820852
points = [p1 + p2 for p1, p2 in zip(self.selectedShape.points, [step] * 4)]
821853
return True in map(self.outOfPixmap, points)

libs/utils.py

+45
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ def keysInfo(lang="en"):
322322
"Ctrl++\t\t\t缩小\n"
323323
"Ctrl--\t\t\t放大\n"
324324
"↑→↓←\t\t\t移动标记框\n"
325+
"Z、X、C、V、B\t\t\t对选中的标记框,单独移动四个顶点\n"
325326
"———————————————————————\n"
326327
"注:Mac用户Command键替换上述Ctrl键"
327328
)
@@ -351,8 +352,52 @@ def keysInfo(lang="en"):
351352
"Ctrl++\t\t\tZoom in\n"
352353
"Ctrl--\t\t\tZoom out\n"
353354
"↑→↓←\t\t\tMove selected box"
355+
"Z, X, C, V, B\t\tMove the four vertices of \n"
356+
and "\t\t\tthe selected bounding box individually"
354357
"———————————————————————\n"
355358
"Notice:For Mac users, use the 'Command' key instead of the 'Ctrl' key"
356359
)
357360

358361
return msg
362+
363+
364+
def polygon_bounding_box_center_and_area(points):
365+
"""
366+
Calculate the center and area of the bounding rectangle of a polygon
367+
"""
368+
if len(points) < 3:
369+
raise ValueError("At least three points are required to form a polygon")
370+
371+
area = 0
372+
min_x = float("inf")
373+
max_x = float("-inf")
374+
min_y = float("inf")
375+
max_y = float("-inf")
376+
377+
n = len(points)
378+
for i in range(n):
379+
x1 = points[i].x()
380+
y1 = points[i].y()
381+
x2 = points[(i + 1) % n].x()
382+
y2 = points[(i + 1) % n].y()
383+
area += x1 * y2 - x2 * y1
384+
385+
min_x = min(min_x, x1)
386+
max_x = max(max_x, x1)
387+
min_y = min(min_y, y1)
388+
max_y = max(max_y, y1)
389+
390+
area = abs(area) / 2.0
391+
center_x = (min_x + max_x) / 2
392+
center_y = (min_y + max_y) / 2
393+
394+
return center_x, center_y, area
395+
396+
397+
def map_value(x, in_min, in_max, out_min, out_max):
398+
"""
399+
Map the numerical value x from the range of [in_in, in_max] to the range of [out_in, out_max]
400+
"""
401+
if in_max == in_min:
402+
raise ValueError("in_max and in_min cannot be equal")
403+
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

0 commit comments

Comments
 (0)