Skip to content

Commit d7e3439

Browse files
authored
Merge pull request #203 from MF-Dust/master
perf:提升启动速度并收敛历史页与延迟加载链路
2 parents 613a003 + b859750 commit d7e3439

16 files changed

Lines changed: 769 additions & 1167 deletions

app/common/camera_preview_backend/detection.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,27 +569,84 @@ def create_onnx_face_detector(
569569

570570

571571
def detect_faces_onnx(frame_bgr, *, detector_state) -> list[Rect]:
572+
def _prepare_detection_frame(src, *, max_side: int = 640):
573+
h0, w0 = src.shape[:2]
574+
if h0 <= 0 or w0 <= 0:
575+
return src, 1.0, 1.0
576+
side = max(int(w0), int(h0))
577+
if side <= int(max_side):
578+
return src, 1.0, 1.0
579+
580+
ratio = float(max_side) / float(side)
581+
w1 = max(1, int(round(float(w0) * ratio)))
582+
h1 = max(1, int(round(float(h0) * ratio)))
583+
584+
with _CV2_IMPORT_LOCK:
585+
import cv2
586+
587+
resized = cv2.resize(src, (w1, h1), interpolation=cv2.INTER_LINEAR)
588+
scale_x = float(w0) / float(w1)
589+
scale_y = float(h0) / float(h1)
590+
return resized, scale_x, scale_y
591+
592+
def _rescale_rects(
593+
rects: list[Rect],
594+
*,
595+
scale_x: float,
596+
scale_y: float,
597+
frame_size: tuple[int, int],
598+
) -> list[Rect]:
599+
h, w = int(frame_size[0]), int(frame_size[1])
600+
if not rects:
601+
return []
602+
if abs(float(scale_x) - 1.0) < 1e-6 and abs(float(scale_y) - 1.0) < 1e-6:
603+
return list(rects)
604+
scaled: list[Rect] = []
605+
for x, y, bw, bh in rects:
606+
try:
607+
x1 = int(round(float(x) * float(scale_x)))
608+
y1 = int(round(float(y) * float(scale_y)))
609+
w1 = int(round(float(bw) * float(scale_x)))
610+
h1 = int(round(float(bh) * float(scale_y)))
611+
except Exception:
612+
continue
613+
if w1 <= 0 or h1 <= 0:
614+
continue
615+
x1 = max(0, min(x1, w - 1))
616+
y1 = max(0, min(y1, h - 1))
617+
w1 = max(1, min(w1, w - x1))
618+
h1 = max(1, min(h1, h - y1))
619+
scaled.append((x1, y1, w1, h1))
620+
return scaled
621+
572622
kind = ""
573623
try:
574624
kind = str(detector_state.get("kind", "")).lower()
575625
except Exception:
576626
kind = ""
577627

578628
frame_size = frame_bgr.shape[:2]
629+
detect_frame, scale_x, scale_y = _prepare_detection_frame(frame_bgr, max_side=640)
579630
if kind == "yunet":
580631
rects = detect_faces_yunet(
581-
frame_bgr,
632+
detect_frame,
582633
detector=detector_state["detector"],
583634
input_size=detector_state.get("input_size"),
584635
)
636+
rects = _rescale_rects(
637+
rects, scale_x=scale_x, scale_y=scale_y, frame_size=frame_size
638+
)
585639
return merge_face_rects(frame_size, rects)
586640
if kind == "ultralight":
587641
rects = detect_faces_ultralight(
588-
frame_bgr,
642+
detect_frame,
589643
net=detector_state["net"],
590644
input_size=detector_state["input_size"],
591645
priors=detector_state.get("priors"),
592646
)
647+
rects = _rescale_rects(
648+
rects, scale_x=scale_x, scale_y=scale_y, frame_size=frame_size
649+
)
593650
return merge_face_rects(frame_size, rects)
594651
raise RuntimeError("Invalid detector state")
595652

app/common/camera_preview_backend/workers.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,13 +466,31 @@ def __init__(self, parent: Optional[QObject] = None) -> None:
466466
self._input_size = None
467467
self._last_detect = 0.0
468468
self._last_load_attempt = 0.0
469+
self._pending_frame = None
470+
self._detect_timer: Optional[QTimer] = None
471+
self._detect_scheduled = False
472+
self._detect_inflight = False
473+
self._target_interval_s = 0.08
474+
self._min_interval_s = 0.08
475+
self._max_interval_s = 0.14
476+
self._detect_interval_s = self._target_interval_s
477+
self._recent_detect_cost_s = self._target_interval_s
469478

470479
self._detector_state = None
471480
self._cv2 = None
472481

473482
@Slot(bool)
474483
def set_enabled(self, enabled: bool) -> None:
475484
self._enabled = bool(enabled)
485+
if not self._enabled:
486+
self._pending_frame = None
487+
self._detect_scheduled = False
488+
timer = self._detect_timer
489+
if timer is not None and timer.isActive():
490+
timer.stop()
491+
return
492+
if self._pending_frame is not None:
493+
self._schedule_detect(0)
476494

477495
@Slot(object)
478496
def set_model_filename(self, model_filename: object) -> None:
@@ -560,23 +578,72 @@ def ensure_loaded(self) -> None:
560578

561579
@Slot(object)
562580
def process_frame(self, frame_bgr) -> None:
581+
if frame_bgr is None:
582+
return
583+
self._pending_frame = frame_bgr
584+
if self._enabled:
585+
self._schedule_detect(0)
586+
587+
def _ensure_detect_timer(self) -> QTimer:
588+
timer = self._detect_timer
589+
if timer is not None:
590+
return timer
591+
timer = QTimer(self)
592+
timer.setSingleShot(True)
593+
timer.timeout.connect(self._consume_pending_frame)
594+
self._detect_timer = timer
595+
return timer
596+
597+
def _schedule_detect(self, delay_ms: int) -> None:
563598
if not self._enabled:
564599
return
565-
if self._cv2 is None:
600+
timer = self._ensure_detect_timer()
601+
delay = max(0, int(delay_ms))
602+
if timer.isActive():
603+
try:
604+
remaining = int(timer.remainingTime())
605+
except Exception:
606+
remaining = delay
607+
if remaining <= delay:
608+
return
609+
timer.stop()
610+
self._detect_scheduled = True
611+
timer.start(delay)
612+
613+
@Slot()
614+
def _consume_pending_frame(self) -> None:
615+
self._detect_scheduled = False
616+
if not self._enabled or self._detect_inflight:
617+
return
618+
619+
frame = self._pending_frame
620+
if frame is None:
566621
return
567622

568623
now = time.monotonic()
569-
if now - self._last_detect < 0.05:
624+
elapsed = now - self._last_detect
625+
if self._last_detect > 0.0 and elapsed < self._detect_interval_s:
626+
wait_ms = int(max(1.0, (self._detect_interval_s - elapsed) * 1000.0))
627+
self._schedule_detect(wait_ms)
628+
return
629+
630+
self._pending_frame = None
631+
self._detect_inflight = True
632+
started_at = time.perf_counter()
633+
emitted_error = False
634+
if not self._enabled:
635+
self._detect_inflight = False
570636
return
571-
self._last_detect = now
572637

573638
self.ensure_loaded()
574639

575640
try:
641+
if self._cv2 is None:
642+
return
576643
state = self._detector_state
577644
if state is None:
578645
return
579-
results = detect_faces_onnx(frame_bgr, detector_state=state)
646+
results = detect_faces_onnx(frame, detector_state=state)
580647
except Exception as exc:
581648
logger.exception("人脸检测失败: {}", exc)
582649
key = "detect_failed"
@@ -587,6 +654,28 @@ def process_frame(self, frame_bgr) -> None:
587654
):
588655
key = "model_incompatible"
589656
self.error_occurred.emit(key, "Face detection failed", msg)
657+
emitted_error = True
590658
return
659+
finally:
660+
cost_s = max(0.0, time.perf_counter() - started_at)
661+
self._recent_detect_cost_s = self._recent_detect_cost_s * 0.7 + cost_s * 0.3
662+
if self._recent_detect_cost_s > self._detect_interval_s * 1.05:
663+
desired = min(
664+
self._max_interval_s,
665+
max(0.10, self._recent_detect_cost_s * 1.2),
666+
)
667+
else:
668+
desired = max(
669+
self._target_interval_s,
670+
self._detect_interval_s * 0.92,
671+
)
672+
self._detect_interval_s = min(
673+
self._max_interval_s,
674+
max(self._min_interval_s, desired),
675+
)
676+
self._last_detect = time.monotonic()
677+
self._detect_inflight = False
678+
if self._pending_frame is not None and not emitted_error:
679+
self._schedule_detect(0)
591680

