Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 101 additions & 43 deletions PPOCRLabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,15 @@ def get_str(str_id):
enabled=True,
)

convertToRect = action(
get_str("convertToRect"),
self.convertToRect,
"Ctrl+T",
"edit",
get_str("convertToRectDetail"),
enabled=False,
)

settings_action = action(
get_str("settings"),
self.showSettingsDialog,
Expand Down Expand Up @@ -1012,6 +1021,7 @@ def get_str(str_id):
delete=delete,
edit=edit,
focusAndZoom=focusAndZoom,
convertToRect=convertToRect,
copy=copy,
saveRec=saveRec,
singleRere=singleRere,
Expand Down Expand Up @@ -1054,6 +1064,7 @@ def get_str(str_id):
createpoly,
edit,
focusAndZoom,
convertToRect,
copy,
delete,
singleRere,
Expand All @@ -1078,6 +1089,7 @@ def get_str(str_id):
createpoly,
edit,
focusAndZoom,
convertToRect,
copy,
delete,
singleRere,
Expand Down Expand Up @@ -1815,6 +1827,7 @@ def shapeSelectionChanged(self, selected_shapes):
self.actions.lock.setEnabled(n_selected)
self.actions.change_cls.setEnabled(n_selected)
self.actions.expand.setEnabled(n_selected)
self.actions.convertToRect.setEnabled(n_selected > 0)

def addLabel(self, shape):
shape.paintLabel = self.displayLabelOption.isChecked()
Expand Down Expand Up @@ -3868,70 +3881,115 @@ def get_top_left(rect):
[max(p[1] for p in rect) - min(p[1] for p in rect) for rect in rectangles]
) / len(rectangles)
threshold = avg_height * row_height_threshold

# Keep track of original indices to handle duplicates correctly
indexed_rects = [(i, get_top_left(rect)) for i, rect in enumerate(rectangles)]
indexed_rects.sort(key=lambda x: x[1][1])

rows = []
current_row = []
last_y = indexed_rects[0][1][1]
for item in indexed_rects:
i, (x, y) = item
if abs(y - last_y) <= threshold:
current_row.append(item)
else:
if indexed_rects:
last_y = indexed_rects[0][1][1]
for item in indexed_rects:
i, (x, y) = item
if abs(y - last_y) <= threshold:
current_row.append(item)
else:
rows.append(current_row)
current_row = [item]
last_y = y
if current_row:
rows.append(current_row)
current_row = [item]
last_y = y
if current_row:
rows.append(current_row)
sorted_rects = []

sorted_indices = []
for row in rows:
row.sort(key=lambda x: x[1][0])
sorted_rects.extend([rectangles[i] for i, _ in row])
return sorted_rects
sorted_indices.extend([i for i, _ in row])
return sorted_indices

def resortBoxPosition(self):
# get original elements
items = []
for i in range(self.BoxList.count()):
item = self.BoxList.item(i)
items.append({"text": item.text(), "object": item})
# get coordinate points
# get coordinate points from shapes directly to be more reliable
rectangles = []
for item in items:
text = item["text"]
try:
rect = ast.literal_eval(text) # 转为列表
rectangles.append(rect)
except (ValueError, SyntaxError) as e:
logger.error(f"Error parsing text: {text}")
continue
# start resort
sorted_rectangles = self.sort_rectangles(rectangles, row_height_threshold=0.5)
# old_idx <--> new_idx
index_map = []
for sorted_rect in sorted_rectangles:
for old_idx, rect in enumerate(rectangles):
if rect == sorted_rect:
index_map.append(old_idx)
break
# resort BoxList labelList canvas.shapes
items = [self.BoxList.takeItem(0) for _ in range(self.BoxList.count())]
for shape in self.canvas.shapes:
rect = [[int(p.x()), int(p.y())] for p in shape.points]
rectangles.append(rect)

if not rectangles:
return

# start resort - now returns indices
index_map = self.sort_rectangles(rectangles, row_height_threshold=0.5)

if len(index_map) != len(self.canvas.shapes):
logger.error("Resort failed: index map size mismatch")
return

# resort BoxList, labelList, and canvas.shapes
# Take all items out first
items_box = [self.BoxList.takeItem(0) for _ in range(self.BoxList.count())]
items_label = [
self.labelList.takeItem(0) for _ in range(self.labelList.count())
]
shapes = self.canvas.shapes
shapes = list(self.canvas.shapes)

self.canvas.shapes = []
for new_idx in range(len(index_map)):
old_idx = index_map[new_idx]
self.BoxList.insertItem(new_idx, items[old_idx])
for new_idx, old_idx in enumerate(index_map):
self.BoxList.insertItem(new_idx, items_box[old_idx])
self.labelList.insertItem(new_idx, items_label[old_idx])
self.canvas.shapes.insert(new_idx, shapes[old_idx])
self.canvas.shapes.append(shapes[old_idx])

# Update internal indices and refresh UI
self.updateIndexList()
for i, shape in enumerate(self.canvas.shapes):
shape.idx = i

