ebay图片推理入库流程.md 20 KB

eBay 图片推理入库全流程文档

ebay_record_2026h1 的图片做模型推理,读出真实 card_id / 评级公司 / 评级分数,回填进 ClickHouse card_transactions_unified_sync。 本文档覆盖:取数 → 缓存 → 推理 → 语种判定 → OCR → 重检索 → 入库 → 人工核验导出,以及全程踩过的坑与解法。


一、背景与目标

原来的 import_card_transactions_unified.py纯文本路线:从 title 用正则提 card_no → 查 PG cards_master 映射 card_id(命中率极低,大量 UNKNOWN),eBay 的 grade_company 直接写死 RAWgrade_score 写死 完全没用图片

本流程改用图片模型推理

  • DINOv2 算出真实 card_id(图像相似度 Top1)
  • best.pt(YOLO 评级卡检测)读出真实 grade_company(PSA/BGS/CGC/SGC)
  • PaddleOCR 读出评级标签上的 grade_score + 判定语种

最终成果(2026-07-09 收工)

指标 数值
推理总量 184881 张(category_id=16 Pokemon 且 img_stat=40 且有图)
ClickHouse 整表 158304 行(本次新增 61947)
评级卡 38768
有评级分数 32652
人工核验案例(sim<0.60) 26577 → 导出待人工核对

二、整体架构(两机协作 + 三库)

┌─────────────────────────────────────────────────────────────┐
│  本地 Windows (D:\顾工交接\wzj\ebay_sync\)                    │
│  ─ Anaconda Python,连三库                                   │
│  ─ 01 导出目标 / 04 入库 / 05 导出人工核验 / finalize_watch   │
│  ─ paramiko SSH 编排服务器                                   │
└───────────────┬─────────────────────────────────────────────┘
                │ SSH (martin@192.168.77.249) + scp
                ▼
┌─────────────────────────────────────────────────────────────┐
│  服务器 192.168.77.249 (~/wzj/wzj/ebay_sync/)                │
│  ─ GPU: V100×2 (GPU1/GPU2) + GTX1060 (GPU0,不用)            │
│  ─ conda env: pytorch (推理) / paddleocr (OCR+语种)         │
│  ─ ebay_infer_stream / ebay_lang_judge / ebay_ocr_step /     │
│    ebay_rematch / run_infer_dual.sh                          │
└───────────────┬─────────────────────────────────────────────┘
                │ 内网拉图
                ▼
         内网 MinIO 192.168.77.80:9000 (~0.1s/张)

三个数据库:
  MySQL   100.64.0.21:3306  crawler/readonly/Pass2024        ← 取原始记录 + 图片路径
  PG      100.64.0.10:25432 hs_sync_data/readonlyuser/Pass2026 ← cards_master 中文名/图(人工核验用)
  ClickHouse 192.168.31.233:8123  card_transactions_ro/...    ← 最终入库(可写)

关键隔离点:本地能连三库,服务器能拉图 + 跑 GPU,两者网络隔离,靠 SSH/scp 传 CSV。


三、端到端数据流

[本地 01] MySQL 拉 source_id+全字段
    └─ ebay_targets.csv (184881行, 含 img_url=MinIO内网地址)
         │ scp 上传服务器
         ▼
[服务器 infer] 双卡并行 (V100×2, max-items 分片重启防OOM)
    下载图 → best.pt 评级壳 → YOLO 卡面分割 → 透视矫正 → DINOv2 特征 → 全库向量检索 Top1
    └─ ebay_infer_result.csv  (card_id/sim/grade_company/...)
    └─ ebay_feats.npz         (DINOv2 特征缓存, 供 rematch 复用)
    └─ data/ebay_cardcrops/   (卡面图, 供 lang 判语种)
    └─ data/ebay_crops/       (评级标签条, 供 ocr 读分数)
         ▼
[服务器 lang] PaddleOCR 判语种 (kana/CJK/latin + hanzidentifier)
    └─ ebay_lang.csv (tx_id, language)
         ▼
[服务器 ocr]  PaddleOCR 读评级标签分数
    └─ ebay_ocr_result.csv (tx_id, grade_score)
         ▼
[服务器 rematch] 语种分层重检索 (复用缓存特征, 不重跑模型)
    英文卡只在 tcg us 库找, 避免错配日/中文卡
    └─ ebay_infer_result_lang.csv (覆盖 card_id/sim/top5, 增列 language)
         │ scp 取回本地
         ▼
