面向接手的开发/运维。照着做即可。资产清单与落位见
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 消歧逻辑。所以它挂了对准确率无影响。
离线(建库/批量/其它子系统):见第三、四、五节,这些是有中间文件落盘的多步链路。
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 &
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:103 的 CASCADE_BACKEND(gpu / milvus),或命令行 --backend。249 现役 gpu(特征库 fp32 常驻显存,召回 0.4ms/卡);milvus 为回滚兜底。
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
| 产物 | 路径 | 说明 |
|---|---|---|
| 下载的查询图 | 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, top5(high_conf = fusion ≥ SIMILARITY_THRESHOLD)<out_csv 同名>.topk.json,每张图的完整 Top-K 明细⚠️ 用户要求推理结果只存 .xlsx 不要 csv。这个脚本原生出 CSV,跑完请转成 xlsx 并删掉 csv(或改脚本用
to_excel)。
现役脚本: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 上下两库形状一致(行对齐) |
为什么建库和查询必须同一个预处理入口:库特征和查询特征要在同一个向量空间里做点积才有意义。
letterbox392→split_half→ ImageNet 归一化 → CLS → L2,任何一环不一致,相似度就不可比。所以两侧都只走modules/feature_extractor_dual.py,不要另写预处理。
v0904 建库输出在 data/gallery_v0904/,config.py 就直接指向该子目录(:96-98),建完重启服务即生效。
(v819 时代"输出 gallery_v819/ 再手工搬主路径"的步骤已废除;旧主路径 npy 若存在属 v819 回滚资产,勿混用。)
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 路径,仅回滚场景使用。)
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 |
build_dual_gallery_v0904.py 只能全量重建,没有 append 模式。要只加新卡不重建,需要自己改:读现有 npy → np.vstack 新特征 → 合并 meta 的 card_ids/metas(必须保持行对齐)→ 覆盖保存(gpu 后端保存后重启服务即生效;Milvus 兜底侧无增量入口,只有 --drop 全灌)。
好消息是:加卡不需要重训模型(模型与图库解耦)。只有真重训才必须"权重 + 特征库(+ Milvus 兜底)整套同步换"。
{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,:33;engine=transformers, device=cpu) |
card_no_ocr.json:rec_text / card_no / leniency_applied / empty_reason / rec_score / det_conf |
MARGIN 环境变量(默认 0.05,:17)控制编号框外扩幅度。modules/card_no_leniency.py 的 finalize_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-95 与 serve_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)。
这条线的脚本在 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/,重启生效 |
| 权重图库混版 | 匹配全乱、相似度普遍很低 | 检查 /health 的 gallery_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
README_上线清单.mddocs/宝可梦卡牌识别服务文档接口.mddocs/宝可梦整体流程框架详解.mddocs/卡牌区域检测裁切与旋转正立流程.mddocs/DINOv2重训实验总结报告.mddocs/卡牌区域OCR_rec宽容政策.md