Skip to content

Commit 2862734

Browse files
Enchance box manipulation and fix CTRL+B bug (#255)
* Enchance box manipulation and fix CTRL+B bug * Improve shape handling
1 parent 207c60e commit 2862734

10 files changed

Lines changed: 193 additions & 94 deletions

File tree

PPOCRLabel.py

Lines changed: 101 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,15 @@ def get_str(str_id):
931931
enabled=True,
932932
)
933933

934+
convertToRect = action(
935+
get_str("convertToRect"),
936+
self.convertToRect,
937+
"Ctrl+T",
938+
"edit",
939+
get_str("convertToRectDetail"),
940+
enabled=False,
941+
)
942+
934943
settings_action = action(
935944
get_str("settings"),
936945
self.showSettingsDialog,
@@ -1012,6 +1021,7 @@ def get_str(str_id):
10121021
delete=delete,
10131022
edit=edit,
10141023
focusAndZoom=focusAndZoom,
1024+
convertToRect=convertToRect,
10151025
copy=copy,
10161026
saveRec=saveRec,
10171027
singleRere=singleRere,
@@ -1054,6 +1064,7 @@ def get_str(str_id):
10541064
createpoly,
10551065
edit,
10561066
focusAndZoom,
1067+
convertToRect,
10571068
copy,
10581069
delete,
10591070
singleRere,
@@ -1078,6 +1089,7 @@ def get_str(str_id):
10781089
createpoly,
10791090
edit,
10801091
focusAndZoom,
1092+
convertToRect,
10811093
copy,
10821094
delete,
10831095
singleRere,
@@ -1815,6 +1827,7 @@ def shapeSelectionChanged(self, selected_shapes):
18151827
self.actions.lock.setEnabled(n_selected)
18161828
self.actions.change_cls.setEnabled(n_selected)
18171829
self.actions.expand.setEnabled(n_selected)
1830+
self.actions.convertToRect.setEnabled(n_selected > 0)
18181831

18191832
def addLabel(self, shape):
18201833
shape.paintLabel = self.displayLabelOption.isChecked()
@@ -3868,70 +3881,115 @@ def get_top_left(rect):
38683881
[max(p[1] for p in rect) - min(p[1] for p in rect) for rect in rectangles]
38693882
) / len(rectangles)
38703883
threshold = avg_height * row_height_threshold
3884+
3885+
# Keep track of original indices to handle duplicates correctly
38713886
indexed_rects = [(i, get_top_left(rect)) for i, rect in enumerate(rectangles)]
38723887
indexed_rects.sort(key=lambda x: x[1][1])
3888+
38733889
rows = []
38743890
current_row = []
3875-
last_y = indexed_rects[0][1][1]
3876-
for item in indexed_rects:
3877-
i, (x, y) = item
3878-
if abs(y - last_y) <= threshold:
3879-
current_row.append(item)
3880-
else:
3891+
if indexed_rects:
3892+
last_y = indexed_rects[0][1][1]
3893+
for item in indexed_rects:
3894+
i, (x, y) = item
3895+
if abs(y - last_y) <= threshold:
3896+
current_row.append(item)
3897+
else:
3898+
rows.append(current_row)
3899+
current_row = [item]
3900+
last_y = y
3901+
if current_row:
38813902
rows.append(current_row)
3882-
current_row = [item]
3883-
last_y = y
3884-
if current_row:
3885-
rows.append(current_row)
3886-
sorted_rects = []
3903+
3904+
sorted_indices = []
38873905
for row in rows:
38883906
row.sort(key=lambda x: x[1][0])
3889-
sorted_rects.extend([rectangles[i] for i, _ in row])
3890-
return sorted_rects
3907+
sorted_indices.extend([i for i, _ in row])
3908+
return sorted_indices
38913909

