README_操作方法.md 25 KB

宝可梦卡牌识别 · 操作方法(每一步怎么做)

面向接手的开发/运维。照着做即可。资产清单与落位见 README_上线清单.md,接口契约见 docs/宝可梦卡牌识别服务文档接口.md。 本文所有脚本路径以包内 code/ 为基准;部署到服务器后即框架根 ~/顾工交接/wzj/。 每条结论都标了 脚本:行,可自行核对。


零、先看这张图:当前自动化识别方案的链式流程

在线(生产,一次请求 ~0.5s,全程在内存里跑完,无中间文件):

                          HTTP 请求 (image_url / multipart image)
                                     │
                    ┌────────────────┴────────────────┐
              :8010 /recognize                  :8000 /match
        serve_recognition_api.py            serve_card_match.py
                    └────────────────┬────────────────┘
                                     │  两者都调同一个匹配器
                                     ▼
                    image_downloader.download_card_image()
                    下载到 data/query_images/<md5(url)>.jpg
                    ★用完即删 (serve_recognition_api.py:162-166)
                                     │
                                     ▼
                    DualCascadeMatcher.query()   ← scripts/cascade_match.py:150
                                     │
        ┌────────────────────────────┴───────────────────────────┐
        │ ① 裁卡  CardDetector.detect_and_crop()                  │  模型①
        │    modules/yolo_detector.py:108                         │  card_seg_pn_v2_best.pt
        │    seg_warp: mask → 4 角点 → warp_perspective → 竖放     │  (yolo26s-seg)
        │    输出:摆正的卡面 RGB 数组(可变尺寸,不落盘)          │
        ├─────────────────────────────────────────────────────────┤
        │ ② letterbox392()   feature_extractor_dual.py:30          │
        │    BICUBIC 等比缩放 + 黑边填充 → 392×392                 │
        ├─────────────────────────────────────────────────────────┤
        │ ③ split_half()     feature_extractor_dual.py:43          │
        │    上半区 crop(0,0,392,196) │ 下半区 crop(0,196,392,392) │
        │    各 196×392                                            │
        ├──────────────────────────┬──────────────────────────────┤
        │ ④上半区 extract_upper()  │ ⑤下半区 extract_lower()      │  模型②③
        │  dinov2-large → CLS      │  dinov2-large → CLS          │  best_upper_half.pth
        │  → L2 归一化 → (1024,)   │  → L2 归一化 → (1024,)        │  best_layer3_bottom.pth
        │  feature_extractor_dual.py:137 (F.normalize)             │
        ├──────────────────────────┼──────────────────────────────┤
        │ ⑥ 全库召回(物种级)      │ ⑦ 候选重排(版本级)          │
        │  gallery_upper @ q_u     │  gallery_lower[cand] @ q_l    │
        │  argpartition 取 K=30    │  只对这 30 个算               │
        │  cascade_match.py:105-112│  cascade_match.py:110         │
        └──────────────────────────┴──────────────────────────────┘
                                     │
                                     ▼
        ⑧ 融合重排  fusion = α·upper + (1-α)·lower    α=0.5
           cascade_match.py:142
                                     │
                                     ▼
        返回 {predicted_card_id, fusion_score, upper_sim, lower_sim, top_k[]}
             cascade_match.py:157-164
                    │                               │
          8010 换算 match_rate=round(f×100)   8000 拿 card_id 查
          输出 matches[]                       card_master_fields.csv 出业务字段

为什么要分上下两区:上半区(标题+插画)决定"是哪只精灵",下半区(技能框+卡号+稀有度符号+闪卡反光层)决定"哪个版本"。一个模型学不好两种维度,所以按卡牌版式上下各切 50%,各训一个专用模型,先召回再消歧。

旁挂件(可选,不影响主链):8100 serve_lang_judge.py 出语种/方向。★注意它当前不参与检索——serve_recognition_api.py:18-19 注释明确写"仅记 language 到日志,不参与检索前置",recognize() 函数体里没有任何 language 消歧逻辑。所以它挂了对准确率无影响。

