Metadata-Version: 2.4
Name: wechat_screenshot_vision_algorithm
Version: 0.2.2
Summary: WeChat screenshot computer-vision algorithms: card bbox, speaker bands, avatar detection, OCR helpers
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: loguru>=0.7
Requires-Dist: numpy>=1.26
Requires-Dist: opencv-python-headless>=4.10.0
Provides-Extra: png
Requires-Dist: Pillow>=10.0; extra == "png"
Provides-Extra: ocr
Requires-Dist: paddleocr>=3.0; extra == "ocr"
Provides-Extra: merge
Requires-Dist: python-Levenshtein>=0.25; extra == "merge"

# wechat_screenshot_vision_algorithm

微信截图计算机视觉算法库 —— 简历卡片检测、发言人气泡分割、头像检测、OCR 辅助等。

## 安装

```bash
pip install wechat_screenshot_vision_algorithm

# 可选依赖
pip install wechat_screenshot_vision_algorithm[ocr]    # PaddleOCR >=3.0
pip install wechat_screenshot_vision_algorithm[png]    # Pillow PNG 压缩上传
pip install wechat_screenshot_vision_algorithm[merge]  # python-Levenshtein 多页文本合并
```

## 包结构

```
src/wechat_screenshot_vision_algorithm/
├── _config.py        # Platform/Profile 解析与参数查表
├── png_utils.py      # extras=[png]: PNG 长边压缩
├── algorithms/       # 默认依赖 (numpy + opencv)
│   ├── card_bbox.py          # 简历卡片 bbox 检测
│   ├── template_matching.py  # 模板匹配原语 (收藏标签/未读分割线/标题栏等)
│   ├── speaker_band.py       # 发言人气泡纵向分桶
│   ├── avatar_column.py      # 左侧头像列布局检测
│   ├── phash_utils.py        # dHash 感知哈希 (滚动到底检测 / 去重)
│   ├── badge_detection.py    # 未读红点 HSV 检测
│   └── title_ocr.py          # 群聊标题 OCR
├── ocr/              # extras=[ocr]: PaddleOCR ≥3.0
│   ├── text_ocr_adapter.py   # PaddleOCR 引擎封装 (UVDoc 已禁用)
│   ├── nickname_binding.py   # 昵称归属 (卡片/聊天气泡绑定)
│   ├── avatar_guard.py       # 昵称 vs 头像距离守门
│   └── badge_ocr.py          # 未读徽章数字 OCR
├── merge/            # extras=[merge]: python-Levenshtein
│   └── multipage.py          # N-gram + Levenshtein 多页拼接
├── profiles/         # 平台参数配置
│   ├── android_wechat.py     # 安卓微信 (头像左侧, 全部阈值)
│   ├── ios_wechat.py         # iOS 微信 (头像右侧, 占位)
│   └── harmony_wechat.py     # 鸿蒙微信 (占位)
└── templates/        # package_data (pip install 分发)
    └── wechat/{platform}/{version}/
```

## 核心概念

### Platform / WeChatVersion / Profile

不同平台 (Android / iOS / 鸿蒙) 和微信版本的截图布局不同（头像列位置、颜色阈值、间距等），通过 `Profile` 统一参数化：

```python
from wechat_screenshot_vision_algorithm import Platform, WeChatVersion, Profile

# 安卓微信 8.0.69
profile = Profile(platform=Platform.ANDROID, wechat_version=WeChatVersion.V8_0_69)

profile.AVATAR_SIDE         # "left" — Android 发言人头像在左侧
profile.CORE_THRESHOLD      # 0.80 — 模板匹配阈值
profile.VLINE_MIN_SEG_RATIO # 0.08 — 竖线候选列最低方差段占比
profile.templates_dir       # Path — 内嵌模板 PNG 目录
```

所有设计常量基于 1080px 屏幕宽度锚定；运行时按 `device_screen_width / 1080` 缩放。

---

## 模块详解

### `_config` — 配置与 Profile 解析

