screenshot-vision-algorithm 0.3.2__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/PKG-INFO +1 -1
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/pyproject.toml +1 -1
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/badge_detection.py +1 -1
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/card_bbox.py +64 -83
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/speaker_band.py +10 -3
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/template_matching.py +110 -26
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/ocr/avatar_guard.py +1 -1
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/ocr/nickname_binding.py +26 -11
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm.egg-info/PKG-INFO +1 -1
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/README.md +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/setup.cfg +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/_config.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/avatar_column.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/phash_utils.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/algorithms/title_ocr.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/merge/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/merge/multipage.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/ocr/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/ocr/badge_ocr.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/ocr/text_ocr_adapter.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/profiles/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/profiles/android.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/profiles/harmony.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/profiles/ios.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_back_chevron.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_emoji_smile.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_plus.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_voice.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_title_more_dots.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/favorite_label.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/new_messages_hint_suffix.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/unread_divider_hint.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/unread_divider_hint_v2_textonly.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/wechat_note_header.png +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/xhs/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/android/zhihu/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/logging.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm/png_utils.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm.egg-info/SOURCES.txt +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm.egg-info/dependency_links.txt +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm.egg-info/requires.txt +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/src/screenshot_vision_algorithm.egg-info/top_level.txt +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/tests/test_card_bbox.py +0 -0
- {screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/tests/test_package.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: screenshot_vision_algorithm
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Multi-platform screenshot vision algorithms: WeChat (card bbox, speaker bands, avatar detection, OCR helpers), XHS & Zhihu (planned)
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "screenshot_vision_algorithm"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.8"
|
|
8
8
|
description = "Multi-platform screenshot vision algorithms: WeChat (card bbox, speaker bands, avatar detection, OCR helpers), XHS & Zhihu (planned)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -218,7 +218,7 @@ def count_badges_per_row_roi(
|
|
|
218
218
|
|
|
219
219
|
del kw # reserved for forwards-compat
|
|
220
220
|
|
|
221
|
-
from
|
|
221
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.template_matching import (
|
|
222
222
|
AVATAR_UNREAD_BADGE_LEFT_INSET_BASELINE,
|
|
223
223
|
AVATAR_UNREAD_BADGE_RIGHT_PAD_BASELINE,
|
|
224
224
|
CONVERSATION_AVATAR_RIGHT_EDGE_BASELINE,
|
|
@@ -46,17 +46,6 @@ from typing import Iterable, Optional, Sequence
|
|
|
46
46
|
import cv2
|
|
47
47
|
import numpy as np
|
|
48
48
|
|
|
49
|
-
#: Distance from the top of the ``favorite_label.png`` hit to the top of the
|
|
50
|
-
#: enclosing note card, measured at 1080x2248 baseline. Used as an
|
|
51
|
-
#: **upper-bound heuristic** for the first hit only (subsequent hits derive
|
|
52
|
-
#: their top from the previous hit's bottom + ``FAVORITE_TAIL_OFFSET``).
|
|
53
|
-
FAVORITE_TO_CARD_TOP_OFFSET_BASELINE = 421
|
|
54
|
-
|
|
55
|
-
#: Gap from the bottom of the ``favorite_label.png`` hit to the card's
|
|
56
|
-
#: actual bottom edge, at 1080x2248 baseline. Used to extend the card bbox
|
|
57
|
-
#: past the label.
|
|
58
|
-
FAVORITE_TAIL_OFFSET_BASELINE = 60
|
|
59
|
-
|
|
60
49
|
#: ``favorite_label.png`` raster size @ 1080-wide baseline (`chat_profiles/README.md`).
|
|
61
50
|
FAVORITE_LABEL_TEMPLATE_W_BASELINE = 70
|
|
62
51
|
FAVORITE_LABEL_TEMPLATE_H_BASELINE = 60
|
|
@@ -166,7 +155,10 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
166
155
|
bgr_img: np.ndarray,
|
|
167
156
|
screen_w: int,
|
|
168
157
|
screen_h: int,
|
|
169
|
-
) -> tuple[list[ThumbnailCard], list[BubbleBbox]]:
|
|
158
|
+
) -> tuple[list[ThumbnailCard], list[BubbleBbox], bool]:
|
|
159
|
+
"""Returns ``(cards, bubbles, pipeline_active)``; ``pipeline_active`` is
|
|
160
|
+
``True`` when the pipeline found vlines / zones and actively decided which
|
|
161
|
+
anchors to keep or drop, ``False`` when it dropped out early (no texture)."""
|
|
170
162
|
"""PRD S6: vline-segment + hline-boundary card + bubble bbox detection.
|
|
171
163
|
|
|
172
164
|
Steps:
|
|
@@ -248,7 +240,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
248
240
|
vd_lines = [vx for vx in vd_lines if vx - AR >= min_card_x_gap]
|
|
249
241
|
|
|
250
242
|
if not vd_lines:
|
|
251
|
-
return [], []
|
|
243
|
+
return [], [], False
|
|
252
244
|
|
|
253
245
|
# Pick best vline: max total mid pixels + longest continuous segment
|
|
254
246
|
vline_scores: dict[int, float] = {}
|
|
@@ -274,7 +266,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
274
266
|
best_vx = vx
|
|
275
267
|
|
|
276
268
|
if best_vx is None:
|
|
277
|
-
return [], []
|
|
269
|
+
return [], [], False
|
|
278
270
|
|
|
279
271
|
# Step 3 — vline segments, computed per candidate vline. A fav-anchored
|
|
280
272
|
# card may sit on a different right-edge vline than the frame-global best
|
|
@@ -306,7 +298,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
306
298
|
)
|
|
307
299
|
zones_by_vx = {vx: _zones_for(vx) for vx in vline_order}
|
|
308
300
|
if not any(zones_by_vx.values()):
|
|
309
|
-
return [], []
|
|
301
|
+
return [], [], False
|
|
310
302
|
|
|
311
303
|
# Step 4 — card boundary hlines (span > 50% card_w, X-near vline,
|
|
312
304
|
# max gray gradient >= 30 = white→gray boundary)
|
|
@@ -405,6 +397,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
405
397
|
gap_tol = max(1, int(round(GAP_HLINE_TO_NEXT_ZONE_MAX_BASELINE * sw)))
|
|
406
398
|
cards: list[ThumbnailCard] = []
|
|
407
399
|
prev_bottom: int | None = None # anti-overlap: enforce card boundaries non-overlapping
|
|
400
|
+
any_anchor_seen: bool = False # True if ≥1 anchor entered zone-matching loop
|
|
408
401
|
for idx, hit in enumerate(ordered_hits):
|
|
409
402
|
fav_cy = hit.y + hit.h // 2
|
|
410
403
|
|
|
@@ -413,6 +406,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
413
406
|
zones = zones_by_vx[vx]
|
|
414
407
|
if not zones:
|
|
415
408
|
continue
|
|
409
|
+
any_anchor_seen = True # anchor entered vline loop with non-empty zones
|
|
416
410
|
card_hlines = hlines_by_vx[vx]
|
|
417
411
|
|
|
418
412
|
def _snap(y1_: int, y2_: int) -> tuple[int, int]:
|
|
@@ -553,7 +547,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
553
547
|
continue
|
|
554
548
|
bubbles.append(BubbleBbox(top=by1, bottom=by2, left=AR, right=int(vx)))
|
|
555
549
|
|
|
556
|
-
return cards, bubbles
|
|
550
|
+
return cards, bubbles, any_anchor_seen
|
|
557
551
|
|
|
558
552
|
|
|
559
553
|
def _compute_exact_card_bboxes(
|
|
@@ -561,13 +555,22 @@ def _compute_exact_card_bboxes(
|
|
|
561
555
|
bgr_img: np.ndarray,
|
|
562
556
|
screen_w: int,
|
|
563
557
|
screen_h: int,
|
|
564
|
-
) -> list[ThumbnailCard]:
|
|
558
|
+
) -> tuple[list[ThumbnailCard], bool]:
|
|
565
559
|
"""Backward-compat wrapper — cards only (see
|
|
566
|
-
:func:`_compute_exact_card_and_bubble_bboxes`).
|
|
567
|
-
|
|
560
|
+
:func:`_compute_exact_card_and_bubble_bboxes`).
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
(cards, pipeline_active):
|
|
564
|
+
- ``pipeline_active``: ``True`` when the pipeline found vlines
|
|
565
|
+
and zones (it *actively* decided to drop anchors without a
|
|
566
|
+
containing zone); ``False`` when the image had insufficient
|
|
567
|
+
texture (no vlines / no zones) and a legacy fallback is
|
|
568
|
+
appropriate.
|
|
569
|
+
"""
|
|
570
|
+
cards, _bubbles, pipeline_active = _compute_exact_card_and_bubble_bboxes(
|
|
568
571
|
ordered_hits, bgr_img, screen_w, screen_h,
|
|
569
572
|
)
|
|
570
|
-
return cards
|
|
573
|
+
return cards, pipeline_active
|
|
571
574
|
|
|
572
575
|
|
|
573
576
|
def derive_cards_and_bubbles(
|
|
@@ -591,7 +594,7 @@ def derive_cards_and_bubbles(
|
|
|
591
594
|
return [], []
|
|
592
595
|
ordered = sorted(hits, key=lambda h: h.y)
|
|
593
596
|
try:
|
|
594
|
-
cards, bubbles = _compute_exact_card_and_bubble_bboxes(
|
|
597
|
+
cards, bubbles, _pipeline_active = _compute_exact_card_and_bubble_bboxes(
|
|
595
598
|
ordered, chat_img, screen_w, screen_h,
|
|
596
599
|
)
|
|
597
600
|
except Exception:
|
|
@@ -622,15 +625,15 @@ def derive_cards(
|
|
|
622
625
|
(we sort by y ourselves).
|
|
623
626
|
screen_w / screen_h: the source screenshot's raw-image dimensions
|
|
624
627
|
(usually equal to ``device_info.screen_resolution``).
|
|
625
|
-
scale_w: ``screen_w / baseline_w`` (baseline = 1080).
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
628
|
+
scale_w: ``screen_w / baseline_w`` (baseline = 1080). Passed to
|
|
629
|
+
:func:`drop_top_clamped_false_positive_cards` for
|
|
630
|
+
false-positive filtering.
|
|
631
|
+
chat_img: required BGR image (as returned by ``cv2.imdecode``).
|
|
632
|
+
The vline-segment + hline-boundary variance-based
|
|
633
|
+
algorithm (PRD S6) is used for precise card bbox
|
|
634
|
+
derivation. Returns ``[]`` when ``chat_img`` is
|
|
635
|
+
``None`` or the pipeline encounters an unrecoverable
|
|
636
|
+
error.
|
|
634
637
|
|
|
635
638
|
Returns:
|
|
636
639
|
One :class:`ThumbnailCard` per hit, sorted top -> bottom. Empty list
|
|
@@ -642,61 +645,22 @@ def derive_cards(
|
|
|
642
645
|
|
|
643
646
|
ordered = sorted(hits, key=lambda h: h.y)
|
|
644
647
|
|
|
645
|
-
#
|
|
646
|
-
#
|
|
647
|
-
#
|
|
648
|
-
|
|
649
|
-
# pipeline produces no cards (e.g. variance texture insufficient),
|
|
650
|
-
# legacy fixed-offset serves as a reasonable fallback.
|
|
651
|
-
if chat_img is not None and len(ordered) > 0:
|
|
648
|
+
# vline-segment + hline-boundary algorithm (PRD S6). Pipeline
|
|
649
|
+
# result is final — anchors it dropped (no containing zone → "点对卡、
|
|
650
|
+
# 记错框" guard) must NOT be resurrected by fallback logic.
|
|
651
|
+
if chat_img is not None:
|
|
652
652
|
try:
|
|
653
|
-
exact_cards = _compute_exact_card_bboxes(
|
|
653
|
+
exact_cards, _pipeline_active = _compute_exact_card_bboxes(
|
|
654
654
|
ordered, chat_img, screen_w, screen_h,
|
|
655
655
|
)
|
|
656
656
|
except Exception:
|
|
657
|
-
|
|
657
|
+
return []
|
|
658
658
|
else:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
)
|
|
663
|
-
# exact_cards empty → fall through to legacy
|
|
664
|
-
|
|
665
|
-
# Legacy: fixed-offset algorithm
|
|
666
|
-
top_offset = int(round(FAVORITE_TO_CARD_TOP_OFFSET_BASELINE * scale_w))
|
|
667
|
-
tail_offset = int(round(FAVORITE_TAIL_OFFSET_BASELINE * scale_w))
|
|
668
|
-
|
|
669
|
-
cards: list[ThumbnailCard] = []
|
|
670
|
-
for idx, hit in enumerate(ordered):
|
|
671
|
-
if idx == 0:
|
|
672
|
-
card_top = max(0, hit.y1 - top_offset)
|
|
673
|
-
else:
|
|
674
|
-
prev_bottom = ordered[idx - 1].y2 + tail_offset
|
|
675
|
-
card_top = max(prev_bottom + 1, 0)
|
|
676
|
-
|
|
677
|
-
card_bottom = min(screen_h - 1, hit.y2 + tail_offset)
|
|
678
|
-
|
|
679
|
-
if card_bottom <= card_top:
|
|
680
|
-
continue
|
|
681
|
-
|
|
682
|
-
click_x = screen_w // 2
|
|
683
|
-
click_y = min(screen_h - 1, max(0, hit.y + hit.h // 2))
|
|
684
|
-
midline = screen_w / 2
|
|
685
|
-
click_side = "left" if click_x < midline else "right"
|
|
686
|
-
|
|
687
|
-
cards.append(ThumbnailCard(
|
|
688
|
-
index=idx,
|
|
689
|
-
top=card_top,
|
|
690
|
-
bottom=card_bottom,
|
|
691
|
-
left=0,
|
|
692
|
-
right=screen_w - 1,
|
|
693
|
-
click_x=click_x,
|
|
694
|
-
click_y=click_y,
|
|
695
|
-
click_side=click_side,
|
|
696
|
-
favorite_hit=hit,
|
|
697
|
-
))
|
|
659
|
+
return drop_top_clamped_false_positive_cards(
|
|
660
|
+
exact_cards, scale_w=scale_w,
|
|
661
|
+
)
|
|
698
662
|
|
|
699
|
-
return
|
|
663
|
+
return []
|
|
700
664
|
|
|
701
665
|
|
|
702
666
|
def drop_top_clamped_false_positive_cards(
|
|
@@ -874,12 +838,32 @@ def _card_height(card: ThumbnailCard) -> int:
|
|
|
874
838
|
return card.bottom - card.top + 1
|
|
875
839
|
|
|
876
840
|
|
|
841
|
+
#: 卡片高度匹配容差(像素)。同一张卡片在相邻两帧中 bbox 高度可能差 1-2px
|
|
842
|
+
#: (back_to_chat 过渡动画导致列表微偏移),±5px 容差可覆盖此误差。
|
|
843
|
+
_CARD_HEIGHT_TOLERANCE: int = 5
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _heights_match(
|
|
847
|
+
a_heights: list[int], a_start: int,
|
|
848
|
+
b_heights: list[int], b_start: int,
|
|
849
|
+
length: int,
|
|
850
|
+
) -> bool:
|
|
851
|
+
"""Return True if a_heights[a_start:a_start+length] equals b_heights[b_start:b_start+length]
|
|
852
|
+
within ``_CARD_HEIGHT_TOLERANCE`` px per element."""
|
|
853
|
+
for k in range(length):
|
|
854
|
+
if abs(a_heights[a_start + k] - b_heights[b_start + k]) > _CARD_HEIGHT_TOLERANCE:
|
|
855
|
+
return False
|
|
856
|
+
return True
|
|
857
|
+
|
|
858
|
+
|
|
877
859
|
def _longest_subsequence_match(
|
|
878
860
|
a_heights: list[int],
|
|
879
861
|
b_heights: list[int],
|
|
880
862
|
) -> tuple[int, int, int] | None:
|
|
881
863
|
"""Find the longest contiguous substring of a_heights that appears in b_heights.
|
|
882
864
|
|
|
865
|
+
Matching uses ± ``_CARD_HEIGHT_TOLERANCE`` px per element (PRD §10 第二步 容差)。
|
|
866
|
+
|
|
883
867
|
Returns ``(a_start, b_start, length)`` or ``None``.
|
|
884
868
|
If multiple matches share the same maximum length, picks the one with
|
|
885
869
|
the smallest ``b_start`` (earliest in B).
|
|
@@ -894,9 +878,8 @@ def _longest_subsequence_match(
|
|
|
894
878
|
for length in range(max_len, 0, -1):
|
|
895
879
|
if best is not None and length < best[2]:
|
|
896
880
|
break
|
|
897
|
-
sub = a_heights[a_start:a_start + length]
|
|
898
881
|
for b_start in range(len(b_heights) - length + 1):
|
|
899
|
-
if b_heights
|
|
882
|
+
if _heights_match(a_heights, a_start, b_heights, b_start, length):
|
|
900
883
|
if (
|
|
901
884
|
best is None
|
|
902
885
|
or length > best[2]
|
|
@@ -971,8 +954,6 @@ def pick_next_unclicked_card(
|
|
|
971
954
|
__all__ = [
|
|
972
955
|
"FAVORITE_LABEL_TEMPLATE_W_BASELINE",
|
|
973
956
|
"FAVORITE_LABEL_TEMPLATE_H_BASELINE",
|
|
974
|
-
"FAVORITE_TO_CARD_TOP_OFFSET_BASELINE",
|
|
975
|
-
"FAVORITE_TAIL_OFFSET_BASELINE",
|
|
976
957
|
"FAV_LABEL_HLINE_EXCLUDE_PAD_BASELINE",
|
|
977
958
|
"GAP_HLINE_TO_NEXT_ZONE_MAX_BASELINE",
|
|
978
959
|
"PROCESSED_CARD_MATCH_MIN_OVERLAP_RATIO",
|
|
@@ -26,7 +26,7 @@ from screenshot_vision_algorithm.android.wechat.algorithms.template_matching imp
|
|
|
26
26
|
CHAT_BAND_FIRST_AVATAR_TOP_GAP_REJECT_BASELINE = 30
|
|
27
27
|
|
|
28
28
|
#: 首个发言人筒上移量(一个头像高度,防止几何误差),见 PRD §5 补充2。
|
|
29
|
-
CHAT_FIRST_BAND_TOP_EXTEND_BASELINE =
|
|
29
|
+
CHAT_FIRST_BAND_TOP_EXTEND_BASELINE = 20
|
|
30
30
|
|
|
31
31
|
#: 底部输入区预留(与主列表 scanner 一脉,baseline)。
|
|
32
32
|
CHAT_COMPOSER_RESERVE_BOTTOM_BASELINE = 160
|
|
@@ -173,7 +173,7 @@ def detect_chat_side_avatar_ytops(
|
|
|
173
173
|
y1 = max(0, bounds.y_top)
|
|
174
174
|
y2 = min(h, bounds.y_bottom_excl)
|
|
175
175
|
try:
|
|
176
|
-
from
|
|
176
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.avatar_column import (
|
|
177
177
|
detect_left_avatar_column_layout,
|
|
178
178
|
left_avatar_ytops,
|
|
179
179
|
)
|
|
@@ -254,7 +254,7 @@ def build_prd_speaker_vertical_bands(
|
|
|
254
254
|
if not merged_starts or y - merged_starts[-1] >= min_sep:
|
|
255
255
|
merged_starts.append(y)
|
|
256
256
|
|
|
257
|
-
# 首个筒上界上移(PRD §5 补充2):上移
|
|
257
|
+
# 首个筒上界上移(PRD §5 补充2):上移 20px @1080,防几何误差与系统变化。
|
|
258
258
|
first_band_extend = int(round(int(first_band_top_extend_baseline) * sw))
|
|
259
259
|
|
|
260
260
|
bands: list[tuple[int, int]] = []
|
|
@@ -275,6 +275,13 @@ def build_prd_speaker_vertical_bands(
|
|
|
275
275
|
elif first_band_start < orphan_top[1]:
|
|
276
276
|
orphan_top = (orphan_top[0], first_band_start)
|
|
277
277
|
|
|
278
|
+
# 无 orphan 时,首个 band 上界对齐 y_ct,保证纵向连续全覆盖
|
|
279
|
+
# (extend 较小且头像紧贴 y_ct 时可能产生小间隙)
|
|
280
|
+
if orphan_top is None and bands:
|
|
281
|
+
first_y0, first_y1 = bands[0]
|
|
282
|
+
if first_y0 > y_ct:
|
|
283
|
+
bands[0] = (y_ct, first_y1)
|
|
284
|
+
|
|
278
285
|
return PrdChatVerticalLayout(
|
|
279
286
|
content_bounds=bounds,
|
|
280
287
|
orphan_top_band=orphan_top,
|
|
@@ -419,6 +419,43 @@ def detect_chat_title_more_dots(
|
|
|
419
419
|
return more
|
|
420
420
|
|
|
421
421
|
|
|
422
|
+
def detect_wechat_layer(
|
|
423
|
+
screen_bgr: np.ndarray,
|
|
424
|
+
scale_w: float,
|
|
425
|
+
) -> str | None:
|
|
426
|
+
"""4-layer classification — layout identical to the 162/162 3-device experiment.
|
|
427
|
+
|
|
428
|
+
Priority order (top to bottom):
|
|
429
|
+
|
|
430
|
+
* **L3**: ``detect_wechat_note_header`` hit
|
|
431
|
+
* **L2**: ``match_chat_back_chevron_core`` or ``match_chat_title_more_dots_core``
|
|
432
|
+
(no chrome / session-list-shell suppressor — the experiment used bare
|
|
433
|
+
``_core`` hits + ``_CONTEXT_SUPPRESS`` post-filtering, and the
|
|
434
|
+
note_header gate is already exercised by the L3-takes-priority
|
|
435
|
+
ordering)
|
|
436
|
+
* **L1**: ``is_wechat_main_conversation_list_chrome`` (without pinned-row
|
|
437
|
+
requirement) or ``detect_wechat_main_bottom_tab_bar_four_columns``
|
|
438
|
+
* **None**: no layer recognised
|
|
439
|
+
|
|
440
|
+
Callers that also need external foreground-package gating (L0)
|
|
441
|
+
are responsible for checking the foreground app before calling
|
|
442
|
+
this function.
|
|
443
|
+
"""
|
|
444
|
+
if detect_wechat_note_header(screen_bgr, scale_w) is not None:
|
|
445
|
+
return "L3"
|
|
446
|
+
if match_chat_back_chevron_core(screen_bgr, scale_w) is not None:
|
|
447
|
+
return "L2"
|
|
448
|
+
if match_chat_title_more_dots_core(screen_bgr, scale_w) is not None:
|
|
449
|
+
return "L2"
|
|
450
|
+
if is_wechat_main_conversation_list_chrome(
|
|
451
|
+
screen_bgr, scale_w, require_visible_pinned_row=False,
|
|
452
|
+
):
|
|
453
|
+
return "L1"
|
|
454
|
+
if detect_wechat_main_bottom_tab_bar_four_columns(screen_bgr, scale_w):
|
|
455
|
+
return "L1"
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
|
|
422
459
|
def _chat_bottom_input_roi(screen_bgr: np.ndarray, scale_w: float) -> np.ndarray:
|
|
423
460
|
"""Bottom / keyboard-adjacent band for 话筒・表情・「+」(含输入框略抬高时的中区)。"""
|
|
424
461
|
h, w = screen_bgr.shape[:2]
|
|
@@ -851,6 +888,12 @@ CONVERSATION_FIRST_ROW_TOP_BASELINE = 280
|
|
|
851
888
|
#: Avatar column ends ~here @ 1080 baseline; nickname + preview middle band;
|
|
852
889
|
# mirrors :meth:`collector.driver.TopGroupScanDriver.scan_pinned_groups`.
|
|
853
890
|
CONVERSATION_AVATAR_RIGHT_EDGE_BASELINE = 220
|
|
891
|
+
#: Horizontal inset left of ``CONVERSATION_AVATAR_RIGHT_EDGE_BASELINE`` for
|
|
892
|
+
#: unread badge horizontal ROI (1080 baseline).
|
|
893
|
+
AVATAR_UNREAD_BADGE_LEFT_INSET_BASELINE = 90
|
|
894
|
+
#: Horizontal padding right of ``CONVERSATION_AVATAR_RIGHT_EDGE_BASELINE`` for
|
|
895
|
+
#: unread badge horizontal ROI (1080 baseline).
|
|
896
|
+
AVATAR_UNREAD_BADGE_RIGHT_PAD_BASELINE = 36
|
|
854
897
|
CONVERSATION_RIGHT_SIDE_RESERVED_BASELINE = 220
|
|
855
898
|
|
|
856
899
|
#: Horizontal ROI (1080 baseline) for list-side avatar centroid detection — left
|
|
@@ -1256,6 +1299,9 @@ def detect_wechat_main_bottom_tab_bar_four_columns(
|
|
|
1256
1299
|
# Weaker tab may be 「我」单层笔画;最强一般为「通讯录」—— 拒绝单点极强、其余近空
|
|
1257
1300
|
if fmin < fmax * 0.20:
|
|
1258
1301
|
return False
|
|
1302
|
+
# 任一列墨水>0.8%即认可("最近"页面无"微信"栏会匹配)
|
|
1303
|
+
if fmax < 0.008:
|
|
1304
|
+
return False
|
|
1259
1305
|
return True
|
|
1260
1306
|
|
|
1261
1307
|
|
|
@@ -1455,23 +1501,17 @@ def reposition_anchor_bottom_wechat_tap_lane_plausible(
|
|
|
1455
1501
|
return True
|
|
1456
1502
|
|
|
1457
1503
|
|
|
1458
|
-
def _longest_true_run(mask: np.ndarray) -> int:
|
|
1459
|
-
"""最大连续 True 块的行数。"""
|
|
1460
|
-
if not mask.any():
|
|
1461
|
-
return 0
|
|
1462
|
-
padded = np.concatenate(([False], mask, [False]))
|
|
1463
|
-
edges = np.where(padded[:-1] != padded[1:])[0]
|
|
1464
|
-
if len(edges) == 0:
|
|
1465
|
-
return 0
|
|
1466
|
-
runs = edges[1::2] - edges[::2]
|
|
1467
|
-
return int(runs.max())
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
1504
|
def _recent_pull_top_heading_likely(
|
|
1471
1505
|
screen_bgr: np.ndarray,
|
|
1472
1506
|
scale_w: float,
|
|
1473
1507
|
) -> bool:
|
|
1474
|
-
"""顶区「最近」类标题:亮底深字 **或** 暗底浅字,竖向窄中带。
|
|
1508
|
+
"""顶区「最近」类标题:亮底深字 **或** 暗底浅字,竖向窄中带。
|
|
1509
|
+
|
|
1510
|
+
- 优先检测 dark-pixel 墨迹密度 + 竖向跨度 span
|
|
1511
|
+
- 再尝试 gray>96 亮像素(小米/华为 ROM 差异化兼容)
|
|
1512
|
+
- Honor/暗色主题下 ink_d 异常高,走 gray>150 亮字簇路径,
|
|
1513
|
+
用 **最大连续亮行 run** 排除散点噪声 vs 真标题集群。
|
|
1514
|
+
"""
|
|
1475
1515
|
h, w = screen_bgr.shape[:2]
|
|
1476
1516
|
sw = max(scale_w, 1e-6)
|
|
1477
1517
|
y1 = int(round(222 * sw))
|
|
@@ -1492,27 +1532,72 @@ def _recent_pull_top_heading_likely(
|
|
|
1492
1532
|
|
|
1493
1533
|
_, mask_dark = cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV)
|
|
1494
1534
|
ink_d = float(cv2.countNonZero(mask_dark)) / area
|
|
1535
|
+
|
|
1536
|
+
# 路径 1:暗墨标题(D1/D2 浅色主题首选)
|
|
1495
1537
|
if 0.014 <= ink_d <= 0.48:
|
|
1496
1538
|
row_dark = (mask_dark.sum(axis=1) // 255).astype(np.int32)
|
|
1497
1539
|
has_row = row_dark > 9
|
|
1498
1540
|
if has_row.any():
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if
|
|
1541
|
+
idx = np.where(has_row)[0]
|
|
1542
|
+
span_rows = int(idx.max()) - int(idx.min()) + 1
|
|
1543
|
+
if span_rows <= int(round(70 * sw)):
|
|
1502
1544
|
return True
|
|
1503
1545
|
|
|
1546
|
+
# 路径 2:暗色主题下 ink_d 异常高 → 走亮字簇(D3 Honor 暗色主题)
|
|
1547
|
+
if ink_d > 0.48:
|
|
1548
|
+
bright = (gray > 150).astype(np.uint8)
|
|
1549
|
+
ink_b = float(np.sum(bright)) / area
|
|
1550
|
+
if 0.004 <= ink_b <= 0.10:
|
|
1551
|
+
row_counts = bright.sum(axis=1).astype(np.int32)
|
|
1552
|
+
max_run = 0
|
|
1553
|
+
cur_run = 0
|
|
1554
|
+
for rc in row_counts:
|
|
1555
|
+
if rc > 3:
|
|
1556
|
+
cur_run += 1
|
|
1557
|
+
if cur_run > max_run:
|
|
1558
|
+
max_run = cur_run
|
|
1559
|
+
else:
|
|
1560
|
+
cur_run = 0
|
|
1561
|
+
if 8 <= max_run <= int(round(80 * sw)):
|
|
1562
|
+
return True
|
|
1563
|
+
|
|
1564
|
+
# 路径 3:不依赖字体颜色的通用亮底(小米 / 华为 ROM 兼容)
|
|
1504
1565
|
light = (gray > 96).astype(np.uint8)
|
|
1505
1566
|
ink_l = float(np.sum(light)) / area
|
|
1506
1567
|
if 0.006 <= ink_l <= 0.32:
|
|
1507
1568
|
row_lit = light.sum(axis=1).astype(np.int32)
|
|
1508
1569
|
has_row = row_lit > 5
|
|
1509
1570
|
if has_row.any():
|
|
1510
|
-
|
|
1511
|
-
|
|
1571
|
+
idx = np.where(has_row)[0]
|
|
1572
|
+
span_rows = int(idx.max()) - int(idx.min()) + 1
|
|
1573
|
+
if span_rows <= int(round(72 * sw)):
|
|
1512
1574
|
return True
|
|
1513
1575
|
return False
|
|
1514
1576
|
|
|
1515
1577
|
|
|
1578
|
+
def is_wechat_recent_page_chrome(
|
|
1579
|
+
screen_bgr: np.ndarray,
|
|
1580
|
+
scale_w: float,
|
|
1581
|
+
) -> bool:
|
|
1582
|
+
"""Detect WeChat 「最近」小程序抽屉页。
|
|
1583
|
+
|
|
1584
|
+
Two conditions must BOTH hold:
|
|
1585
|
+
1. Top mid-band contains a compact heading-like dark/bright ink cluster
|
|
1586
|
+
(:func:`_recent_pull_top_heading_likely`).
|
|
1587
|
+
2. The WeChat 4-tab bottom bar is **absent** — the 「最近」drawer
|
|
1588
|
+
replaces it with a dark/empty strip.
|
|
1589
|
+
|
|
1590
|
+
Condition (2) prevents false positives on the normal conversation list
|
|
1591
|
+
where group names and message previews can produce heading-like ink
|
|
1592
|
+
densities in the mid-band.
|
|
1593
|
+
"""
|
|
1594
|
+
if not _recent_pull_top_heading_likely(screen_bgr, scale_w):
|
|
1595
|
+
return False
|
|
1596
|
+
if detect_wechat_main_bottom_tab_bar_four_columns(screen_bgr, scale_w):
|
|
1597
|
+
return False
|
|
1598
|
+
return True
|
|
1599
|
+
|
|
1600
|
+
|
|
1516
1601
|
def is_wechat_recent_pull_anchor_early_stop_frame(
|
|
1517
1602
|
screen_bgr: np.ndarray,
|
|
1518
1603
|
scale_w: float,
|
|
@@ -1520,15 +1605,12 @@ def is_wechat_recent_pull_anchor_early_stop_frame(
|
|
|
1520
1605
|
tab_x: int,
|
|
1521
1606
|
tab_y: int,
|
|
1522
1607
|
) -> bool:
|
|
1523
|
-
"""§步骤1(2)
|
|
1608
|
+
"""§步骤1(2) 早停:heading 命中 + 底栏四列缺失。
|
|
1524
1609
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
):
|
|
1530
|
-
return False
|
|
1531
|
-
return _recent_pull_top_heading_likely(screen_bgr, scale_w)
|
|
1610
|
+
通过 :func:`is_wechat_recent_page_chrome` 双条件把关 —
|
|
1611
|
+
heading 启发式 + 四列缺失保证不会在普通会话列表上误触发。
|
|
1612
|
+
``tab_x`` / ``tab_y`` 保留为兼容参数,内部不参与判断。"""
|
|
1613
|
+
return is_wechat_recent_page_chrome(screen_bgr, scale_w)
|
|
1532
1614
|
|
|
1533
1615
|
|
|
1534
1616
|
def split_conversation_rows(
|
|
@@ -2133,6 +2215,8 @@ __all__ = [
|
|
|
2133
2215
|
"LIST_AVATAR_BADGE_ORANGE_AUX_VAL_MIN",
|
|
2134
2216
|
"CONVERSATION_ROW_HEIGHT_BASELINE",
|
|
2135
2217
|
"CONVERSATION_AVATAR_RIGHT_EDGE_BASELINE",
|
|
2218
|
+
"AVATAR_UNREAD_BADGE_LEFT_INSET_BASELINE",
|
|
2219
|
+
"AVATAR_UNREAD_BADGE_RIGHT_PAD_BASELINE",
|
|
2136
2220
|
"CONVERSATION_LIST_AVATAR_ROI_LEFT_BASELINE",
|
|
2137
2221
|
"CONVERSATION_LIST_AVATAR_ROI_RIGHT_BASELINE",
|
|
2138
2222
|
"CONVERSATION_LIST_AVATAR_MEDIAN_X_HALF_WIDTH_BASELINE",
|
|
@@ -19,7 +19,7 @@ except ImportError:
|
|
|
19
19
|
cv2 = None # type: ignore[assignment]
|
|
20
20
|
|
|
21
21
|
try:
|
|
22
|
-
from
|
|
22
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.avatar_column import AvatarCentroid
|
|
23
23
|
except ImportError:
|
|
24
24
|
AvatarCentroid = None # type: ignore[misc, assignment]
|
|
25
25
|
|
|
@@ -351,6 +351,9 @@ _PURE_TIME_RE = re.compile(r"^\d{1,2}:\d{2}(:\d{2})?$")
|
|
|
351
351
|
_WEEKDAY_TIME_RE = re.compile(
|
|
352
352
|
r"^周[一二三四五六日天].*?(?:上午|下午|晚上|凌晨)?\s*\d{0,2}[::]?\d{0,2}"
|
|
353
353
|
)
|
|
354
|
+
_YESTERDAY_TODAY_TIME_RE = re.compile(
|
|
355
|
+
r"^(?:昨天|今天)\s*\d{0,2}:?\d{0,2}$"
|
|
356
|
+
)
|
|
354
357
|
_MOBILE_PHONE_SUFFIX_RE = re.compile(r"1\d{10}\s*$")
|
|
355
358
|
|
|
356
359
|
|
|
@@ -388,6 +391,8 @@ def _is_chat_timestamp_or_divider(text: str) -> bool:
|
|
|
388
391
|
return True
|
|
389
392
|
if _WEEKDAY_TIME_RE.match(t):
|
|
390
393
|
return True
|
|
394
|
+
if _YESTERDAY_TODAY_TIME_RE.match(t):
|
|
395
|
+
return True
|
|
391
396
|
return False
|
|
392
397
|
|
|
393
398
|
|
|
@@ -411,7 +416,7 @@ def _is_avatar_column_nickname_row(
|
|
|
411
416
|
avatar_layout: Any = None,
|
|
412
417
|
) -> bool:
|
|
413
418
|
try:
|
|
414
|
-
from
|
|
419
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.avatar_column import nickname_bbox_in_avatar_column
|
|
415
420
|
except ImportError:
|
|
416
421
|
nickname_bbox_in_avatar_column = None # type: ignore[assignment,misc]
|
|
417
422
|
if avatar_layout is not None and nickname_bbox_in_avatar_column is not None:
|
|
@@ -446,7 +451,7 @@ def _avatar_anchor_for_block(
|
|
|
446
451
|
if avatar_layout is None or getattr(avatar_layout, "empty", True):
|
|
447
452
|
return None
|
|
448
453
|
try:
|
|
449
|
-
from
|
|
454
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.avatar_column import find_avatar_anchor_for_nickname_bbox
|
|
450
455
|
except ImportError:
|
|
451
456
|
return None
|
|
452
457
|
return find_avatar_anchor_for_nickname_bbox(
|
|
@@ -463,9 +468,9 @@ def _load_left_avatar_layout(
|
|
|
463
468
|
if png_path is None:
|
|
464
469
|
return None
|
|
465
470
|
try:
|
|
466
|
-
from
|
|
467
|
-
from
|
|
468
|
-
from
|
|
471
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.speaker_band import compute_chat_content_vertical_bounds
|
|
472
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.avatar_column import detect_left_avatar_column_layout
|
|
473
|
+
from screenshot_vision_algorithm.android.wechat.ocr.avatar_guard import load_png_bgr
|
|
469
474
|
except ImportError:
|
|
470
475
|
return None
|
|
471
476
|
arr = load_png_bgr(str(png_path))
|
|
@@ -595,7 +600,7 @@ def _nickname_passes_avatar_guard(
|
|
|
595
600
|
if bgr_guard is None:
|
|
596
601
|
return not config.require_avatar_guard_for_nickname
|
|
597
602
|
try:
|
|
598
|
-
from
|
|
603
|
+
from screenshot_vision_algorithm.android.wechat.ocr.avatar_guard import avatar_roi_pass
|
|
599
604
|
except ImportError:
|
|
600
605
|
return not config.require_avatar_guard_for_nickname
|
|
601
606
|
|
|
@@ -687,13 +692,13 @@ def _load_prd_layout_optional(
|
|
|
687
692
|
if png_path is None or not config.use_prd_chat_vertical_bands:
|
|
688
693
|
return None
|
|
689
694
|
try:
|
|
690
|
-
from
|
|
695
|
+
from screenshot_vision_algorithm.android.wechat.algorithms.speaker_band import (
|
|
691
696
|
build_prd_speaker_vertical_bands,
|
|
692
697
|
compute_chat_content_vertical_bounds,
|
|
693
698
|
detect_chat_side_avatar_ytops,
|
|
694
699
|
merge_avatar_ytops_time_order,
|
|
695
700
|
)
|
|
696
|
-
from
|
|
701
|
+
from screenshot_vision_algorithm.android.wechat.ocr.avatar_guard import load_png_bgr
|
|
697
702
|
except ImportError:
|
|
698
703
|
return None
|
|
699
704
|
arr = load_png_bgr(str(png_path))
|
|
@@ -962,7 +967,17 @@ def _resume_thumb_bindings_and_orphans(
|
|
|
962
967
|
_cy1, _cy2 = float(tbb[1]), float(tbb[3])
|
|
963
968
|
for _bi, (_b0, _b1) in enumerate(_sb_list):
|
|
964
969
|
if _cy2 > float(_b0) and _cy1 < float(_b1):
|
|
965
|
-
|
|
970
|
+
band_idx = _bi # 相交 → 候选;持续覆盖 → 最后一个(下方)胜出
|
|
971
|
+
|
|
972
|
+
# ── 如果卡片与 orphan_top_band 相交 → 直接记为找不到归属 ──
|
|
973
|
+
if layout_opt is not None:
|
|
974
|
+
ot = getattr(layout_opt, "orphan_top_band", None)
|
|
975
|
+
if ot is not None:
|
|
976
|
+
oy0, oy1 = float(ot[0]), float(ot[1])
|
|
977
|
+
_cy1, _cy2 = float(tbb[1]), float(tbb[3])
|
|
978
|
+
if _cy2 > oy0 and _cy1 < oy1:
|
|
979
|
+
orphan += 1
|
|
980
|
+
continue
|
|
966
981
|
|
|
967
982
|
matched_nickname: Optional[str] = None
|
|
968
983
|
|
|
@@ -1177,7 +1192,7 @@ def first_attrib_verbatim_display_line_and_bbox(
|
|
|
1177
1192
|
scale_w = float(original_width) / 1080.0
|
|
1178
1193
|
if png_path is not None:
|
|
1179
1194
|
try:
|
|
1180
|
-
from
|
|
1195
|
+
from screenshot_vision_algorithm.android.wechat.ocr.avatar_guard import load_png_bgr
|
|
1181
1196
|
|
|
1182
1197
|
bgr_guard = load_png_bgr(str(png_path))
|
|
1183
1198
|
except ImportError:
|
|
@@ -1748,7 +1763,7 @@ def extract_nicknames(
|
|
|
1748
1763
|
bgr_guard = None
|
|
1749
1764
|
if png_path is not None:
|
|
1750
1765
|
try:
|
|
1751
|
-
from
|
|
1766
|
+
from screenshot_vision_algorithm.android.wechat.ocr.avatar_guard import load_png_bgr
|
|
1752
1767
|
|
|
1753
1768
|
bgr_guard = load_png_bgr(str(png_path))
|
|
1754
1769
|
except ImportError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: screenshot_vision_algorithm
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Multi-platform screenshot vision algorithms: WeChat (card bbox, speaker bands, avatar detection, OCR helpers), XHS & Zhihu (planned)
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/tests/test_card_bbox.py
RENAMED
|
File without changes
|
{screenshot_vision_algorithm-0.3.2 → screenshot_vision_algorithm-0.3.8}/tests/test_package.py
RENAMED
|
File without changes
|