Skip to content

Commit e67b833

Browse files
committed
Added time cost
1 parent 1dead76 commit e67b833

1 file changed

Lines changed: 158 additions & 10 deletions

File tree

  • examples/official/camera_file

examples/official/camera_file/gui.py

Lines changed: 158 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import os
1111
import sys
12+
import time
1213
import traceback
1314
from dataclasses import dataclass, field
1415
from pathlib import Path
@@ -41,6 +42,7 @@
4142
QGraphicsScene,
4243
QGraphicsSimpleTextItem,
4344
QGraphicsView,
45+
QHeaderView,
4446
QHBoxLayout,
4547
QLabel,
4648
QListWidget,
@@ -127,6 +129,14 @@ class PageData:
127129
detect_height: int = 0
128130
barcodes: List[BarcodeHit] = field(default_factory=list)
129131
error: Optional[str] = None
132+
decode_elapsed_ms: Optional[int] = None
133+
134+
135+
@dataclass
136+
class FileScanMetrics:
137+
barcode_count: int = 0
138+
decode_elapsed_ms: Optional[int] = None
139+
used_layout_analysis: bool = False
130140

131141

132142
@dataclass
@@ -403,6 +413,7 @@ def render_detection_mats(file_path: str, page_records: List[PageData]) -> List[
403413
class ScannerSignals(QObject):
404414
fileStarted = Signal(str, int)
405415
pageReady = Signal(object)
416+
fileMetricsReady = Signal(str, int, int, bool)
406417
fileFinished = Signal(str)
407418
allFinished = Signal()
408419
error = Signal(str, str)
@@ -512,17 +523,22 @@ def _scan_with_layout_analysis(
512523
continue
513524

514525
detection_image = detection_images[page_index]
526+
page_start = time.perf_counter()
515527
try:
516528
hits, error = decode_with_layout_analysis(detection_image)
517529
except Exception as exc:
518530
page_record.error = f"Layout analysis failed: {exc}"
531+
page_record.decode_elapsed_ms = int(
532+
(time.perf_counter() - page_start) * 1000
533+
)
519534
self.signals.pageReady.emit(page_record)
520535
continue
521536

522537
page_record.detect_width = detection_image.shape[1]
523538
page_record.detect_height = detection_image.shape[0]
524539
page_record.barcodes = hits
525540
page_record.error = error
541+
page_record.decode_elapsed_ms = int((time.perf_counter() - page_start) * 1000)
526542
self.signals.pageReady.emit(page_record)
527543

528544
def run(self) -> None:
@@ -550,10 +566,17 @@ def run(self) -> None:
550566
break
551567

552568
if self.use_layout_analysis:
569+
file_start = time.perf_counter()
553570
self._scan_with_layout_analysis(file_path, page_records)
571+
file_elapsed_ms = int((time.perf_counter() - file_start) * 1000)
572+
total_barcodes = sum(len(page_record.barcodes) for page_record in page_records)
573+
self.signals.fileMetricsReady.emit(
574+
file_path, total_barcodes, file_elapsed_ms, True
575+
)
554576
self.signals.fileFinished.emit(file_path)
555577
continue
556578

579+
file_start = time.perf_counter()
557580
try:
558581
if cvr is None:
559582
raise RuntimeError("CaptureVisionRouter was not initialized.")
@@ -568,6 +591,8 @@ def run(self) -> None:
568591

569592
results = result_array.get_results() if result_array else None
570593
if not results:
594+
file_elapsed_ms = int((time.perf_counter() - file_start) * 1000)
595+
self.signals.fileMetricsReady.emit(file_path, 0, file_elapsed_ms, False)
571596
self.signals.fileFinished.emit(file_path)
572597
continue
573598

@@ -595,6 +620,14 @@ def run(self) -> None:
595620
page_records[page_idx].barcodes = hits
596621
self.signals.pageReady.emit(page_records[page_idx])
597622

623+
file_elapsed_ms = int((time.perf_counter() - file_start) * 1000)
624+
total_barcodes = sum(len(page_record.barcodes) for page_record in page_records)
625+
if total_pages == 1 and page_records:
626+
page_records[0].decode_elapsed_ms = file_elapsed_ms
627+
self.signals.pageReady.emit(page_records[0])
628+
self.signals.fileMetricsReady.emit(
629+
file_path, total_barcodes, file_elapsed_ms, False
630+
)
598631
self.signals.fileFinished.emit(file_path)
599632

600633
self.signals.allFinished.emit()
@@ -709,7 +742,11 @@ def set_page(self, page: PageData) -> None:
709742
def fit_to_window(self) -> None:
710743
if self._pixmap_item is None:
711744
return
712-
self.fitInView(self._pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
745+
# Reset any accumulated pan/zoom before fitting the current page again.
746+
self.resetTransform()
747+
target_rect = self._pixmap_item.sceneBoundingRect()
748+
self.centerOn(self._pixmap_item)
749+
self.fitInView(target_rect, Qt.AspectRatioMode.KeepAspectRatio)
713750
self._zoom = 0
714751

715752
def wheelEvent(self, event) -> None:
@@ -785,6 +822,7 @@ def __init__(self) -> None:
785822

786823
# data
787824
self._pages: dict[Tuple[str, int], PageData] = {}
825+
self._file_metrics: dict[str, FileScanMetrics] = {}
788826
self._file_items: dict[str, QTreeWidgetItem] = {}
789827
self._scanner: Optional[ScannerThread] = None
790828
self._license_ok = False
@@ -837,7 +875,7 @@ def _build_ui(self) -> None:
837875
"Fit to Window",
838876
self,
839877
)
840-
self._fit_act.triggered.connect(lambda: self.viewer.fit_to_window())
878+
self._fit_act.triggered.connect(self._on_fit_to_window)
841879
toolbar.addAction(self._fit_act)
842880

843881
toolbar.addSeparator()
@@ -876,12 +914,17 @@ def _build_ui(self) -> None:
876914

877915
# tree (file list)
878916
self.tree = QTreeWidget()
879-
self.tree.setHeaderLabels(["File / Page", "Barcodes"])
880-
self.tree.setColumnWidth(0, 220)
881-
self.tree.setMinimumWidth(260)
917+
self.tree.setHeaderLabels(["File / Page", "Barcodes", "Time (ms)"])
918+
self.tree.setMinimumWidth(340)
919+
tree_header = self.tree.header()
920+
tree_header.setStretchLastSection(False)
921+
tree_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
922+
tree_header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
923+
tree_header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
882924
self.tree.currentItemChanged.connect(self._on_tree_changed)
883925

884926
tree_panel = QWidget()
927+
tree_panel.setMinimumWidth(340)
885928
tree_layout = QVBoxLayout(tree_panel)
886929
tree_layout.setContentsMargins(6, 6, 6, 6)
887930
tree_layout.setSpacing(4)
@@ -938,7 +981,7 @@ def _build_ui(self) -> None:
938981
splitter.setStretchFactor(0, 0)
939982
splitter.setStretchFactor(1, 1)
940983
splitter.setStretchFactor(2, 0)
941-
splitter.setSizes([280, 720, 280])
984+
splitter.setSizes([360, 640, 280])
942985
self.setCentralWidget(splitter)
943986

944987
# status bar
@@ -1028,6 +1071,11 @@ def _on_template_changed(self, index: int) -> None:
10281071
f"Re-decoding {os.path.basename(file_path)} with {mode_label}..."
10291072
)
10301073

1074+
def _on_fit_to_window(self) -> None:
1075+
self.viewer.fit_to_window()
1076+
if self.viewer._pixmap_item is not None:
1077+
self._status_label.setText("Fitted current page to window.")
1078+
10311079
def _on_layout_analysis_toggled(self, checked: bool) -> None:
10321080
self._layout_analysis_enabled = checked
10331081
file_path = self._current_file_path()
@@ -1045,6 +1093,7 @@ def _connect_scanner(self) -> None:
10451093
return
10461094
self._scanner.signals.fileStarted.connect(self._on_file_started)
10471095
self._scanner.signals.pageReady.connect(self._on_page_ready)
1096+
self._scanner.signals.fileMetricsReady.connect(self._on_file_metrics_ready)
10481097
self._scanner.signals.fileFinished.connect(self._on_file_finished)
10491098
self._scanner.signals.allFinished.connect(self._on_all_finished)
10501099
self._scanner.signals.error.connect(self._on_scan_error)
@@ -1091,6 +1140,7 @@ def _on_clear(self) -> None:
10911140
self.results.clear()
10921141
self.viewer.clear_view()
10931142
self._pages.clear()
1143+
self._file_metrics.clear()
10941144
self._file_items.clear()
10951145
self._barcode_total = 0
10961146
self._auto_select_target = None
@@ -1163,14 +1213,38 @@ def _expand_paths(paths: List[str]) -> List[str]:
11631213
# ----- scanner signals -------------------------------------------------
11641214

11651215
def _on_file_started(self, file_path: str, total_pages: int) -> None:
1166-
item = QTreeWidgetItem([os.path.basename(file_path), "..."])
1216+
item = QTreeWidgetItem([os.path.basename(file_path), "...", "..."])
11671217
item.setToolTip(0, file_path)
11681218
item.setData(0, self.FILE_ROLE, file_path)
11691219
self.tree.addTopLevelItem(item)
11701220
self._file_items[file_path] = item
11711221
if total_pages > 1:
11721222
item.setExpanded(True)
11731223

1224+
def _on_file_metrics_ready(
1225+
self,
1226+
file_path: str,
1227+
barcode_count: int,
1228+
decode_elapsed_ms: int,
1229+
used_layout_analysis: bool,
1230+
) -> None:
1231+
self._file_metrics[file_path] = FileScanMetrics(
1232+
barcode_count=barcode_count,
1233+
decode_elapsed_ms=decode_elapsed_ms,
1234+
used_layout_analysis=used_layout_analysis,
1235+
)
1236+
1237+
item = self._file_items.get(file_path)
1238+
if item is not None:
1239+
item.setText(1, str(barcode_count))
1240+
item.setText(2, str(decode_elapsed_ms))
1241+
1242+
current = self.tree.currentItem()
1243+
if current is not None:
1244+
page = self._page_from_item(current)
1245+
if page is not None and page.file_path == file_path:
1246+
self._show_page(page)
1247+
11741248
def _on_page_ready(self, page: PageData) -> None:
11751249
key = (page.file_path, page.page_index)
11761250
self._pages[key] = page
@@ -1190,19 +1264,26 @@ def _on_page_ready(self, page: PageData) -> None:
11901264
break
11911265

11921266
n_bc = len(page.barcodes)
1267+
decode_text = (
1268+
str(page.decode_elapsed_ms) if page.decode_elapsed_ms is not None else ""
1269+
)
11931270
if page.total_pages == 1:
11941271
parent.setText(1, str(n_bc))
1272+
parent.setText(2, decode_text)
11951273
parent.setData(0, self.PAGE_ROLE, 0)
11961274
target_item = parent
11971275
else:
11981276
if existing_child is None:
1199-
child = QTreeWidgetItem([f"Page {page.page_index + 1}", str(n_bc)])
1277+
child = QTreeWidgetItem(
1278+
[f"Page {page.page_index + 1}", str(n_bc), decode_text]
1279+
)
12001280
child.setData(0, self.PAGE_ROLE, page.page_index)
12011281
child.setData(0, self.FILE_ROLE, page.file_path)
12021282
parent.addChild(child)
12031283
target_item = child
12041284
else:
12051285
existing_child.setText(1, str(n_bc))
1286+
existing_child.setText(2, decode_text)
12061287
target_item = existing_child
12071288
# roll up total count
12081289
total = sum(
@@ -1211,6 +1292,10 @@ def _on_page_ready(self, page: PageData) -> None:
12111292
)
12121293
parent.setText(1, str(total))
12131294

1295+
metrics = self._file_metrics.get(page.file_path)
1296+
if metrics is not None and metrics.decode_elapsed_ms is not None:
1297+
parent.setText(2, str(metrics.decode_elapsed_ms))
1298+
12141299
# Auto-select the first page of the most recently dropped batch as
12151300
# soon as its placeholder render appears, so the user sees the file
12161301
# immediately. Falls back to picking up the first page that arrives
@@ -1239,9 +1324,12 @@ def _on_all_finished(self) -> None:
12391324
self._progress.setVisible(False)
12401325
total_pages = len(self._pages)
12411326
total_bc = sum(len(p.barcodes) for p in self._pages.values())
1327+
total_elapsed_ms = sum(
1328+
metrics.decode_elapsed_ms or 0 for metrics in self._file_metrics.values()
1329+
)
12421330
self._barcode_total = total_bc
12431331
self._status_label.setText(
1244-
f"Done. {total_pages} page(s), {total_bc} barcode(s) detected."
1332+
f"Done. {total_pages} page(s), {total_bc} barcode(s), {total_elapsed_ms} ms total."
12451333
)
12461334

12471335
def _on_scan_error(self, file_path: str, message: str) -> None:
@@ -1259,6 +1347,22 @@ def _on_tree_changed(self, current: Optional[QTreeWidgetItem], _prev) -> None:
12591347
self._page_label.setText("No page selected")
12601348
self._update_nav_state()
12611349
return
1350+
1351+
file_path = current.data(0, self.FILE_ROLE)
1352+
if file_path:
1353+
metrics = self._file_metrics.get(file_path)
1354+
if metrics is not None and (
1355+
metrics.used_layout_analysis != self._layout_analysis_enabled
1356+
):
1357+
mode_label = (
1358+
"layout analysis" if self._layout_analysis_enabled else "standard decoding"
1359+
)
1360+
self._restart_selected_file_decode(
1361+
file_path,
1362+
f"Re-decoding {os.path.basename(file_path)} with {mode_label}...",
1363+
)
1364+
return
1365+
12621366
page = self._page_from_item(current)
12631367
if page is not None:
12641368
self._show_page(page)
@@ -1268,6 +1372,32 @@ def _on_tree_changed(self, current: Optional[QTreeWidgetItem], _prev) -> None:
12681372
self._page_label.setText(os.path.basename(current.data(0, self.FILE_ROLE) or ""))
12691373
self._update_nav_state()
12701374

1375+
def _restart_selected_file_decode(self, file_path: str, status_text: str) -> None:
1376+
if not file_path:
1377+
self._status_label.setText(status_text)
1378+
return
1379+
1380+
cached = {
1381+
key: page for key, page in self._pages.items() if key[0] == file_path
1382+
}
1383+
if self._scanner and self._scanner.isRunning():
1384+
self._scanner.stop()
1385+
self._scanner.wait(2000)
1386+
1387+
self._auto_select_target = None
1388+
self._progress.setVisible(True)
1389+
self._status_label.setText(status_text)
1390+
1391+
self._scanner = ScannerThread(
1392+
[file_path],
1393+
self._current_template,
1394+
cached_pages=cached,
1395+
use_layout_analysis=self._layout_analysis_enabled,
1396+
parent=self,
1397+
)
1398+
self._connect_scanner()
1399+
self._scanner.start()
1400+
12711401
def _page_from_item(self, item: QTreeWidgetItem) -> Optional[PageData]:
12721402
page_idx = item.data(0, self.PAGE_ROLE)
12731403
file_path = item.data(0, self.FILE_ROLE)
@@ -1278,6 +1408,21 @@ def _page_from_item(self, item: QTreeWidgetItem) -> Optional[PageData]:
12781408
def _show_page(self, page: PageData) -> None:
12791409
self.viewer.set_page(page)
12801410
self.results.clear()
1411+
1412+
metrics = self._file_metrics.get(page.file_path)
1413+
decode_elapsed_ms = page.decode_elapsed_ms
1414+
decode_scope = "Decode"
1415+
if decode_elapsed_ms is None and metrics is not None:
1416+
decode_elapsed_ms = metrics.decode_elapsed_ms
1417+
decode_scope = "File decode"
1418+
1419+
if decode_elapsed_ms is not None:
1420+
info_item = QListWidgetItem(
1421+
f"[info] Count: {len(page.barcodes)} barcode(s) | {decode_scope} time: {decode_elapsed_ms} ms"
1422+
)
1423+
info_item.setForeground(QBrush(QColor("#1c7ed6")))
1424+
self.results.addItem(info_item)
1425+
12811426
if page.error:
12821427
err_item = QListWidgetItem(f"[error] {page.error}")
12831428
err_item.setForeground(QBrush(QColor("#c92a2a")))
@@ -1290,11 +1435,14 @@ def _show_page(self, page: PageData) -> None:
12901435
item = QListWidgetItem(text)
12911436
item.setData(Qt.ItemDataRole.UserRole, hit.text)
12921437
self.results.addItem(item)
1293-
self._page_label.setText(
1438+
page_text = (
12941439
f"{os.path.basename(page.file_path)} "
12951440
f"Page {page.page_index + 1} / {page.total_pages} "
12961441
f"{len(page.barcodes)} barcode(s)"
12971442
)
1443+
if decode_elapsed_ms is not None:
1444+
page_text += f" {decode_scope}: {decode_elapsed_ms} ms"
1445+
self._page_label.setText(page_text)
12981446
self._update_nav_state()
12991447

13001448
def _flat_page_items(self) -> List[QTreeWidgetItem]:

0 commit comments

Comments
 (0)