| 名称 | 类型 | 说明 |
|------|------|------|
| `Platform` | Enum | `ANDROID`, `IOS`, `HARMONY` 三平台枚举 |
| `WeChatVersion` | Enum | `V8_0_69` 微信版本枚举 |
| `Profile` | Class | 参数束：按 `(platform, version)` 查表返回阈值常量与模板路径 |
| `BASELINE_WIDTH` | int | 设计基准宽度 `1080`px |

`Profile` 实例的属性委托到对应平台模块（如 `profiles/android_wechat.py`），包含以下参数类：

**卡片 bbox 检测参数：** `TOP_BAR_BOT_RATIO`, `BOT_BAR_TOP_RATIO`, `AVATAR_COLUMN_WIDTH_BASELINE`, `MIN_CARD_X_GAP_BASELINE`, `VLINE_MIN_SEG_RATIO`, `CARD_HSPAN_MIN_RATIO`, `HLINE_NEAR_VLINE_LIMIT_BASELINE`, `ZONE_MIN_HEIGHT`, `ZONE_MIN_SEG`, `LABEL_INTERSECT_MARGIN`, `GAP_MERGE_MAX_DIST`

**模板匹配参数：** `CORE_THRESHOLD`, `AUX_THRESHOLD`

**发言人气泡参数：** `CHAT_FIRST_BAND_TOP_EXTEND_BASELINE`, `CHAT_COMPOSER_RESERVE_BOTTOM_BASELINE`, 头像 Hough 圆参数等

**头像检测参数：** `LIST_HOUGH_MIN_DIST_BASELINE`, `LIST_HOUGH_MIN_R_BASELINE`, `NICKNAME_AVATAR_BIND_MAX_DY_BASELINE` 等

**卡片点击参数：** `FAVORITE_LABEL_TEMPLATE_W_BASELINE`, `FAVORITE_TO_CARD_TOP_OFFSET_BASELINE`, `REFERENCE_RESUME_CARD_TOP_GAP_BASELINE` 等

---

### `algorithms.card_bbox` — 简历卡片边界框检测

微信聊天列表截图中检测「收藏标签」并推导简历卡片精确 bbox。核心入口 `_compute_exact_card_bboxes` 基于三区方差 + 竖线分段判断卡片左右边界。

**数据结构：**

| 名称 | 字段 | 说明 |
|------|------|------|
| `FavoriteLabelHit` | `x, y, w, h, score` | 收藏标签模板匹配命中 (`y1`, `y2` 属性) |
| `ThumbnailCard` | `index, top, bottom, left, right, click_x, click_y, click_side, favorite_hit` | 一张简历卡片的导出矩形（像素坐标）|
| `BubbleBbox` | `top, bottom, left, right` | 文本聊天气泡矩形 (卡片 zone 排除后的剩余 zone) |
| `TrackedCard` | `height, clicked` | 高度序列去重追踪状态 |

**函数：**

| 函数 | 签名 | 说明 |
|------|------|------|
| `_compute_exact_card_bboxes` | `(ordered_hits, bgr_img, screen_w, screen_h) → list[ThumbnailCard]` | 核心算法：从收藏标签命中 + 截图推导卡片 bbox |
| `derive_cards` | `(...) → list[ThumbnailCard]` | 高层入口：整合模板匹配 + 竖线检测，输出卡片列表 |
| `drop_top_clamped_false_positive_cards` | `(cards) → list[ThumbnailCard]` | 过滤顶部被截断的误检卡片 |
| `bbox_to_metadata_list` | `(card) → list[int]` | 卡片 bbox 转为 `[left, top, right, bottom]` |
| `card_bounding_tuple` | `(card) → tuple` | 转为 `(left, top, right, bottom)` 元组 |
| `card_overlaps_processed` | `(card, processed_bboxes) → bool` | 判断卡片是否与已处理卡片重叠 |
| `click_context_for_tap_thumbnail` | `(card) → dict` | 生成 `click_context` (click_coords + click_side) |
| `pick_first_unprocessed_card` | `(cards, processed_bboxes) → ThumbnailCard\|None` | 返回第一个不重叠的未处理卡片 (旧版，建议用 `pick_next_unclicked_card`) |
| `pick_next_unclicked_card` | `(cards, tracked_cards) → ThumbnailCard\|None` | 基于高度序列匹配的卡片去重，返回下一个未点击卡片 |
| `y_interval_overlap_ratio` | `(top_a, bottom_a, top_b, bottom_b) → float` | 两区间的 Y 轴重叠率 |