离线(建库/批量/其它子系统):见第三、四、五节,这些是有中间文件落盘的多步链路。


一、日常操作:启动在线服务

1.1 三个进程(各自独立,按需起)

cd ~/顾工交接/wzj
mkdir -p logs

# ① OCR sidecar(paddleocr 环境)—— 可选,8010 的旁挂件
CUDA_VISIBLE_DEVICES=1 setsid nohup \
  ~/miniconda3/envs/paddleocr/bin/python serve_lang_judge.py --port 8100 \
  </dev/null >logs/serve_8100.log 2>&1 &

# ② 主识别接口(pytorch 环境)—— 后端调这个
CUDA_VISIBLE_DEVICES=1 setsid nohup \
  ~/miniconda3/envs/pytorch/bin/python serve_recognition_api.py --port 8010 \
  </dev/null >logs/serve_8010.log 2>&1 &

# ③ 业务字段接口(pytorch 环境)—— 小程序/后端要卡名卡号时起
CUDA_VISIBLE_DEVICES=1 setsid nohup \
  ~/miniconda3/envs/pytorch/bin/python serve_card_match.py --port 8000 \
  </dev/null >logs/serve_8000.log 2>&1 &

1.2 启动时到底加载了什么(cascade_match.py:50-86

加载项 文件 npy 后端 milvus 后端
图库元数据 data/gallery_dual_meta.json ✅ 读 ✅ 读(仍要读,取 card_ids/metas)
上半区特征 data/gallery_upper_features.npy np.load ❌ 不读
下半区特征 data/gallery_lower_features.npy np.load ❌ 不读
Milvus 连接 192.168.77.249:19530 ✅ 连两个 collection
裁卡模型 card_seg_pn_v2/weights/best.pt
上/下半区模型 两个 .pth(各 1.2G)
业务字段表 data/card_master_fields.csv 8000/8010 各自单独加载

切后端:config.py:103CASCADE_BACKENDgpu / milvus),或命令行 --backend249 现役 gpu(特征库 fp32 常驻显存,召回 0.4ms/卡);milvus 为回滚兜底。

1.3 验证(两步都要做)

curl http://127.0.0.1:8010/health
# 必须看到 gallery_size: 91116。若是 88384/78272 → 权重/图库混版,立刻停服。

# ★ /health 会「假 ok」,必须再打一次真实请求
curl -X POST http://127.0.0.1:8010/recognize -H 'Content-Type: application/json' \
  -d '{"task_id":"smoke-1","image_url":"https://illustration-1309648802.cos.ap-guangzhou.myqcloud.com/ai_image/04101495/synthesis_result_1781513293_8eb1dce1-e93b-4c78-90f1-16c9d7251498.jpg","top_k":3}'
# 预期 card_id=04101495、match_rate≈96

1.4 会在磁盘上留下什么

产物 路径 说明
下载的查询图 data/query_images/<md5(url)>.jpg 用完即删(两个 serve 的 finally 块)
8000 的调用历史 data/match_results_history.json serve_card_match.py:108-124 持续追加,会一直长大,定期清
日志 logs/serve_{8000,8010,8100}.log

二、离线批量匹配(要跑一批图出表格时)

单图 / 小批量抽查 —— code/scripts/cascade_match.py

cd ~/顾工交接/wzj
CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python scripts/cascade_match.py --image <单图路径>
CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python scripts/cascade_match.py --image-dir <目录>
CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python scripts/cascade_match.py --self-test 500
  • --image / --image-dir:结果只打到 stdout,不落文件。
  • --self-test N:抽 N 张库图自检索,Top-1 应命中自己 → 报告写 data/cascade_test_report_self_test.json
  • 通用参数:--alpha / --top-k-recall / --backend

批量出表 —— code/scripts/cascade_match_batch.py(生产批量入口):

CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python scripts/cascade_match_batch.py \
  --image-dir <图目录> --out-csv <输出.csv> --top-n 5
  • 输入:目录下 *.jpg/jpeg/png/webp
  • 输出①:--out-csv 指定的 CSV,列 = id, predicted_card_id, card_name_ch, series, language, year, master_card_no, fusion, upper_sim, lower_sim, high_conf, top5high_conf = fusion ≥ SIMILARITY_THRESHOLD
  • 输出②:<out_csv 同名>.topk.json,每张图的完整 Top-K 明细

