Skip to content
Open
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
108 changes: 102 additions & 6 deletions PPOCRLabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import signal
import subprocess
import sys
import torch # noqa: F401 — keeps torch DLLs loaded for PaddlePaddle on Windows
from functools import partial
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence

import openpyxl
import cv2
Expand Down Expand Up @@ -398,6 +401,7 @@ def get_str(str_id):
labelListContainer.setLayout(listLayout)
self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)
self.labelList.clicked.connect(self.labelList.item_clicked)
self.labelList.navigated.connect(self.focusAndZoom)

# Connect to itemChanged to detect checkbox changes.
self.labelList.itemChanged.connect(self.labelItemChanged)
Expand Down Expand Up @@ -482,6 +486,35 @@ def get_str(str_id):
self.imageSliderDock.setAttribute(Qt.WA_TranslucentBackground)
self.addDockWidget(Qt.RightDockWidgetArea, self.imageSliderDock)

# ================== Confidence Filter ==================
self.confidenceSlider = QSlider(Qt.Horizontal)
self.confidenceSlider.setMinimum(0)
self.confidenceSlider.setMaximum(100)
self.confidenceSlider.setValue(100)
self.confidenceSlider.setSingleStep(1)
self.confidenceSlider.setToolTip(
"Show only boxes with confidence BELOW this threshold"
)
self.confidenceSlider.valueChanged.connect(self.onConfidenceFilterChanged)

self.confidenceLabel = QLabel("Threshold: 1.00 (show all)")
self.confidenceLabel.setAlignment(Qt.AlignCenter)

confWidget = QWidget()
confLayout = QVBoxLayout()
confLayout.setContentsMargins(4, 4, 4, 4)
confLayout.addWidget(self.confidenceLabel)
confLayout.addWidget(self.confidenceSlider)
confWidget.setLayout(confLayout)

self.confidenceFilterDock = QDockWidget("Confidence Filter", self)
self.confidenceFilterDock.setObjectName("ConfidenceFilter")
self.confidenceFilterDock.setWidget(confWidget)
self.confidenceFilterDock.setFeatures(
QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetClosable
)
self.addDockWidget(Qt.RightDockWidgetArea, self.confidenceFilterDock)

self.zoomWidget = ZoomWidget()
self.colorDialog = ColorDialog(parent=self)
self.zoomWidgetValue = self.zoomWidget.value()
Expand Down Expand Up @@ -1179,6 +1212,14 @@ def get_str(str_id):
self.displayIndexOption.setChecked(settings.get(SETTING_PAINT_INDEX, False))
self.autoSaveUnsavedChangesOption.triggered.connect(self.autoSaveFunc)

QShortcut(
QKeySequence(Qt.Key_F2),
self,
activated=lambda: (
self.labelList.activate_edit() if self.currentItem() else None
),
)