38923910
def resortBoxPosition(self):
3893-
# get original elements
3894-
items = []
3895-
for i in range(self.BoxList.count()):
3896-
item = self.BoxList.item(i)
3897-
items.append({"text": item.text(), "object": item})
3898-
# get coordinate points
3911+
# get coordinate points from shapes directly to be more reliable
38993912
rectangles = []
3900-
for item in items:
3901-
text = item["text"]
3902-
try:
3903-
rect = ast.literal_eval(text) # 转为列表
3904-
rectangles.append(rect)
3905-
except (ValueError, SyntaxError) as e:
3906-
logger.error(f"Error parsing text: {text}")
3907-
continue
3908-
# start resort
3909-
sorted_rectangles = self.sort_rectangles(rectangles, row_height_threshold=0.5)
3910-
# old_idx <--> new_idx
3911-
index_map = []
3912-
for sorted_rect in sorted_rectangles:
3913-
for old_idx, rect in enumerate(rectangles):
3914-
if rect == sorted_rect:
3915-
index_map.append(old_idx)
3916-
break
3917-
# resort BoxList labelList canvas.shapes
3918-
items = [self.BoxList.takeItem(0) for _ in range(self.BoxList.count())]
3913+
for shape in self.canvas.shapes:
3914+
rect = [[int(p.x()), int(p.y())] for p in shape.points]
3915+
rectangles.append(rect)
3916+
3917+
if not rectangles:
3918+
return
3919+
3920+
# start resort - now returns indices
3921+
index_map = self.sort_rectangles(rectangles, row_height_threshold=0.5)
3922+
3923+
if len(index_map) != len(self.canvas.shapes):
3924+
logger.error("Resort failed: index map size mismatch")
3925+
return
3926+
3927+
# resort BoxList, labelList, and canvas.shapes
3928+
# Take all items out first
3929+
items_box = [self.BoxList.takeItem(0) for _ in range(self.BoxList.count())]
39193930
items_label = [
39203931
self.labelList.takeItem(0) for _ in range(self.labelList.count())
39213932
]
3922-
shapes = self.canvas.shapes
3933+
shapes = list(self.canvas.shapes)
3934+
39233935
self.canvas.shapes = []
3924-
for new_idx in range(len(index_map)):
3925-
old_idx = index_map[new_idx]
3926-
self.BoxList.insertItem(new_idx, items[old_idx])
3936+
for new_idx, old_idx in enumerate(index_map):
3937+
self.BoxList.insertItem(new_idx, items_box[old_idx])
39273938
self.labelList.insertItem(new_idx, items_label[old_idx])
3928-
self.canvas.shapes.insert(new_idx, shapes[old_idx])
3939+
self.canvas.shapes.append(shapes[old_idx])
3940+
3941+
# Update internal indices and refresh UI
3942+
self.updateIndexList()
3943+
for i, shape in enumerate(self.canvas.shapes):
3944+
shape.idx = i
3945+
3946+
self.canvas.update()
3947+
self.setDirty()
3948+
39293949
QMessageBox.information(
39303950
self,
39313951
"Information",
39323952
"resort success!",
39333953
)
39343954

3955+
def convertToRect(self):
3956+
if not self.canvas.selectedShapes:
3957+
return
3958+
3959+
changed = False
3960+
for shape in self.canvas.selectedShapes:
3961+
if not shape.points:
3962+
continue
3963+
3964+
if len(shape.points) == 4:
3965+
p0, p1, p2, p3 = shape.points
3966+
if (
3967+
p0.x() == p3.x()
3968+
and p0.y() == p1.y()
3969+
and p2.x() == p1.x()
3970+
and p2.y() == p3.y()
3971+
):
3972+
continue
3973+
3974+
min_x = min(p.x() for p in shape.points)
3975+
max_x = max(p.x() for p in shape.points)
3976+
min_y = min(p.y() for p in shape.points)
3977+
max_y = max(p.y() for p in shape.points)
3978+
3979+
shape.points = [
3980+
QPointF(min_x, min_y),
3981+
QPointF(max_x, min_y),
3982+
QPointF(max_x, max_y),
3983+
QPointF(min_x, max_y),
3984+
]
3985+
shape.close()
3986+
changed = True
3987+
3988+
if changed:
3989+
self.updateBoxlist()
3990+
self.setDirty()
3991+
self.canvas.repaint()
3992+
39353993