⚠️ 用户要求推理结果只存 .xlsx 不要 csv。这个脚本原生出 CSV,跑完请转成 xlsx 并删掉 csv(或改脚本用 to_excel)。


三、图库重建(★这里有最大的坑,务必看完)

3.1 建库脚本

现役脚本code/build_dual_gallery_v0904.py

cd ~/顾工交接/wzj
CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python build_dual_gallery_v0904.py
#   --limit N   仅处理前 N 条(冒烟用,默认 0=全量)
#   --batch N   批大小(默认 64)
输入·卡列表 data/train_meta_v0904.json["records"] 已随包提供(91,116 条,fname=record.id.ext 新命名)
输入·图片 data/gallery_images/<fname> 不随包提供(几十 GB),需另外准备
输出目录 data/gallery_v0904/ —— config.py:96-98 直接指向这里,建完无需搬主路径
输出文件 gallery_upper_features.npy / gallery_lower_features.npy / gallery_dual_meta.json
预处理入口 import modules.feature_extractor_dual(与在线查询同一份代码
断点续跑 。中断只能从头再来(全量约 85 min)
容错 无图 → n_noimg++ 跳过;裁切/特征失败 → n_fail++ 跳过;NaN 特征剔除
自检 结束时 assert 上下两库形状一致(行对齐)

为什么建库和查询必须同一个预处理入口:库特征和查询特征要在同一个向量空间里做点积才有意义。letterbox392split_half → ImageNet 归一化 → CLS → L2,任何一环不一致,相似度就不可比。所以两侧都只走 modules/feature_extractor_dual.py,不要另写预处理。

3.3 无需搬库(v0904 起)

v0904 建库输出在 data/gallery_v0904/config.py 就直接指向该子目录:96-98),建完重启服务即生效。 (v819 时代"输出 gallery_v819/ 再手工搬主路径"的步骤已废除;旧主路径 npy 若存在属 v819 回滚资产,勿混用。)

3.4 校验(通用自检索)

CUDA_VISIBLE_DEVICES=1 ~/miniconda3/envs/pytorch/bin/python scripts/cascade_match.py --self-test 200

抽 N 张库图从原图重跑完整链路,Top-1 应命中自己;报告写 data/cascade_test_report_self_test.json。 (读的是 config 现役库,即 gallery_v0904/;旧 scripts/_verify_gallery_v819.py 硬编码 v819 路径,仅回滚场景使用。)

3.5 Milvus 重灌(仅 CASCADE_BACKEND="milvus" 回滚场景需要)

python scripts/_milvus_rebuild_v819.py --dry-run   # 先只校验不写
python scripts/_milvus_rebuild_v819.py --drop      # 确认后 drop 重建

⚠️ 该脚本读 data/gallery_v819/是 v819 库的回滚工具。v819 库文件已于 2026-09-07 从服务器删除, 备份在本地 宝可梦\249备份\;回滚 = 先传回五件(权重×2→upper_model_output_v0904 同级的 upper_model_output//layer3_bg_model_output/,npy+meta→data/ 主路径)→ 恢复旧 config → 重启。

python scripts/_milvus_rebuild_v819.py --dry-run   # 先只校验不写
python scripts/_milvus_rebuild_v819.py --drop      # 确认后 drop 重建
目标 192.168.77.249:19530:21
Collection pokemon_dual_upper_dinov2l_1024 / pokemon_dual_lower_dinov2l_1024:27-28),dim=1024,metric=COSINE
读取源 data/gallery_v819/ 下的 npy + meta(:22-25)——v819 库文件已删,先从 宝可梦\249备份\ 传回
安全机制 PROTECTED 黑名单:32-34):pokemon_cards(旧768维) / vit_base_pokemon(别的项目) 禁止操作;只有上面两个 collection 在白名单里,动别的直接 raise RuntimeError
灌前校验 行数一致、维度=1024、card_id 唯一且非空且 ≤32 字符、向量已归一化
写入 BATCH=10000 分批 insert → flush → create_index → load