---

### `algorithms.template_matching` — 模板匹配原语

所有模板匹配函数 + 布局常量，跨 collector 运行时和回归测试工具共享。

**常量：**

| 常量 | 说明 |
|------|------|
| `BASELINE_WIDTH` | 基准屏幕宽度 `1080` |
| `CORE_THRESHOLD` | 收藏标签/新消息提示的硬阈值 `0.80` |
| `TEMPLATES_DIR` | 模板 PNG 目录 (可通过 `set_templates_dir` 覆盖) |
| `TEMPLATE_FILE` | 模板名到文件名的映射 dict |

**函数：**

| 函数 | 说明 |
|------|------|
| `load_template(name)` | 加载模板 PNG 返回 BGR numpy 数组 |
| `set_templates_dir(path)` | 覆盖模板目录（iOS / 鸿蒙使用）|
| `is_wechat_main_conversation_list_chrome(...)` | 检测是否为微信主会话列表页面 |
| `find_favorite_labels(...)` | 在截图中匹配收藏标签 |
| `find_new_messages_hints(...)` | 匹配「新消息」分割线 |
| `find_unread_dividers(...)` | 匹配未读消息分割线 |
| `find_wechat_note_header(...)` | 匹配微信笔记标题栏 |
| `find_chat_back_chevron(...)` | 匹配聊天返回箭头 |
| `find_chat_title_more_dots(...)` | 匹配聊天标题栏更多按钮 |
| `find_chat_input_voice(...)` | 匹配语音输入按钮 |
| `find_chat_input_emoji_smile(...)` | 匹配表情按钮 |
| `find_chat_input_plus(...)` | 匹配加号按钮 |
| `split_conversation_rows(...)` | 按头像分割会话列表行 |

---

### `algorithms.speaker_band` — 发言人气泡纵向分桶

根据聊天截图中的发言人头像位置，划分发言人纵向区间（speaker bands）。

**数据结构：**

| 名称 | 说明 |
|------|------|
| `ChatVerticalBounds` | 聊天区域纵向边界 (top / bottom / title_bar_bottom / composer_top) |
| `AvatarTimelineEntry` | 单条发言人头像时间线条目 |
| `PrdChatVerticalLayout` | PRD 规定的聊天纵向布局 (bounds + speaker_bands) |

**函数：**

| 函数 | 说明 |
|------|------|
| `compute_chat_content_vertical_bounds(...)` | 计算聊天内容的纵向边界 |
| `detect_chat_side_avatar_ytops(...)` | 检测聊天侧发言人头像 Y 坐标 |
| `merge_avatar_ytops_time_order(...)` | 按时间顺序合并头像 Y 坐标 |
| `build_prd_speaker_vertical_bands(...)` | 构建 PRD 规定的发言人纵向分桶 |

---

### `algorithms.avatar_column` — 左侧头像列布局检测

检测微信会话列表中左侧头像列的布局（数量、Y 坐标等）。

**数据结构：**

| 名称 | 说明 |
|------|------|
| `AvatarCentroid` | 单个头像质心 (x, y, radius) |
| `LeftAvatarColumnLayout` | 左侧头像列布局 (centroids, median_x, gaps 等) |

**函数：**

| 函数 | 说明 |
|------|------|
| `detect_left_avatar_column_layout(...)` | 用 Hough 圆检测左侧头像列 |
| `find_avatar_anchor_for_nickname_bbox(...)` | 为昵称 bbox 找到对应的发言人头像锚点 |
| `nickname_bbox_in_avatar_column(...)` | 判断昵称 bbox 是否在头像列区域内 |
| `left_avatar_ytops(layout)` | 提取所有头像的 Y 坐标列表 |