[本地 04]   构建 20 列 CH 行, 增量入库
    sim≥0.50 → 入库 ; sim<0.50/失败 → "人工核验" 跳过
    └─ card_transactions_unified_sync (+61947 行)
         ▼
[本地 05]   导出人工核验案例 (info-only)
    └─ D:\顾工交接\wzj\人工核验\ebay\ (manual_top5_detail.csv 等)

四、各阶段详解

阶段 0 | 01_export_targets.py(本地,连 MySQL)

作用:从 MySQL crawler.ebay_record_2026h1 拉满足条件的记录,产出推理+入库用的 CSV。

  • 过滤基线category_id=16 (Pokemon) AND img_stat=40 (图下载成功) AND image_uri IS NOT NULL AND image_uri<>''
  • 图片源:内网 MinIO 网关 http://192.168.77.80:9000/ + image_uri不用 img 列的公网 ebay 图(公网慢 ~10×、且部分失效)。
  • 排序:有 --from/--to 时按 last_sold 序(业务取法),否则按 id ASC(冷启动测试)。
  • 输出 ebay_targets.csv 字段(9列): tx_id, source_id, item_id, title, avg_sold_price, asp_type, total_sold, last_sold, img_url

    • tx_id = ebay_record_2026h1_<source_id>(全流程主键锚点)
    • img_url = 网关 + image_uri(内网图)

      python 01_export_targets.py                    # 默认拉全部满足条件
      python 01_export_targets.py --from 2026-06-22 --to 2026-06-28   # 按日期段
      

阶段 1 | ebay_infer_stream.py(服务器,pytorch env,GPU)★ 核心

作用:逐行下载图 → 评级壳检测 → 卡面分割矫正 → DINOv2 特征 → 全库 Top1 检索,一次跑完 card_id + 评级。

单条处理流程 (process_one):

  1. 下载 img_url 到临时目录(8 线程并发,块 256)
  2. 评级壳检测 GradingDetector(best.pt) 在原图上 → (company, subtype, conf, box);检出则裁标签条存 ebay_crops/
  3. 卡面分割 CardDetector.detect_box_mask(box, mask, orig)
  4. 透视矫正 CardRectifier.rectify(mask, box, angle=0) → 正立卡面;存 ebay_cardcrops/
  5. 特征提取 CardFeatureExtractor.extract (DINOv2, 768d) → 存 ebay_feats.npz供 rematch 复用,不重跑模型
  6. 向量检索 VectorIndex.search(feat, top_k=5) → Top1 card_id + sim + top5
  7. 删临时图(磁盘峰值仅一块 ~128MB)

模型一次性加载:★卡牌区域检测 (card_seg_pn_v2/weights/best.pt,TRT 可选 best.engine;旧 yolov11n_card_seg01.onnx 仅回退) + best.pt + DINOv2 (PokemonCN04) + 图库(78272 卡 ×768d)。