addActions(
self.menus.file,
(
Expand Down Expand Up @@ -1331,6 +1372,40 @@ def keyPressEvent(self, event):
if event.key() == Qt.Key_Control:
# Draw rectangle if Ctrl is pressed
self.canvas.setDrawingShapeToSquare(True)
elif event.key() in (Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter):
if self.currentItem() is not None:
self.labelList.activate_edit()
event.accept()

def _navigateLabel(self, direction):
count = self.labelList.count()
if count == 0:
return
current = self.labelList.currentRow()
if current < 0:
next_row = 0 if direction > 0 else count - 1
else:
next_row = (current + direction) % count
self.labelList.setCurrentRow(next_row)
self.labelList.scrollToItem(self.labelList.item(next_row))
self.focusAndZoom()

def onConfidenceFilterChanged(self, value):
threshold = value / 100.0
if value == 100:
self.confidenceLabel.setText("Threshold: 1.00 (show all)")
else:
self.confidenceLabel.setText(f"Threshold: {threshold:.2f}")
self.applyConfidenceFilter()

def applyConfidenceFilter(self):
threshold = self.confidenceSlider.value() / 100.0
for shape in self.canvas.shapes:
if shape.score is None:
self.canvas.setShapeVisible(shape, True)
else:
self.canvas.setShapeVisible(shape, shape.score < threshold)
self.canvas.update()

def noShapes(self):
return not self.itemsToShapes
Expand Down Expand Up @@ -1896,7 +1971,9 @@ def remLabels(self, shapes):
def loadLabels(self, shapes):
s = []
shape_index = 0
for label, points, line_color, key_cls in shapes:
for item in shapes:
label, points, line_color, key_cls = item[:4]
score = item[4] if len(item) > 4 else None
shape = Shape(
label=label,
line_color=line_color,
Expand All @@ -1912,7 +1989,7 @@ def loadLabels(self, shapes):
shape.addPoint(QPointF(x, y))
shape.idx = shape_index
shape_index += 1
# shape.locked = False
shape.score = score
shape.close()
s.append(shape)

Expand Down Expand Up @@ -1961,13 +2038,16 @@ def saveLabels(self, annotationFilePath, mode="Auto"):

def format_shape(s):
# print('s in saveLabels is ',s)
return dict(
d = dict(
label=s.label, # str
line_color=s.line_color.getRgb(),
fill_color=s.fill_color.getRgb(),
points=[(int(p.x()), int(p.y())) for p in s.points], # QPonitF
key_cls=s.key_cls,
) # bool
)
if s.score is not None:
d["score"] = round(float(s.score), 4)
return d

if mode == "Auto":
shapes = []
Expand All @@ -1979,7 +2059,7 @@ def format_shape(s):
]
# Can add different annotation formats here
for box in self.result_dic:
trans_dic = {"label": box[1][0], "points": box[0]}
trans_dic = {"label": box[1][0], "points": box[0], "score": box[1][1]}
if self.kie_mode:
if len(box) == 3:
trans_dic.update({"key_cls": box[2]})
Expand All @@ -1997,6 +2077,8 @@ def format_shape(s):
"points": box["points"],
"difficult": False,
}
if box.get("score") is not None:
trans_dict["score"] = round(float(box["score"]), 4)
if self.kie_mode:
trans_dict.update({"key_cls": box["key_cls"]})
trans_dic.append(trans_dict)
Expand Down Expand Up @@ -2032,6 +2114,11 @@ def labelSelectionChanged(self):
selected_shapes.append(self.itemsToShapes[item])
if selected_shapes:
self.canvas.selectShapes(selected_shapes)
score = selected_shapes[0].score
if score is not None:
self.confidenceLabel.setText(
f"Threshold: {self.confidenceSlider.value() / 100:.2f} | Selected: {score:.2f}"
)
else:
self.canvas.deSelectShape()

Expand Down Expand Up @@ -2420,6 +2507,7 @@ def showBoundingBoxFromPPlabel(self, filePath):
[[s[0] * width, s[1] * height] for s in box["ratio"]],
DEFAULT_LOCK_COLOR,
key_cls,
None,
)
)
else:
Expand All @@ -2429,6 +2517,7 @@ def showBoundingBoxFromPPlabel(self, filePath):
[[s[0] * width, s[1] * height] for s in box["ratio"]],
DEFAULT_LOCK_COLOR,
key_cls,
None,
)
)
if img_idx in self.PPlabel.keys():
Expand All @@ -2440,12 +2529,14 @@ def showBoundingBoxFromPPlabel(self, filePath):
box["points"],
None,
key_cls,
box.get("score"),
)
)

if shapes:
self.loadLabels(shapes)
self.canvas.verified = False
self.applyConfidenceFilter()

def validFilestate(self, filePath):
if filePath in self.fileStatedict.keys() and self.fileStatedict[filePath] == 1:
Expand Down Expand Up @@ -2588,7 +2679,9 @@ def openDatasetDirDialog(self):