39363994
def inverted(color):
39373995
return QColor(*[255 - v for v in color.getRgb()])

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ labeling in the Excel file, the recommended steps are:
244244
| Ctrl + R | Re-recognize the selected box |
245245
| Ctrl + C | Copy and paste the selected box |
246246
| Ctrl + B | Resort Bounding Box Positions |
247+
| Ctrl + T | Convert PolygonBox to RectBox |
247248
| Ctrl + Left Mouse Button | Multi select the label box |
248249
| Backspace or Delete | Delete the selected box |
249250
| Ctrl + V or End | Check image |

README_ch.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ PPOCRLabel.exe --lang ch
220220
| Ctrl + R | 重新识别所选标记 |
221221
| Ctrl + C | 【复制并粘贴】选中的标记框 |
222222
| Ctrl + B | 重新排序坐标框位置 |
223+
| Ctrl + T | 将多边形框转换为矩形框 |
223224
| Ctrl + 鼠标左键 | 多选标记框 |
224225
| Backspace 或 Delete | 删除所选框 |
225226
| Ctrl + V 或 End | 确认本张图片标记 |

libs/autoDialog.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,11 @@ def run(self):
7272
"The size of %s is too small to be recognised",
7373
img_path,
7474
)
75-
self.result_dic = None
75+
self.result_dic = [] # Clear it instead of None
7676

7777
# 结果保存
78-
if self.result_dic is None or len(self.result_dic) == 0:
78+
if not self.result_dic:
7979
logger.warning("No text detected in file %s", img_path)
80-
pass
8180
else:
8281
strs = ""
8382
for res in self.result_dic:
@@ -93,12 +92,16 @@ def run(self):
9392
+ json.dumps(posi)
9493
+ "\n"
9594
)
96-
# Sending large amounts of data repeatedly through pyqtSignal may affect the program efficiency
95+
9796
self.listValue.emit(strs)
9897
self.mainThread.result_dic = self.result_dic
9998
self.mainThread.filePath = img_path
100-
# 保存
10199
self.mainThread.saveFile(mode="Auto")
100+
# CRITICAL: Clear the result_dic after saving to prevent it from
101+
# leaking into the next image or back into the main UI
102+
self.mainThread.result_dic = []
103+
self.result_dic = []
104+
102105
findex += 1
103106
self.progressBarValue.emit(findex)
104107
else:

libs/canvas.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,15 @@ def boundedMoveVertex(self, pos):
472472
else:
473473
shiftPos = pos - point
474474

475-
if [shape[0].x(), shape[0].y(), shape[2].x(), shape[2].y()] == [
475+
# Symmetric resizing with Ctrl
476+
is_ctrl_pressed = int(QApplication.keyboardModifiers()) == Qt.ControlModifier
477+
478+
if len(shape.points) == 4 and [
479+
shape[0].x(),
480+
shape[0].y(),
481+
shape[2].x(),
482+
shape[2].y(),
483+
] == [
476484
shape[3].x(),
477485
shape[1].y(),
478486
shape[1].x(),
@@ -492,8 +500,22 @@ def boundedMoveVertex(self, pos):
492500
shape.moveVertexBy(rindex, rshift)
493501
shape.moveVertexBy(lindex, lshift)
494502

503+
if is_ctrl_pressed:
504+
opp_index = (index + 2) % 4
505+
shape.moveVertexBy(opp_index, -shiftPos)
506+
shape.moveVertexBy((opp_index + 1) % 4, -lshift)
507+
shape.moveVertexBy((opp_index + 3) % 4, -rshift)
508+
495509
else:
496510
shape.moveVertexBy(index, shiftPos)
511+
if is_ctrl_pressed and len(shape.points) > 1:
512+
# Calculate symmetric opposite index for simple shapes
513+
if len(shape.points) == 4:
514+
opp_index = (index + 2) % 4
515+
shape.moveVertexBy(opp_index, -shiftPos)
516+
elif len(shape.points) == 2:
517+
opp_index = (index + 1) % 2
518+
shape.moveVertexBy(opp_index, -shiftPos)
497519

498520
def boundedMoveShape(self, shapes, pos):
499521
if type(shapes).__name__ != "list":

0 commit comments

Comments
 (0)