self.canvas.update()
self.setDirty()

QMessageBox.information(
self,
"Information",
"resort success!",
)

def convertToRect(self):
if not self.canvas.selectedShapes:
return

changed = False
for shape in self.canvas.selectedShapes:
if not shape.points:
continue

if len(shape.points) == 4:
p0, p1, p2, p3 = shape.points
if (
p0.x() == p3.x()
and p0.y() == p1.y()
and p2.x() == p1.x()
and p2.y() == p3.y()
):
continue

min_x = min(p.x() for p in shape.points)
max_x = max(p.x() for p in shape.points)
min_y = min(p.y() for p in shape.points)
max_y = max(p.y() for p in shape.points)
Comment on lines +3972 to +3977
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convertToRect() assumes every selected shape has at least one point. If a selected shape ever has shape.points == [], the min()/max() calls will raise ValueError and crash the action. Add a guard (e.g., skip shapes with no points) before computing bounds.

Copilot uses AI. Check for mistakes.

shape.points = [
QPointF(min_x, min_y),
QPointF(max_x, min_y),
QPointF(max_x, max_y),
QPointF(min_x, max_y),
]
shape.close()
changed = True

if changed:
self.updateBoxlist()
self.setDirty()
self.canvas.repaint()


def inverted(color):
return QColor(*[255 - v for v in color.getRgb()])
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ labeling in the Excel file, the recommended steps are:
| Ctrl + R | Re-recognize the selected box |
| Ctrl + C | Copy and paste the selected box |
| Ctrl + B | Resort Bounding Box Positions |
| Ctrl + T | Convert PolygonBox to RectBox |
| Ctrl + Left Mouse Button | Multi select the label box |
| Backspace or Delete | Delete the selected box |
| Ctrl + V or End | Check image |
Expand Down
1 change: 1 addition & 0 deletions README_ch.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ PPOCRLabel.exe --lang ch
| Ctrl + R | 重新识别所选标记 |
| Ctrl + C | 【复制并粘贴】选中的标记框 |
| Ctrl + B | 重新排序坐标框位置 |
| Ctrl + T | 将多边形框转换为矩形框 |
| Ctrl + 鼠标左键 | 多选标记框 |
| Backspace 或 Delete | 删除所选框 |
| Ctrl + V 或 End | 确认本张图片标记 |
Expand Down
13 changes: 8 additions & 5 deletions libs/autoDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ def run(self):
"The size of %s is too small to be recognised",
img_path,
)
self.result_dic = None
self.result_dic = [] # Clear it instead of None

# 结果保存
if self.result_dic is None or len(self.result_dic) == 0:
if not self.result_dic:
logger.warning("No text detected in file %s", img_path)
pass
else:
strs = ""
for res in self.result_dic:
Expand All @@ -93,12 +92,16 @@ def run(self):
+ json.dumps(posi)
+ "\n"
)
# Sending large amounts of data repeatedly through pyqtSignal may affect the program efficiency

self.listValue.emit(strs)
self.mainThread.result_dic = self.result_dic
self.mainThread.filePath = img_path
# 保存
self.mainThread.saveFile(mode="Auto")
# CRITICAL: Clear the result_dic after saving to prevent it from
# leaking into the next image or back into the main UI
self.mainThread.result_dic = []
self.result_dic = []

findex += 1
self.progressBarValue.emit(findex)
else:
Expand Down
24 changes: 23 additions & 1 deletion libs/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,15 @@ def boundedMoveVertex(self, pos):
else:
shiftPos = pos - point

if [shape[0].x(), shape[0].y(), shape[2].x(), shape[2].y()] == [
# Symmetric resizing with Ctrl
is_ctrl_pressed = int(QApplication.keyboardModifiers()) == Qt.ControlModifier

if len(shape.points) == 4 and [
shape[0].x(),
shape[0].y(),
shape[2].x(),
shape[2].y(),
] == [
shape[3].x(),
shape[1].y(),
shape[1].x(),
Expand All @@ -492,8 +500,22 @@ def boundedMoveVertex(self, pos):
shape.moveVertexBy(rindex, rshift)
shape.moveVertexBy(lindex, lshift)

if is_ctrl_pressed:
opp_index = (index + 2) % 4
shape.moveVertexBy(opp_index, -shiftPos)
shape.moveVertexBy((opp_index + 1) % 4, -lshift)
shape.moveVertexBy((opp_index + 3) % 4, -rshift)

else:
shape.moveVertexBy(index, shiftPos)
if is_ctrl_pressed and len(shape.points) > 1:
# Calculate symmetric opposite index for simple shapes
if len(shape.points) == 4:
opp_index = (index + 2) % 4
shape.moveVertexBy(opp_index, -shiftPos)
elif len(shape.points) == 2:
opp_index = (index + 1) % 2
shape.moveVertexBy(opp_index, -shiftPos)

def boundedMoveShape(self, shapes, pos):
if type(shapes).__name__ != "list":
Expand Down
Loading
Loading