else:
if self.lang == "ch":
self.msgBox.warning(self, "提示", "\n 原文件夹已不存在,请从新选择数据集路径!")
self.msgBox.warning(
self, "提示", "\n 原文件夹已不存在,请从新选择数据集路径!"
)
else:
self.msgBox.warning(
self,
Expand Down Expand Up @@ -3248,6 +3341,9 @@ def autoRecognitionCurrent(self):
self.loadFile(self.filePath, isAdjustScale=False)
self.canvas.isInTheSameImage = False
self.setDirty()
for shape, box in zip(self.canvas.shapes, self.result_dic):
shape.score = box[1][1]
self.applyConfidenceFilter()

def singleRerecognition(self):
img = cv2.imdecode(np.fromfile(self.filePath, dtype=np.uint8), cv2.IMREAD_COLOR)
Expand Down
107 changes: 101 additions & 6 deletions libs/editinlist.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QModelIndex
from PyQt5.QtWidgets import QListWidget
from PyQt5.QtCore import QEvent, QModelIndex, pyqtSignal
from PyQt5.QtWidgets import QApplication, QLineEdit, QListWidget


class EditInList(QListWidget):
navigated = pyqtSignal() # emitted after Tab/Shift+Tab navigation

def __init__(self):
super(EditInList, self).__init__()
self.edited_item = None
self._app_filter_active = False

def item_clicked(self, modelindex: QModelIndex):
try:
Expand All @@ -19,15 +22,107 @@ def item_clicked(self, modelindex: QModelIndex):
self.edited_item = self.item(modelindex.row())
self.openPersistentEditor(self.edited_item)
self.editItem(self.edited_item)
self._install_app_filter()

def mouseDoubleClickEvent(self, event):
pass

def leaveEvent(self, event):
pass

def activate_edit(self):
"""Open persistent editor for the currently selected item (called by F2/Tab)."""
item = self.currentItem()
if item is None:
return
if self.edited_item is not None:
try:
self.closePersistentEditor(self.edited_item)
except Exception:
pass
self.edited_item = item
self.openPersistentEditor(item)
self.editItem(item)
self._install_app_filter()

def _install_app_filter(self):
if not self._app_filter_active:
QApplication.instance().installEventFilter(self)
self._app_filter_active = True

def _remove_app_filter(self):
if self._app_filter_active:
QApplication.instance().removeEventFilter(self)
self._app_filter_active = False

def _save_and_close(self):
"""Write editor text back to the item, then close all editors."""
if self.edited_item is not None:
# Use the currently focused widget — guaranteed to be what user typed in
focused = QApplication.focusWidget()
if isinstance(focused, QLineEdit):
self.edited_item.setText(focused.text())
else:
# Fallback: search visible QLineEdit children
for child in self.findChildren(QLineEdit):
if child.isVisible():
self.edited_item.setText(child.text())
break
self._remove_app_filter()
for i in range(self.count()):
self.closePersistentEditor(self.item(i))
self.edited_item = None

def eventFilter(self, obj, event):
if event.type() == QEvent.KeyPress:
focused = QApplication.focusWidget()
# Only act when the focused widget is inside this list
if focused is not None and self.isAncestorOf(focused):
key = event.key()
if key == 16777217: # Tab: save, move to next, open editor
self._save_and_close()
next_row = self.currentRow() + 1
if next_row < self.count():
self.setCurrentRow(next_row)
self.scrollToItem(self.item(next_row))
self.activate_edit()
self.navigated.emit()
return True
if key == 16777218: # Shift+Tab: save, move to previous, open editor
self._save_and_close()
prev_row = self.currentRow() - 1
if prev_row >= 0:
self.setCurrentRow(prev_row)
self.scrollToItem(self.item(prev_row))
self.activate_edit()
self.navigated.emit()
return True
return False

def keyPressEvent(self, event) -> None:
# close edit
if event.key() in [16777220, 16777221]:
for i in range(self.count()):
self.closePersistentEditor(self.item(i))
key = event.key()
if key in (16777220, 16777221): # Enter / Return: save and move to next
self._save_and_close()
next_row = self.currentRow() + 1
if next_row < self.count():
self.setCurrentRow(next_row)
self.scrollToItem(self.item(next_row))
event.accept()
return
if key == 16777217: # Tab with no editor open: just navigate
next_row = self.currentRow() + 1
if next_row < self.count():
self.setCurrentRow(next_row)
self.scrollToItem(self.item(next_row))
self.navigated.emit()
event.accept()
return
if key == 16777218: # Shift+Tab with no editor open: navigate back
prev_row = self.currentRow() - 1
if prev_row >= 0:
self.setCurrentRow(prev_row)
self.scrollToItem(self.item(prev_row))
self.navigated.emit()
event.accept()
return
super().keyPressEvent(event)
1 change: 1 addition & 0 deletions libs/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(
self.MOVE_VERTEX: (1.5, self.P_SQUARE),
}
self.fontsize = 8
self.score = None # OCR confidence score (0.0-1.0), None if unknown

self._closed = False
self.font_family = font_family
Expand Down