3.6 增量加卡(★没有现成脚本)

build_dual_gallery_v0904.py 只能全量重建,没有 append 模式。要只加新卡不重建,需要自己改:读现有 npy → np.vstack 新特征 → 合并 meta 的 card_ids/metas必须保持行对齐)→ 覆盖保存(gpu 后端保存后重启服务即生效;Milvus 兜底侧无增量入口,只有 --drop 全灌)。

好消息是:加卡不需要重训模型(模型与图库解耦)。只有真重训才必须"权重 + 特征库(+ Milvus 兜底)整套同步换"。


四、card_no 编号识别线

{IN_DIR}/*.jpg ──①──► {OUT_DIR}/card_no_crops/*.jpg ──②──► {OUT_DIR}/card_no_ocr.json
                  flat_card_no_detect    + card_no_crop_records.json    flat_card_no_ocr
                  (pytorch)                                            (paddleocr37)
                  模型:card_seg_pn_v2 + yolo26s_card_no                 PP-OCRv6_medium_rec
脚本 环境 模型 输出
pokemon/flat_card_no_detect.py pytorch card_seg_pn_v2(裁卡)+ yolo26s_card_no(检编号框,:29-30 card_no_crops/ 裁剪图(文件名带 conf 前缀)+ card_no_crop_records.json
pokemon/flat_card_no_ocr.py paddleocr37 PP-OCRv6_medium_rec仅 rec,不用 det:33engine=transformers, device=cpu card_no_ocr.jsonrec_text / card_no / leniency_applied / empty_reason / rec_score / det_conf
  • MARGIN 环境变量(默认 0.05,:17)控制编号框外扩幅度。
  • 宽容判定:modules/card_no_leniency.pyfinalize_card_no()flat_card_no_ocr.py:61 被调用,单次宽容后仍不合规就置空,并用 empty_reason 区分 无编号(rec_empty) vs 模糊不可读(blur_unreadable)
  • pokemon/flat_card_no.py 是"检测+可视化"的集成示例版(出 vis_num/ + labels_num/),不出 OCR 结果。
  • parse_card_no.py已归档的老方案(固定 ROI seq_left/seq_right + det+rec + 五格式猜号),2026-08-11 起废弃,留作历史参照。

五、语种 / 方向判断

两个版本、同一套判定规则(ocr_judge.py:76-95serve_lang_judge.py:90-106 逻辑一致):

版本 脚本 用法 输出
批处理 yolo_crop.py(pytorch,裁卡) → ocr_judge.py(paddleocr,判定) 离线跑一批 language_labels_320.json{language, angle, detail, text}
服务 serve_lang_judge.py :8100 常驻,8010 内部调 同结构 JSON

规则:① 假名 ≥10 → tcg jp;② 汉字×3 > 拉丁字母 → 中文,再用 hanzidentifier 投票判 简中/繁中;③ 否则 → tcg us

必须拆两个 conda 环境:裁卡要 ultralytics(pytorch 环境),OCR 要 paddleocr==3.2.0 + paddlepaddle 3.2.0,两套包互不兼容、无法共存。

★再强调一次:语种当前没有参与在线检索serve_recognition_api.py:18-19)。


六、cat16 三属性(rarity / element)—— 不在本包内

这条线的脚本在 D:\顾工交接\wzj\ebay_入库\完整流程\ebay_sync\D:\顾工交接\wzj\ebay_sync\没有随本包交付(属 ebay 入库子系统)。模型权重在包内 models/04_cat16三属性与编号/,需要时按下面的流程接:

infer_cat16_pipeline.py --ocr-dir roi_ocr_A     (pytorch)
   裁卡 → 透视矫正 → 切 box1/box2(稀有度) box3(属性) ROI → 4× LANCZOS 放大 → 喂检测
   模型:yolo26s_det_rarity(25类) / yolo26s_det_element(13类)
   ├─► infer_cat16_raw_2000.csv          (box1/2/3_cls + conf)
   └─► roi_ocr_A/seq_{left,right}/*.jpg  (编号 ROI 裁剪)
                    │
   ocr_card_no_step.py --crop-dir roi_ocr_A     (paddleocr)
   └─► roi_ocr_A_viz/seq_{left,right}_texts.tsv
                    │
   parse_card_no_ebay.py
   └─► card_no_parsed_cat16.csv
                    │
   run_full_rerun_cat16.py::step5_parse_and_merge()
   └─► infer_cat16_result_2000.csv
       列:id, crop_status, card_no, rarity_cls, rarity_conf, box3_cls, box3_conf, error

⚠️ 这批脚本路径硬编码严重(服务器 /home/user/... 与本地 D:\... 混写),迁移要逐个改。


七、新增卡片上线的标准流程(把上面串起来)

# 做什么 怎么做 人工? 耗时
1 更新卡列表 从 PG cards_master_v2 导出,更新 train_meta_v0904.json / card_master_fields.csv 分钟级
2 下图 图片下到 data/gallery_images/文件名必须是 record.id.ext 命名(对齐 train_meta_v0904.json 的 fname) ~30 min
3 建库 python build_dual_gallery_v0904.py ~90 min
4 校验 python scripts/cascade_match.py --self-test 200,看 Top-1 命中率 5-10 min
5 重启服务 按 §1.1 重启,验 gallery_size 变成新行数 + 打真实请求 1 min
6 (仅 milvus 兜底)灌 Milvus python scripts/_milvus_rebuild_v819.py --drop(需先适配读取源) (需确认) 10-15 min

只加卡、不重训是常态;一旦重训了模型,权重 / 特征库(/ Milvus 兜底)必须整套一起换。 若重训换权重:新权重进新的版本化目录(如 upper_model_output_v0904/)+ config 指过去,勿覆盖旧目录(便于回滚)。


八、关键参数与它们在哪

参数 位置 含义
CASCADE_ALPHA 0.5 config.py:91 fusion = α·upper + (1-α)·lower
CASCADE_TOP_K_RECALL 30 config.py:90 上半区全库召回多少个交给下半区重排
SIMILARITY_THRESHOLD 0.50 config.py:75 高/低置信分界,只影响 high_conf 标记,不卡返回
CASCADE_BACKEND gpu config.py:103 检索后端(gpu=特征库常驻显存;milvus=回滚兜底)
DUAL_FEATURE_DIM 1024 config.py:82 dinov2-large CLS 维度
CARD_CROP_MODE seg_warp config.py:16 seg_warp=mask 4 点 warp(默认)/ bbox_legacy=只切矩形
MARGIN 0.05 环境变量,flat_card_no_detect.py:17 编号框外扩
LANG_JUDGE_TIMEOUT 8s 环境变量 调 8100 超时即降级

九、踩坑速查(都是实测过的)

现象 对策
config 指错库 服务起来 gallery_size 还是旧数 确认 config.py:96-98 指向 data/gallery_v0904/,重启生效
权重图库混版 匹配全乱、相似度普遍很低 检查 /healthgallery_size;权重和特征库必须同版
选错 GPU no kernel image is available 必须使用 CUDA_VISIBLE_DEVICES=1
/health 假 ok health 通了但请求全错 必须再打一个真实请求验
ultralytics 内存泄漏 批量 predict 越跑越占内存 分片跑、跑完重启进程
onnxruntime 影子化 明明该走 GPU 却静默回退 CPU requirements.txt 里的裸 onnxruntime 会覆盖 GPU 版
PaddleOCR 环境串了 import paddle 失败或 ultralytics 报错 pytorch 与 paddleocr 两个环境的包互不兼容,别混装
history 文件涨爆盘 data/match_results_history.json 巨大 8000 每次请求都追加,定期清理

十一、包内脚本地图

code/                              (2026-09-07 全量对齐 249/73 生产)
├── serve_recognition_api.py       :8010 主识别接口(后端用)
├── serve_card_match.py            :8000 业务字段接口(后台/树莓派用)
├── serve_card_match_v2.py         :8020 新版多卡接口(gunicorn -w1 --threads8,生产主入口)
├── serve_lang_judge.py            :8100 语种/方向 sidecar(内部)
├── config.py                      ★所有路径/阈值/参数,所有脚本都读它
├── build_dual_gallery_88384.py    v819 建库(历史参照)
├── build_dual_gallery_v0904.py    ★现役建库(91,116,输出 data/gallery_v0904/)
├── prepare_train_data_v3.py       ★训练数据准备(gallery+meta→上/下半区裁片分组)
├── train_dinov2_upper_half.py     ★上半区训练(bs=224 双卡 100ep)+ _v0904 变体
├── train_dinov2_layer3_bottom.py  ★下半区训练(bs=8 单卡 150ep)+ _v0904 变体
├── mine_layer3_step1_extract.py   ★下半区挖矿①提特征(+ _v0904 变体)
├── mine_layer3_step2_pairs.py     ★下半区挖矿②混淆组(+ _v0904 变体)
├── ab_test_v0904.py               A/B 同尺 h2h 评测(v0904 胜出判据)
├── parse_card_no.py               旧 card_no 方案(已归档)
├── yolo_crop.py / ocr_judge.py    语种方向批处理版(两环境)
├── step1_crop.py                  评级区域切图工具(独立)
├── scripts/
│   ├── cascade_match.py           ★双区级联核心 DualCascadeMatcher + 单图/自检入口
│   ├── cascade_match_batch.py     ★批量匹配出 CSV
│   ├── build_gallery.py           旧 A4 768 维建库(废弃)
│   ├── _verify_gallery_v819.py    v819 库自检索校验(回滚场景用)
│   └── _milvus_rebuild_v819.py    Milvus 重灌(带 PROTECTED 白名单,回滚场景用)
├── tools/                         8020 运维/压测/调优工具集
│   ├── loadtest_8020.py           吞吐压测
│   ├── profile_8020_pipeline.py   管线耗时剖析
│   ├── abtest_backend_gpu.py      gpu/milvus 后端 A/B
│   ├── test_card_merge.py         近距 mask 合并测试
│   └── …
├── modules/
│   ├── feature_extractor_dual.py  ★建库与查询共用的唯一预处理入口(letterbox/split_half/提特征)
│   ├── yolo_detector.py           ★裁卡(mask 4 点 warp + 竖放 + 近距合并 50px + card_type)
│   ├── attribute_detector.py      ★属性(cat16 配方 box3 右上 ROI + 4× LANCZOS)
│   ├── feature_extractor.py       旧 A4 单区 768 维(废弃)
│   ├── card_no_leniency.py        编号宽容判定
│   ├── rectifier.py               透视矫正/正立
│   ├── vector_index.py            旧 768 维向量检索(废弃)
│   ├── image_downloader.py        下图(md5 命名)
│   ├── grading_detector.py        评级标签检测封装
│   └── capp_upload.py / csv_reader.py / db_reader.py / ultralytics_compat.py
└── pokemon/
    ├── flat_grading.py / flat_ocr.py                       ★评级线(日常,平铺目录)
    ├── grading_ocr.py / grading_ocr_step2.py /
    │   parse_grading_ocr.py / infer_test.py                评级 OCR 与测试
    ├── flat_card_no_detect.py / flat_card_no_ocr.py        ★card_no 线
    ├── flat_card_no.py                                     card_no 集成示例版
    └── pokemon_grading.py / pokemon_ocr_step2.py / pokemon_report.py

相关文档

  • 资产清单 / MD5 / 落位映射:README_上线清单.md
  • 三个接口的契约(请求头/响应字段/错误码):docs/宝可梦卡牌识别服务文档接口.md
  • 算法原理与演进:docs/宝可梦整体流程框架详解.md
  • 裁卡与摆正细节:docs/卡牌区域检测裁切与旋转正立流程.md
  • 重训漂移评估方法:docs/DINOv2重训实验总结报告.md
  • 编号 OCR 宽容政策:docs/卡牌区域OCR_rec宽容政策.md