592681
self.faces_ready.emit(results)

app/common/history/background_loader.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from app.Language.obtain_language import get_content_name_async
55
from app.common.history.history_reader import (
6-
check_class_has_gender_or_group,
6+
check_roll_call_students_have_gender_or_group,
77
filter_roll_call_history_by_subject,
88
get_lottery_history_data,
99
get_lottery_pool_list,
@@ -79,7 +79,9 @@ def build_roll_call_history_payload(
7979
all_names = [name for _, name, _, _ in cleaned_students if name]
8080
base_history_data = get_roll_call_history_data(class_name)
8181
available_subjects = _collect_roll_call_subjects(base_history_data)
82-
has_gender, has_group = check_class_has_gender_or_group(class_name)
82+
has_gender, has_group = check_roll_call_students_have_gender_or_group(
83+
cleaned_students
84+
)
8385
reverse_order = bool(sort_order_desc)
8486

8587
if mode_index == 0:
@@ -91,7 +93,12 @@ def build_roll_call_history_payload(
9193
students_data = get_roll_call_students_data(
9294
cleaned_students, history_data, subject_name
9395
)
94-
students_weight_data = calculate_weight(students_data, class_name, subject_name)
96+
students_weight_data = calculate_weight(
97+
students_data,
98+
class_name,
99+
subject_name,
100+
history_data=history_data,
101+
)
95102
format_weight, _, _ = format_weight_for_display(
96103
students_weight_data, "next_weight"
97104
)

app/common/history/history_reader.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from loguru import logger
88

99
from app.tools.path_utils import get_data_path, open_file, file_exists
10-
from app.common.data.list import get_gender_list, get_group_list
1110

1211

1312
# ==================================================
@@ -46,6 +45,15 @@ def get_roll_call_student_list(
4645
return []
4746

4847

48+
def check_roll_call_students_have_gender_or_group(
49+
cleaned_students: List[Tuple[str, str, str, str]],
50+
) -> Tuple[bool, bool]:
51+
"""根据已加载的学生快照判断是否包含性别和小组信息"""
52+
has_gender = any(str(gender or "").strip() for _, _, gender, _ in cleaned_students)
53+
has_group = any(str(group or "").strip() for _, _, _, group in cleaned_students)
54+
return has_gender, has_group
55+
56+
4957
def get_roll_call_history_data(
5058
class_name: str,
5159
) -> Dict[str, Any]:
@@ -325,11 +333,8 @@ def check_class_has_gender_or_group(
325333
Returns:
326334
Tuple[bool, bool]: (has_gender, has_group)
327335
"""
328-
gender_list = get_gender_list(class_name)
329-
group_list = get_group_list(class_name)
330-
has_gender = bool(gender_list) and gender_list != [""]
331-
has_group = bool(group_list) and group_list != [""]
332-
return has_gender, has_group
336+
cleaned_students = get_roll_call_student_list(class_name)
337+
return check_roll_call_students_have_gender_or_group(cleaned_students)
333338

334339

335340
# ==================================================

0 commit comments

Comments
 (0)