screenshot-vision-algorithm 0.3.1__tar.gz → 0.3.5__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.1 → screenshot_vision_algorithm-0.3.5}/PKG-INFO +1 -1
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/pyproject.toml +1 -1
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/__init__.py +0 -1
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/badge_detection.py +1 -1
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/card_bbox.py +62 -84
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/template_matching.py +110 -26
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm.egg-info/PKG-INFO +1 -1
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/README.md +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/setup.cfg +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/_config.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/avatar_column.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/phash_utils.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/speaker_band.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/algorithms/title_ocr.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/merge/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/merge/multipage.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/ocr/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/ocr/avatar_guard.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/ocr/badge_ocr.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/ocr/nickname_binding.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/ocr/text_ocr_adapter.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/profiles/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/profiles/android.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/profiles/harmony.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/profiles/ios.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_back_chevron.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_emoji_smile.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_plus.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_input_voice.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/chat_title_more_dots.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/favorite_label.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/new_messages_hint_suffix.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/unread_divider_hint.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/unread_divider_hint_v2_textonly.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/wechat/templates/android/8.0.69/wechat_note_header.png +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/xhs/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/android/zhihu/__init__.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/logging.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm/png_utils.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm.egg-info/SOURCES.txt +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm.egg-info/dependency_links.txt +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm.egg-info/requires.txt +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/src/screenshot_vision_algorithm.egg-info/top_level.txt +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/tests/test_card_bbox.py +0 -0
- {screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/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.5
|
|
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.5"
|
|
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" }
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from screenshot_vision_algorithm._config import Platform, WeChatVersion, Profile
|
|
4
4
|
|
|
5
5
|
from screenshot_vision_algorithm.android.wechat.algorithms.card_bbox import (
|
|
6
|
-
TAP_ANTI_DETECT_X_JITTER_PX,
|
|
7
6
|
_compute_exact_card_and_bubble_bboxes,
|
|
8
7
|
_compute_exact_card_bboxes,
|
|
9
8
|
BubbleBbox,
|
|
@@ -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
|
|
@@ -83,11 +72,6 @@ REFERENCE_RESUME_CARD_TOP_GAP_BASELINE: int = 542
|
|
|
83
72
|
#: **相对 screencap + OpenCV**,性能可忽略。
|
|
84
73
|
PROCESSED_CARD_MATCH_MIN_OVERLAP_RATIO: float = 0.70
|
|
85
74
|
|
|
86
|
-
#: Anti-detection horizontal jitter (px @ 1080 baseline) applied by
|
|
87
|
-
#: ``enter_note`` on every tap attempt: ``tap_x = click_x ± random(0..15)``.
|
|
88
|
-
#: Y is never jittered (speaker attribution depends on exact Y).
|
|
89
|
-
TAP_ANTI_DETECT_X_JITTER_PX: int = 15
|
|
90
|
-
|
|
91
75
|
#: 3b gap 合并/延长判别距离(px @ 1080 baseline,运行时 ×scale_w):
|
|
92
76
|
#: 收藏锚点落在同一竖线相邻两 zone 的间隙时,取锚点下方第一条横线,
|
|
93
77
|
#: 横线与下方 zone 起点距离 ≤ 本值 → 两 zone 同卡(被收藏标签行打断),合并;
|
|
@@ -171,7 +155,10 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
171
155
|
bgr_img: np.ndarray,
|
|
172
156
|
screen_w: int,
|
|
173
157
|
screen_h: int,
|
|
174
|
-
) -> 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)."""
|
|
175
162
|
"""PRD S6: vline-segment + hline-boundary card + bubble bbox detection.
|
|
176
163
|
|
|
177
164
|
Steps:
|
|
@@ -253,7 +240,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
253
240
|
vd_lines = [vx for vx in vd_lines if vx - AR >= min_card_x_gap]
|
|
254
241
|
|
|
255
242
|
if not vd_lines:
|
|
256
|
-
return [], []
|
|
243
|
+
return [], [], False
|
|
257
244
|
|
|
258
245
|
# Pick best vline: max total mid pixels + longest continuous segment
|
|
259
246
|
vline_scores: dict[int, float] = {}
|
|
@@ -279,7 +266,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
279
266
|
best_vx = vx
|
|
280
267
|
|
|
281
268
|
if best_vx is None:
|
|
282
|
-
return [], []
|
|
269
|
+
return [], [], False
|
|
283
270
|
|
|
284
271
|
# Step 3 — vline segments, computed per candidate vline. A fav-anchored
|
|
285
272
|
# card may sit on a different right-edge vline than the frame-global best
|
|
@@ -311,7 +298,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
311
298
|
)
|
|
312
299
|
zones_by_vx = {vx: _zones_for(vx) for vx in vline_order}
|
|
313
300
|
if not any(zones_by_vx.values()):
|
|
314
|
-
return [], []
|
|
301
|
+
return [], [], False
|
|
315
302
|
|
|
316
303
|
# Step 4 — card boundary hlines (span > 50% card_w, X-near vline,
|
|
317
304
|
# max gray gradient >= 30 = white→gray boundary)
|
|
@@ -410,6 +397,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
410
397
|
gap_tol = max(1, int(round(GAP_HLINE_TO_NEXT_ZONE_MAX_BASELINE * sw)))
|
|
411
398
|
cards: list[ThumbnailCard] = []
|
|
412
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
|
|
413
401
|
for idx, hit in enumerate(ordered_hits):
|
|
414
402
|
fav_cy = hit.y + hit.h // 2
|
|
415
403
|
|
|
@@ -418,6 +406,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
418
406
|
zones = zones_by_vx[vx]
|
|
419
407
|
if not zones:
|
|
420
408
|
continue
|
|
409
|
+
any_anchor_seen = True # anchor entered vline loop with non-empty zones
|
|
421
410
|
card_hlines = hlines_by_vx[vx]
|
|
422
411
|
|
|
423
412
|
def _snap(y1_: int, y2_: int) -> tuple[int, int]:
|
|
@@ -502,7 +491,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
502
491
|
if exact_y2 <= exact_y1 or exact_y2 - exact_y1 < 80:
|
|
503
492
|
continue
|
|
504
493
|
|
|
505
|
-
click_x =
|
|
494
|
+
click_x = screen_w // 2
|
|
506
495
|
click_y = min(screen_h - 1, max(0, hit.y + hit.h // 2))
|
|
507
496
|
midline = screen_w / 2.0
|
|
508
497
|
click_side = "left" if click_x < midline else "right"
|
|
@@ -558,7 +547,7 @@ def _compute_exact_card_and_bubble_bboxes(
|
|
|
558
547
|
continue
|
|
559
548
|
bubbles.append(BubbleBbox(top=by1, bottom=by2, left=AR, right=int(vx)))
|
|
560
549
|
|
|
561
|
-
return cards, bubbles
|
|
550
|
+
return cards, bubbles, any_anchor_seen
|
|
562
551
|
|
|
563
552
|
|
|
564
553
|
def _compute_exact_card_bboxes(
|
|
@@ -566,13 +555,22 @@ def _compute_exact_card_bboxes(
|
|
|
566
555
|
bgr_img: np.ndarray,
|
|
567
556
|
screen_w: int,
|
|
568
557
|
screen_h: int,
|
|
569
|
-
) -> list[ThumbnailCard]:
|
|
558
|
+
) -> tuple[list[ThumbnailCard], bool]:
|
|
570
559
|
"""Backward-compat wrapper — cards only (see
|
|
571
|
-
:func:`_compute_exact_card_and_bubble_bboxes`).
|
|
572
|
-
|
|
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(
|
|
573
571
|
ordered_hits, bgr_img, screen_w, screen_h,
|
|
574
572
|
)
|
|
575
|
-
return cards
|
|
573
|
+
return cards, pipeline_active
|
|
576
574
|
|
|
577
575
|
|
|
578
576
|
def derive_cards_and_bubbles(
|
|
@@ -596,7 +594,7 @@ def derive_cards_and_bubbles(
|
|
|
596
594
|
return [], []
|
|
597
595
|
ordered = sorted(hits, key=lambda h: h.y)
|
|
598
596
|
try:
|
|
599
|
-
cards, bubbles = _compute_exact_card_and_bubble_bboxes(
|
|
597
|
+
cards, bubbles, _pipeline_active = _compute_exact_card_and_bubble_bboxes(
|
|
600
598
|
ordered, chat_img, screen_w, screen_h,
|
|
601
599
|
)
|
|
602
600
|
except Exception:
|
|
@@ -627,15 +625,15 @@ def derive_cards(
|
|
|
627
625
|
(we sort by y ourselves).
|
|
628
626
|
screen_w / screen_h: the source screenshot's raw-image dimensions
|
|
629
627
|
(usually equal to ``device_info.screen_resolution``).
|
|
630
|
-
scale_w: ``screen_w / baseline_w`` (baseline = 1080).
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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.
|
|
639
637
|
|
|
640
638
|
Returns:
|
|
641
639
|
One :class:`ThumbnailCard` per hit, sorted top -> bottom. Empty list
|
|
@@ -647,58 +645,22 @@ def derive_cards(
|
|
|
647
645
|
|
|
648
646
|
ordered = sorted(hits, key=lambda h: h.y)
|
|
649
647
|
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
|
|
654
|
-
# callers and pipeline exceptions.
|
|
655
|
-
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:
|
|
656
652
|
try:
|
|
657
|
-
exact_cards = _compute_exact_card_bboxes(
|
|
653
|
+
exact_cards, _pipeline_active = _compute_exact_card_bboxes(
|
|
658
654
|
ordered, chat_img, screen_w, screen_h,
|
|
659
655
|
)
|
|
660
656
|
except Exception:
|
|
661
|
-
|
|
657
|
+
return []
|
|
662
658
|
else:
|
|
663
659
|
return drop_top_clamped_false_positive_cards(
|
|
664
660
|
exact_cards, scale_w=scale_w,
|
|
665
661
|
)
|
|
666
662
|
|
|
667
|
-
|
|
668
|
-
top_offset = int(round(FAVORITE_TO_CARD_TOP_OFFSET_BASELINE * scale_w))
|
|
669
|
-
tail_offset = int(round(FAVORITE_TAIL_OFFSET_BASELINE * scale_w))
|
|
670
|
-
|
|
671
|
-
cards: list[ThumbnailCard] = []
|
|
672
|
-
for idx, hit in enumerate(ordered):
|
|
673
|
-
if idx == 0:
|
|
674
|
-
card_top = max(0, hit.y1 - top_offset)
|
|
675
|
-
else:
|
|
676
|
-
prev_bottom = ordered[idx - 1].y2 + tail_offset
|
|
677
|
-
card_top = max(prev_bottom + 1, 0)
|
|
678
|
-
|
|
679
|
-
card_bottom = min(screen_h - 1, hit.y2 + tail_offset)
|
|
680
|
-
|
|
681
|
-
if card_bottom <= card_top:
|
|
682
|
-
continue
|
|
683
|
-
|
|
684
|
-
click_x = min(screen_w - 1, max(0, hit.x + hit.w // 2))
|
|
685
|
-
click_y = min(screen_h - 1, max(0, hit.y + hit.h // 2))
|
|
686
|
-
midline = screen_w / 2
|
|
687
|
-
click_side = "left" if click_x < midline else "right"
|
|
688
|
-
|
|
689
|
-
cards.append(ThumbnailCard(
|
|
690
|
-
index=idx,
|
|
691
|
-
top=card_top,
|
|
692
|
-
bottom=card_bottom,
|
|
693
|
-
left=0,
|
|
694
|
-
right=screen_w - 1,
|
|
695
|
-
click_x=click_x,
|
|
696
|
-
click_y=click_y,
|
|
697
|
-
click_side=click_side,
|
|
698
|
-
favorite_hit=hit,
|
|
699
|
-
))
|
|
700
|
-
|
|
701
|
-
return drop_top_clamped_false_positive_cards(cards, scale_w=scale_w)
|
|
663
|
+
return []
|
|
702
664
|
|
|
703
665
|
|
|
704
666
|
def drop_top_clamped_false_positive_cards(
|
|
@@ -876,12 +838,32 @@ def _card_height(card: ThumbnailCard) -> int:
|
|
|
876
838
|
return card.bottom - card.top + 1
|
|
877
839
|
|
|
878
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
|
+
|
|
879
859
|
def _longest_subsequence_match(
|
|
880
860
|
a_heights: list[int],
|
|
881
861
|
b_heights: list[int],
|
|
882
862
|
) -> tuple[int, int, int] | None:
|
|
883
863
|
"""Find the longest contiguous substring of a_heights that appears in b_heights.
|
|
884
864
|
|
|
865
|
+
Matching uses ± ``_CARD_HEIGHT_TOLERANCE`` px per element (PRD §10 第二步 容差)。
|
|
866
|
+
|
|
885
867
|
Returns ``(a_start, b_start, length)`` or ``None``.
|
|
886
868
|
If multiple matches share the same maximum length, picks the one with
|
|
887
869
|
the smallest ``b_start`` (earliest in B).
|
|
@@ -896,9 +878,8 @@ def _longest_subsequence_match(
|
|
|
896
878
|
for length in range(max_len, 0, -1):
|
|
897
879
|
if best is not None and length < best[2]:
|
|
898
880
|
break
|
|
899
|
-
sub = a_heights[a_start:a_start + length]
|
|
900
881
|
for b_start in range(len(b_heights) - length + 1):
|
|
901
|
-
if b_heights
|
|
882
|
+
if _heights_match(a_heights, a_start, b_heights, b_start, length):
|
|
902
883
|
if (
|
|
903
884
|
best is None
|
|
904
885
|
or length > best[2]
|
|
@@ -973,13 +954,10 @@ def pick_next_unclicked_card(
|
|
|
973
954
|
__all__ = [
|
|
974
955
|
"FAVORITE_LABEL_TEMPLATE_W_BASELINE",
|
|
975
956
|
"FAVORITE_LABEL_TEMPLATE_H_BASELINE",
|
|
976
|
-
"FAVORITE_TO_CARD_TOP_OFFSET_BASELINE",
|
|
977
|
-
"FAVORITE_TAIL_OFFSET_BASELINE",
|
|
978
957
|
"FAV_LABEL_HLINE_EXCLUDE_PAD_BASELINE",
|
|
979
958
|
"GAP_HLINE_TO_NEXT_ZONE_MAX_BASELINE",
|
|
980
959
|
"PROCESSED_CARD_MATCH_MIN_OVERLAP_RATIO",
|
|
981
960
|
"REFERENCE_RESUME_CARD_TOP_GAP_BASELINE",
|
|
982
|
-
"TAP_ANTI_DETECT_X_JITTER_PX",
|
|
983
961
|
"FavoriteLabelHit",
|
|
984
962
|
"ThumbnailCard",
|
|
985
963
|
"TrackedCard",
|
|
@@ -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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: screenshot_vision_algorithm
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
File without changes
|
|
File without changes
|
{screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/tests/test_card_bbox.py
RENAMED
|
File without changes
|
{screenshot_vision_algorithm-0.3.1 → screenshot_vision_algorithm-0.3.5}/tests/test_package.py
RENAMED
|
File without changes
|