---

### `algorithms.phash_utils` — 感知哈希与去重

dHash 感知哈希，用于滚动到底检测与会话去重。

**数据结构：**

| 名称 | 说明 |
|------|------|
| `PHashResult` | 哈希结果 (value, roi_kind, bbox) |
| `ScrollDuplicationTracker` | 滚动去重追踪器 (observe → is_stuck) |

**函数：**

| 函数 | 说明 |
|------|------|
| `compute_dhash(bgr, roi_kind)` | 计算截图的 dHash |
| `hamming_distance(a, b)` | 两个哈希的汉明距离 |
| `is_duplicate(ph1, ph2, threshold)` | 判断两帧是否重复 |
| `crop_roi(bgr, roi_kind)` | 根据 roi_kind 裁剪 ROI |
| `manual_labeling_phash_matches(a, b)` | 判定手动标注阶段是否匹配 |

---

### `algorithms.badge_detection` — 未读红点检测

HSV 颜色空间检测微信会话列表中白底红字的未读徽章。

**函数：**

| 函数 | 说明 |
|------|------|
| `count_badges_white_on_red(bgr, min_area_px)` | 检测全图白底红字徽章数量 |
| `count_badges_per_row_roi(bgr, avatar_ytops, ...)` | 按行 ROI 统计徽章数 |

---

### `algorithms.title_ocr` — 群聊标题 OCR

**函数：**

| 函数 | 说明 |
|------|------|
| `try_ocr_group_chat_title_from_png(png_path, scale_w)` | 从 PNG 截图 OCR 群聊标题文本 |

---

### `ocr.text_ocr_adapter` — PaddleOCR 引擎封装

> **依赖：** `pip install wechat_screenshot_vision_algorithm[ocr]`

PaddleOCR 3.x 引擎封装，已**显式禁用 UVDoc 矫正**（防止坐标变形）。跨 collector 和 housekeeping 后端共享。

**数据结构：**

| 名称 | 说明 |
|------|------|
| `OcrPageResult` | OCR 页面结果 (blocks, page_size, engine_info) |
| `TextBlock` | 单个 OCR 文本块 (text, bbox_xyxy, confidence) |

---

### `ocr.nickname_binding` — 昵称归属

> **依赖：** `pip install wechat_screenshot_vision_algorithm[ocr]`

将 OCR 识别出的文本块归因为「昵称」「正文」「丢弃」三类，并绑定到发言人或简历卡片。

**数据结构：**

| 名称 | 说明 |
|------|------|
| `NicknameOcrConfig` | 昵称 OCR 配置 (阈值、avatar_column_width 等) |
| `NicknameExtraction` | 单条昵称提取结果 |
| `SpeakerBodySegment` | 发言人正文段落 |
| `ResumeThumbBinding` | 简历卡片绑定结果 |
| `ChatSpeakerAttribution` | 完整发言人归属结果 |

**函数：**

| 函数 | 说明 |
|------|------|
| `extract_nicknames(...)` | **主入口**：从 OCR 页面结果提取昵称并归属 |
| `first_attrib_verbatim_display_line(...)` | 获取首次属性的原始显示行 |

---

### `ocr.avatar_guard` — 头像距离守门

> **依赖：** `pip install wechat_screenshot_vision_algorithm[ocr]`

验证 OCR 识别出的昵称文本块与对应发言人头像的距离是否合理。通过拉普拉斯方差、颜色均值、边缘形状等多重信号判断。

**函数：**

| 函数 | 说明 |
|------|------|
| `nickname_row_passes_prd_avatar_guard(...)` | PRD 规定的完整头像守门检查 |
| `nickname_row_passes_avatar_roi(...)` | 头像 ROI 区域通过性检查 |
| `avatar_roi_pass(...)` | 头像 ROI 多重信号判断 |
| `nickname_left_roi_passes_avatar_signal(...)` | 昵称左侧 ROI 的头像信号检测 |

---

### `ocr.badge_ocr` — 未读数字 OCR