产出

  • ebay_infer_result.csv — tx_id/source_id/card_id/similarity/grade_company/grade_subtype/grade_conf/is_graded/crop_file/top5/status
  • ebay_feats.npz — tx_ids + feats(N,768)
  • data/ebay_cardcrops/*.jpgdata/ebay_crops/*.jpg

断点续跑:启动时读 ebay_infer_result.csv 已 done 的 tx_id,自动跳过。

阶段 1.5 | 双卡并行编排 run_infer_dual.sh ★ 防OOM核心

第五章。把剩余 tx_id 分成 shard0/shard1,两块 V100 并行,每跑 1500 条重启进程释放泄漏内存。

阶段 2 | ebay_lang_judge.py(服务器,paddleocr env,双卡)

作用:读卡面图 ebay_cardcrops/,用 PaddleOCR (PP-OCRv5) + hanzidentifier 判定语种(kana=日 / CJK=中 / latin=英)。

  • 双卡分片:SHARD=0/2SHARD=1/2 各处理一半
  • 输出 ebay_lang_0.csv / _1.csv → 合并成 ebay_lang.csv (tx_id, language)

阶段 3 | ebay_ocr_step.py(服务器,paddleocr env,双卡)

作用:读评级标签条 ebay_crops/,PaddleOCR 识别评级分数(如 GEM MT 109.5)。

  • 双卡分片同上
  • 输出 ebay_ocr_result.csv (tx_id, grade_score)

阶段 4 | ebay_rematch.py(服务器,pytorch env,单卡秒级)

作用语种分层重检索——复用阶段1缓存的 DINOv2 特征 + 阶段2判定的语种,在对应语种子库内重新检索,覆盖 card_id/similarity/top5。

为什么需要:阶段1全库检索时,英文卡可能错配到日/中文卡(DINOv2 特征相近)。按语种分层后,英文卡只在 tcg us 库找,准确率提升。

性能优化:预建"每种语种 → 子索引"(只建 4 次),替代每次检索重建 mask。检索变成纯 numpy matmul,41876 条秒级完成。

输入ebay_feats.npz + ebay_lang.csv + ebay_infer_result.csv 输出ebay_infer_result_lang.csv(覆盖 card_id/sim/top5,增列 language)

阶段 5 | 04_load_clickhouse.py(本地,写 ClickHouse)★ 入库

作用:合并 ebay_targets.csv(全字段) + 推理结果 + OCR,构建 20 列 CH 行入库。

字段映射策略: | CH 字段 | 来源 | |---|---| | tx_id/source_id/title/item_id/auction_type/total_sold/price_original/sold_date/image_uri | ebay_targets.csv | | card_id | status==ok 且 sim≥0.50 → 图像 Top1;否则 "人工核验"(不入库,留给 05)| | grade_company | 检出评级壳且 ∈{PSA,BGS,CGC,SGC} → 该公司;否则 "非评级卡" | | grade_score | 评级卡 → OCR 分数;否则 "无" | | source_table/platform/currency | 固定 ebay_record_2026h1/eBay/USD |

入库模式(关键):

  • 默认增量追加:查 DB 已有同源 tx_id,只插新的,保留已入库数据(本次:DB已有96357 → 新增61947)
  • --replace:全量替换(先 ALTER DELETE WHERE 1=1 清表再灌)
  • --dry-run:只打印样例不动库
  • --target-valid N:入库有效行封顶(0=全部高置信入库)

    python 04_load_clickhouse.py --dry-run      # 先校验
    python 04_load_clickhouse.py                # 增量入库(默认)
    

注意:sim<0.50 的"人工核验"行不入库if cid == MANUAL: continue),留给 05 导出。所以入库数 < 推理数是正常的。

阶段 6 | 05_export_manual.py(本地)

作用:把 sim<0.50 / 匹配不稳的案例导出到本地,供人工核对。

  • 默认 --info-only(只导信息,不下图)
  • 查 PG cards_master 补候选 card_id 的中文名/img_url
  • 输出 D:\顾工交接\wzj\人工核验\ebay\
    • manual_top5_detail.csv — 每个案例的 Top5 候选明细
    • manual_index.csv — 汇总索引

五、双卡并行与防 OOM 设计(核心技术决策)

5.1 为什么要双卡

infer 是 GPU 密集型(下载只占 ~8% 时间),单卡时 GPU 空闲多。两块 V100 并行 → infer 时间约减半。提 workers(8→32) 只加速下载的 ~8%,收益小;双卡收益大。

5.2 分片机制

prepare_shards.py:读 ebay_infer_result.csv 已 done 的 tx_id,从 ebay_targets.csv 减去,剩余按偶/奇索引分成 shard0/shard1(各 ~18060 条)。

5.3 max-items 分片重启(防 OOM 灵魂)

问题:ultralytics model.predict() 会缓存 Results 对象,每图 ~6MB 泄漏,两个 detector(yolo+grading) 双倍。跑到 5120 条时 RSS 飙到 30GB → OOM Killed。

解法--max-items 1500 让进程处理 1500 条后正常退出(写 checkpoint),外层 run_shard() 循环重启新进程(自动跳过已 done)。

  • 只有进程退出才能彻底释放 ultralytics 缓存,这是唯一可靠办法
  • 每批实际跑 1536 条(1500 按 chunk 256 取整 = 6块),重启加载图库 ~9s,开销可忽略
  • RSS 从原来 5120 条 30GB → 现在 2.4GB 稳定

5.4 run_shard() 失败检测(防死循环)

run_shard() {
  while true; do
    DONE=$(行数)
    [ $DONE -ge $TOTAL ] && break
    python ebay_infer_stream.py --max-items 1500 ...
    NEWDONE=$(新行数)
    if [ $NEWDONE -le $DONE ]; then
      FAIL=$((FAIL+1))
      [ $FAIL -ge 4 ] && break   # 连续4次无进展放弃, 防死循环
    else FAIL=0; fi
  done
}

5.5 checkpoint 内存优化

原实现 np.stack([100k arrays]) 在首次 checkpoint 触发峰值 OOM。改为:

  • 结果 CSV:prev_results + results 全量重写
  • 特征 npz:用 np.concatenate([old_keep, new_feats]) 替代 list+stack,并 del 中间量

5.6 合并 ebay_merge_results.py

双卡产出 ebay_infer_result_0.csv/_1.csv + ebay_feats_0.npz/_1.npz,合并到主文件(后者覆盖去重),特征用预分配 np.empty((n,768)) 逐条填避免峰值,然后清理分片文件。


六、踩过的坑与解决方案 ⚠️(重点)

坑1:5120 条必 OOM

  • 现象:infer 每次卡在 5120 条,Killed process ... anon-rss:30291292kB
  • 根因:ultralytics predict() 缓存 Results 对象,6MB/图 × 5120 = 30GB;yolo_detector + grading_detector 两个都 predict,双倍泄漏
  • 解法--max-items 1500 分片重启(进程退出才彻底释放)+ del downloaded + 每512条 gc.collect() + checkpoint 改 concatenate
  • 详见第五章 5.3-5.5

坑2:milvus 抢内存(且 pkill 无效)

  • 现象:服务器可用内存被 milvus 吃到只剩很少,infer 一跑就 thrash
  • 误区sudo pkill milvus 杀掉后又被自动拉起,内存又涨回 7.5GB
  • 根因:milvus 是 Docker 容器(milvus-standalone + milvus-minio + milvus-etcd),restart policy=always。pkill 进程后 docker daemon 立即重启它
  • 解法docker stop milvus-standalone milvus-minio milvus-etcd + docker update --restart=no(永久停直到手动恢复)。推理完再 finalize_watch 的 [5/5] 恢复
  • 注意:249 的 milvus-minio 也用 port 9000,但和拉图的 192.168.77.80 是不同 minio,停 milvus 不影响拉图

坑3:run_single_finish.sh 无 set -e → 假 PIPELINE_DONE

  • 现象:infer 崩溃后脚本继续跑 lang/ocr/rematch,最后照样 touch PIPELINE_DONE,finalize 误以为完成去入库(结果不全)
  • 根因:旧编排脚本没有失败检测
  • 解法:新 run_infer_dual.shrun_shard() 加 4 次无进展放弃机制(5.4)

坑4:checkpoint npz 不落盘 / 峰值 OOM

  • 现象:第一次触发 checkpoint 时又 OOM
  • 根因list(10万 arrays) + np.stack 在 stack 那一刻内存峰值爆炸
  • 解法:改 np.concatenate + del 中间量;合并阶段用预分配(5.5)

坑5:ulimit -v 14000000 致 DINOv2 加载失败

  • 现象:为限内存加 ulimit -v 14000000,结果 DINOv2 模型 mmap 报 Cannot allocate memory (12)
  • 根因:虚拟地址空间限制卡住了 mmap
  • 解法:删除 ulimit,靠 max-items 重启控内存(治本)

坑6:paramiko 后台启动进程时 o.read() 挂起

  • 现象:用 setsid nohup ... & 后台启动 run_infer_dual.sh,paramiko o.read() 一直挂住不返回
  • 根因:读 backgrounded 进程的 stdout,channel 不会 EOF
  • 解法:用 transport.open_session() + chan.exec_command(...) + chan.close()不读 stdout

坑7:dl_fail 间歇升高(~9-15%)

  • 现象:infer 时 dl_fail 从 270 升到 601(~15%),后来稳定 ~9%
  • 根因:内网 MinIO 间歇性失败(网络抖动/限速)
  • 处理非致命——失败行 status=download_fail,04 入库时归为"人工核验"跳过,不阻断整体

坑8:MySQL 大 IN-list 超时

  • 现象:用 WHERE id IN (...) 一次查几万 id 容易超时
  • 解法:分块查(每块 200),或直接 WHERE 过滤条件 ORDER BY 全量拉(01 采用后者)

坑9:查进度.py 读错日志 → 误判卡死

  • 现象:旧版查进度读单卡时代的 infer_single.log,双卡改用 infer0/1.log 后显示全是"7分前"和空,误以为卡死;读旧残留 ebay_ocr_result.csv(2141行) 误导
  • 解法:重写成链式结构——一条 SSH 命令 + <<<TAG>>> 分段一次拿回全部(进程/进度/RSS/GPU/内存/milvus),单次往返;阶段判断优先用进程名(has_lang 等),log-age 兜底

坑10:05 导出 PermissionError

  • 现象05_export_manual.pymanual_top5_detail.csvPermission denied
  • 根因:该 CSV 被Excel/WPS 打开占用(之前导出的旧文件)
  • 解法:关闭占用程序后重跑 python 05_export_manual.py(非致命,入库已完成)

七、如何运行

一键全自动(推荐)

# 本地后台启动监控(会自动: 轮询PIPELINE_DONE → 下载结果 → 04入库 → 05导出 → 恢复milvus)
python finalize_watch.py

手动分步

# 1) 本地导出目标
python 01_export_targets.py
# scp 上传 ebay_targets.csv 到服务器 ~/wzj/wzj/ebay_sync/

# 2) 服务器双卡推理(SSH 上去)
cd ~/wzj/wzj
setsid nohup bash ebay_sync/run_infer_dual.sh </dev/null >/dev/null 2>&1 &
# 产物: ebay_infer_result.csv + ebay_feats.npz + ebay_lang.csv + ebay_ocr_result.csv + ebay_infer_result_lang.csv
# 生成 PIPELINE_DONE 标记

# 3) 本地入库(先 dry-run)
python 04_load_clickhouse.py --dry-run
python 04_load_clickhouse.py             # 增量入库

# 4) 本地导出人工核验
python 05_export_manual.py

查进度

python 查进度.py     # 链式版, 单次SSH往返, 显示阶段/进度/RSS/GPU/内存/milvus/进程

恢复 milvus(推理完成后)

# 服务器
sudo docker update --restart=always milvus-etcd milvus-minio milvus-standalone
sudo docker start milvus-etcd milvus-minio     # 先依赖
sleep 6
sudo docker start milvus-standalone

(finalize_watch 的 [5/5] 会自动做这步)


八、关键配置与依赖

GPU 映射(服务器)

物理位 UUID 前缀 型号 用途
GPU0 GPU-bb6d8194 GTX1060 3GB 不用
GPU1 GPU-9d6999fb V100 16GB shard0 (V1)
GPU2 GPU-db470791 V100 16GB shard1 (V2)

环境变量(GPU 隔离)

CUDA_VISIBLE_DEVICES=GPU-9d6999fb-...   # 指定单卡
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:64,expandable_segments:True  # 减少显存碎片

关键阈值

  • SIM_THRESHOLD = 0.50(card_id 入库门槛,<0.50 归人工核验)—— config.py + 04
  • --max-items 1500(防 OOM 分片大小)
  • --workers 8 --chunk 256(并发下载/分块)

ClickHouse 表

  • card_transactions.card_transactions_unified_sync
  • 引擎 ReplacingMergeTree,ORDER BY (card_id, sold_date, platform, tx_id)
  • 20 列(见 04 的 CH_COLUMNS)
  • 默认 APPEND,按 tx_id 去重

文件镜像

本地 D:\顾工交接\wzj\ebay_sync\ ↔ 服务器 ~/wzj/wzj/ebay_sync/,同名同结构。


九、文件清单

文件 位置 作用
01_export_targets.py 本地 MySQL 拉目标 → ebay_targets.csv
ebay_infer_stream.py 服务器 ★ 核心:下载+评级+分割+特征+检索
prepare_shards.py 服务器 剩余 tx_id 分 shard0/shard1
run_infer_dual.sh 服务器 ★ 双卡编排 + max-items 重启 + 失败检测
ebay_merge_results.py 服务器 合并双卡 result + feats,清理分片
ebay_lang_judge.py 服务器 PaddleOCR 判语种(双卡)
ebay_ocr_step.py 服务器 PaddleOCR 读评级分数(双卡)
ebay_rematch.py 服务器 语种分层重检索(复用特征)
04_load_clickhouse.py 本地 ★ 构建20列行,增量入库
05_export_manual.py 本地 导出人工核验案例
finalize_watch.py 本地 轮询 DONE 自动收尾(+恢复milvus)
查进度.py 本地 链式查进度(单次SSH往返)

十、复盘要点(给下一次)

  1. 内存泄漏类问题:ultralytics/某些库的缓存只能靠进程退出释放 → --max-items 分片重启是最稳的通用解法
  2. milvus/常驻服务抢资源:先查清楚是 systemd 还是 docker,docker 要 update --restart=no 才能真停
  3. 编排脚本必须有失败检测:无进展计数器防死循环,set -e 或显式 rc 检查防假完成
  4. 大数组合并避免 list+stack:用 concatenate 或预分配
  5. 进度查询要读对文件:架构变了日志名就变,链式结构 + 进程名判断最稳
  6. 入库与导出分离:sim<0.50 不入库而是导出人工核验,保证入库数据全是高置信的