> **依赖：** `pip install wechat_screenshot_vision_algorithm[ocr]`

对检测到的未读红点区域进行数字 OCR，返回未读计数。

**函数：**

| 函数 | 说明 |
|------|------|
| `ocr_unread_badge_digits(bgr, badge_hits)` | 对未读徽章区域进行数字 OCR，返回 `{index: digit}` |

---

### `merge.multipage` — 多页文本合并

> **依赖：** `pip install wechat_screenshot_vision_algorithm[merge]`

将同一简历的多页 OCR 文本合并为完整简历文本。使用 N-gram 重叠检测 + Levenshtein 编辑距离相似度。

**函数：**

| 函数 | 说明 |
|------|------|
| `merge_multipage_ocr_texts(pages)` | 合并多页 OCR 文本，返回完整简历字符串 |
| `paragraph_content_bounds(merged, anchor)` | 在合并文本中查找锚点所在的段落边界 |

---

### `png_utils` — PNG 压缩上传

> **依赖：** `pip install wechat_screenshot_vision_algorithm[png]`

将截图 PNG 压缩到指定长边尺寸，用于上传前预处理。

**函数：**

| 函数 | 说明 |
|------|------|
| `resolved_collector_upload_long_edge_max(argparse_value)` | 解析上传 PNG 长边最大像素数（环境变量 `COLLECTOR_UPLOAD_LONG_EDGE_MAX` / 默认 1920） |
| `maybe_resize_png_long_edge(png_bytes, long_edge_max)` | 如 PNG 长边超过限制则等比缩小 |

---

## 使用示例

```python
from wechat_screenshot_vision_algorithm import Platform, WeChatVersion, Profile

# 1. 按平台选择 Profile
profile = Profile(platform=Platform.ANDROID, wechat_version=WeChatVersion.V8_0_69)

# 2. 模板匹配
from wechat_screenshot_vision_algorithm.algorithms.template_matching import (
    load_template, find_favorite_labels,
)
import cv2
screenshot = cv2.imread("chat_screenshot.png")
favorite_labels = find_favorite_labels(screenshot, scale_w=screenshot.shape[1] / profile.BASELINE_WIDTH)

# 3. 简历卡片检测
from wechat_screenshot_vision_algorithm.algorithms.card_bbox import (
    _compute_exact_card_bboxes, pick_next_unclicked_card,
)
cards = _compute_exact_card_bboxes(favorite_labels, screenshot,
                                    screen_w=screenshot.shape[1],
                                    screen_h=screenshot.shape[0])
print(f"检测到 {len(cards)} 张简历卡片")

# 4. 发言人分桶
from wechat_screenshot_vision_algorithm.algorithms.speaker_band import (
    compute_chat_content_vertical_bounds, build_prd_speaker_vertical_bands,
)
bounds = compute_chat_content_vertical_bounds(screenshot, scale_w=1.0)
layout = build_prd_speaker_vertical_bands(screenshot, bounds, scale_w=1.0)

# 5. OCR 昵称提取 (需 pip install wechat_screenshot_vision_algorithm[ocr])
from wechat_screenshot_vision_algorithm.ocr.text_ocr_adapter import OcrPageResult
from wechat_screenshot_vision_algorithm.ocr.nickname_binding import extract_nicknames, NicknameOcrConfig
# ... 初始化 PaddleOCR 并调用 extract_nicknames(ocr_result, config)
```

## 开发指南

### 添加新平台支持

1. 在 `profiles/` 下创建新文件（如 `ios_wechat.py`），复制 `android_wechat.py` 并修改阈值
2. 将模板 PNG 放入 `templates/wechat/{platform}/{version}/`
3. 在 `_config.py` 的 `_PROFILE_MODULES` 中注册

### 本地开发安装

```bash
git clone https://github.com/yuyidream/wechat_screenshot_vision_algorithm.git
cd wechat_screenshot_vision_algorithm
pip install -e .            # 默认依赖
pip install -e ".[ocr,png,merge]"  # 全部可选依赖
```

### 运行测试

```bash
pytest tests/ -v
```
