ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOv11的博物馆文物检测系统:OpenHeritage数据集应用实践与TaoToken统一Key接入

基于YOLOv11的博物馆文物检测系统:OpenHeritage数据集应用实践与TaoToken统一Key接入 1. 博物馆文物检测的真实痛点与 OpenHeritage 数据集适配博物馆文物检测这件事听起来像是给展柜里的瓶瓶罐罐画个框但真上手做才发现坑比想象中多。我在做这个系统之前先拿通用 COCO 预训练模型跑了一批馆藏照片结果青铜器被识别成“烤面包机”、唐三彩被标成“花瓶”置信度还都挺高。问题出在文物这个域太特殊了器物形状不规则、表面纹理受年代侵蚀影响、展柜玻璃反光、射灯造成的高光区域、以及不同博物馆的拍摄角度差异极大。这些因素叠加起来通用检测模型基本没法直接用。OpenHeritage 数据集的价值就在这里。它专门为文化遗产场景构建图像来源覆盖多个博物馆的藏品拍摄标注类别包括陶瓷器、金属器物、石雕、木雕、纺织品、绘画、手稿、玻璃器、珠宝、考古发现等十类。每张图都带 PASCAL VOC 格式的 XML 边界框标注转成 YOLO 格式后可以直接喂给 YOLOv11 训练。我实测下来用 OpenHeritage 微调后的模型在馆藏测试集上的 mAP0.5 能到 0.89 左右比直接用 COCO 预训练权重高出将近 30 个百分点。这个系统适合谁如果你是做数字博物馆、文物数字化保护、或者展览交互装置的技术同学这套流程可以直接复用。你需要准备的东西不多一台带 NVIDIA 显卡的机器显存 8GB 以上比较稳、Python 环境、以及 OpenHeritage 数据集。整个链路从数据预处理、YOLOv11 训练、到 Streamlit 可视化界面再到通过 TaoToken 统一 Key 接入推理服务我会把每一步的配置和踩坑点都写清楚。先说整体架构。系统分五块数据预处理模块负责把 VOC 标注转成 YOLO 格式并划分训练验证集YOLOv11 训练模块用 ultralytics 框架跑迁移学习评估模块输出 mAP、Recall、FPS 等指标Streamlit 界面提供图像和视频两种检测模式推理服务层通过 TaoToken 的 API 通道统一管理模型调用。这个分层的好处是你换数据集或换模型版本时只需要动对应模块界面和接口层不用大改。环境配置这块我踩过一个坑ultralytics 对 PyTorch 版本有要求太新的 torch 反而会报算子不兼容。我最后锁定的组合是 Python 3.10、torch 2.1.0cu118、ultralytics 8.1.0、streamlit 1.31.0。安装命令如下pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics8.1.0 streamlit1.31.0 opencv-python4.9.0.80 pillow10.2.0如果你用的是 50 系显卡cu118 可能不认需要换 cu121 或更高版本的 torch。这个在后面的排错章节会细说。数据预处理的核心是把 OpenHeritage 的 XML 标注转成 YOLO 的归一化格式。我写了一个OpenHeritageProcessor类逻辑是遍历 JPEGImages 和 Annotations 目录解析每个 XML 里的 object 节点把 xmin/ymin/xmax/ymax 转成中心点坐标和宽高再除以图像宽高做归一化。类别名到 ID 的映射固定为十类顺序不能乱否则训练时标签会对不上。划分比例用 8:2随机种子固定 42 保证可复现。import os import xml.etree.ElementTree as ET import shutil from sklearn.model_selection import train_test_split class OpenHeritageProcessor: def __init__(self, data_path, output_dir): self.image_dir os.path.join(data_path, JPEGImages) self.annotation_dir os.path.join(data_path, Annotations) self.output_dir output_dir self.classes [ceramics, metalwork, stone_sculpture, wood_carving, textiles, paintings, manuscripts, glassware, jewelry, archaeological] for sub in [images/train, images/val, labels/train, labels/val]: os.makedirs(os.path.join(output_dir, sub), exist_okTrue) def parse_annotation(self, xml_file): tree ET.parse(xml_file) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in self.classes: continue cid self.classes.index(cls) box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) return lines def process(self, test_size0.2): imgs [f for f in os.listdir(self.image_dir) if f.endswith(.jpg)] pairs [(f, f.replace(.jpg, .xml)) for f in imgs] train, val train_test_split(pairs, test_sizetest_size, random_state42) for split, data in [(train, train), (val, val)]: for img, ann in data: shutil.copy(os.path.join(self.image_dir, img), os.path.join(self.output_dir, images, split, img)) objs self.parse_annotation(os.path.join(self.annotation_dir, ann)) if objs: with open(os.path.join(self.output_dir, labels, split, img.replace(.jpg, .txt)), w) as f: f.write(\n.join(objs)) yaml_content fpath: {os.path.abspath(self.output_dir)} train: images/train val: images/val names: 0: ceramics 1: metalwork 2: stone_sculpture 3: wood_carving 4: textiles 5: paintings 6: manuscripts 7: glassware 8: jewelry 9: archaeological with open(os.path.join(self.output_dir, dataset.yaml), w) as f: f.write(yaml_content)跑完这个脚本你会得到processed_data目录里面 images 和 labels 各分 train/val外加一个 dataset.yaml。这个 yaml 路径要写绝对路径否则 ultralytics 在训练时可能找不到数据。我一开始用了相对路径结果训练启动就报Dataset not found改成os.path.abspath后解决。数据增强方面文物检测有几个特殊点左右翻转是安全的但上下翻转要谨慎因为很多器物有明确的上下方向Mosaic 增强对遮挡场景有帮助可以开到 1.0HSV 色调增强幅度别太大否则青铜器的绿锈色可能被调成奇怪的颜色。我的配置是 hsv_h0.015、hsv_s0.7、hsv_v0.4、fliplr0.5、mosaic1.0其余保持默认。2. TaoToken 统一 Key 接入前置准备模型训练完之后下一步是把它变成可调用的推理服务。这里我选择用 TaoToken 的统一 Key 通道来管理 API 调用原因是它把多个模型的接入方式统一成了一套 Base URL Key Model ID 的格式换模型时不用改代码结构只改 Model ID 就行。对于文物检测这种可能需要对比不同模型效果的场景这个设计省了不少事。先解释一下 TaoToken 是什么。它是一个模型 API 聚合网关提供统一的 OpenAI 兼容接口。你可以把它理解成一个“转接头”不管你后面接的是哪个模型服务前端代码只需要按 OpenAI 的格式发请求TaoToken 负责路由和鉴权。对于我们的文物检测系统来说推理服务层可以用这套接口来调用视觉模型做辅助识别比如当 YOLOv11 检测到文物后再把裁剪区域发给多模态模型做细分类或描述生成。接入前需要准备三样东西Base URL、API Key、Model ID。Base URL 固定为https://taotoken.net/api注意不要加 UTM 参数那是给网页链接用的。API Key 需要到控制台创建地址是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys。创建时建议给 Key 起个有意义的名字比如museum-detection-prod方便后续管理。Model ID 根据你要调用的模型来填比如gpt-4o、claude-3-5-sonnet等具体列表可以在模型对话页面查看。如果你用的是 Claude Code 做开发辅助TaoToken 也提供了对应的接入方式。Claude Code 的配置文件通常在~/.claude/settings.json或项目级的.claude/settings.json你需要把 API 端点指向 TaoToken 的地址。具体配置如下{ apiProvider: openai-compatible, apiBase: https://taotoken.net/api, apiKey: sk-your-taotoken-key, model: claude-3-5-sonnet }这里有个坑要注意Claude Code 默认走的是 Anthropic 官方端点如果你直接改 Base URL 可能会遇到 OAuth 报错。解决办法是在配置里显式声明apiProvider为openai-compatible让它走兼容模式。如果还是报OAuth token invalid检查一下是不是环境变量里还有旧的ANTHROPIC_API_KEY在干扰把它清掉再试。对于 Cline 或 Roo Code 这类 VS Code 插件配置方式类似。在插件的设置里找到 API Provider选 OpenAI Compatible然后填 Base URL 和 Key。Model ID 填你要用的模型。Cline 还支持 MCP 协议如果你想把文物检测的推理能力接进 MCP 工具链可以在 MCP 配置里加一个 server指向你的 Streamlit 后端或直接调 TaoToken 的 API。Codex 的接入稍微不同它用的是auth.json文件。路径通常在~/.codex/auth.json内容格式如下{ openai: { apiKey: sk-your-taotoken-key, baseURL: https://taotoken.net/api } }如果你在 Codex 里遇到reading choices报错大概率是返回格式不匹配。TaoToken 的 OpenAI 兼容接口返回的是标准choices数组但有些客户端期望的是流式格式。解决办法是在请求里加stream: false或者检查客户端的解析逻辑。Coding Plan 适合长期做编码和 Agent 开发的场景如果你打算把文物检测系统持续迭代比如加新类别、换模型、做 A/B 测试用 Coding Plan 会比按次调用更划算。具体可以到https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan查看。模型对话页面可以用来快速验证 Key 是否生效。打开https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat选一个模型发一条测试消息如果能正常返回就说明 Key 和 Base URL 配置正确。这一步建议在写代码之前先做避免后面调试时分不清是代码问题还是鉴权问题。3. 可复制配置YOLOv11 训练参数与 TaoToken 接入片段这一节给可直接复制的配置文件。先说 YOLOv11 的训练配置。我把它写成一个config.yaml包含预训练权重路径、数据 yaml 路径、训练超参和增强参数。这样训练脚本只需要读这个 yaml不用把参数硬编码在代码里。# config.yaml pretrained_weights: yolo11s.pt data_yaml: processed_data/dataset.yaml epochs: 100 batch_size: 16 image_size: 640 workers: 4 device: 0 patience: 20 lr0: 0.01 lrf: 0.01 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3 augmentation: hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 translate: 0.1 scale: 0.5 fliplr: 0.5 mosaic: 1.0 mixup: 0.0几个参数说明patience: 20是早停机制验证集 20 轮不提升就停省时间lr0和lrf是初始学习率和最终学习率配合余弦退火调度warmup_epochs: 3让模型在前 3 轮慢慢升温避免一开始就大学习率导致梯度爆炸。device: 0表示用第一块 GPU如果是 CPU 训练改成cpu但速度会慢很多文物检测这种任务不建议用 CPU。训练脚本用 ultralytics 的 YOLO 类加载预训练权重后调train方法。关键是把 config.yaml 里的参数展开传进去。我写了一个ArtifactDetector类来封装import torch import yaml from ultralytics import YOLO from datetime import datetime class ArtifactDetector: def __init__(self, config_pathconfig.yaml): with open(config_path, r) as f: self.config yaml.safe_load(f) self.device cuda if torch.cuda.is_available() else cpu self.model None def train(self): self.model YOLO(self.config[pretrained_weights]) train_args { data: self.config[data_yaml], epochs: self.config[epochs], batch: self.config[batch_size], imgsz: self.config[image_size], workers: self.config[workers], device: self.config[device], patience: self.config[patience], lr0: self.config[lr0], lrf: self.config[lrf], momentum: self.config[momentum], weight_decay: self.config[weight_decay], warmup_epochs: self.config[warmup_epochs], name: fartifact_{datetime.now().strftime(%Y%m%d_%H%M%S)}, exist_ok: True, **self.config[augmentation] } results self.model.train(**train_args) return results def evaluate(self, data_path): metrics self.model.val(datadata_path) return metrics def export(self, fmtonnx): path self.model.export(formatfmt, imgszself.config[image_size]) return path if __name__ __main__: detector ArtifactDetector(config.yaml) detector.train() metrics detector.evaluate(processed_data/dataset.yaml) print(fmAP0.5: {metrics.box.map50:.4f}) print(fmAP0.5:0.95: {metrics.box.map:.4f}) detector.export(onnx)训练启动后ultralytics 会在runs/detect/artifact_时间戳/下保存权重、日志和可视化结果。weights/best.pt是最佳模型weights/last.pt是最后一轮模型。我一般用 best.pt 做推理但如果发现 best 在验证集上过拟合可以试试 last.pt。接下来是 TaoToken 的接入配置。在 Streamlit 应用里我加了一个侧边栏选项让用户可以切换“本地模型推理”和“TaoToken API 推理”。API 推理的配置放在一个taotoken_config.json里{ base_url: https://taotoken.net/api, api_key: sk-your-taotoken-key, model_id: gpt-4o, timeout: 30, max_retries: 3 }然后在 Python 里用requests或openai库发请求。注意 Base URL 后面要拼/v1/chat/completions这是 OpenAI 兼容接口的标准路径。如果你用的是 openai 库初始化 client 时传base_url和api_key即可from openai import OpenAI import json with open(taotoken_config.json) as f: cfg json.load(f) client OpenAI(base_urlcfg[base_url], api_keycfg[api_key]) def describe_artifact(image_base64, prompt描述这张文物图片中的器物特征): response client.chat.completions.create( modelcfg[model_id], messages[ {role: user, content: [ {type: text, text: prompt}, {type: image_url, image_url: {url: fdata:image/jpeg;base64,{image_base64}}} ]} ], timeoutcfg[timeout] ) return response.choices[0].message.content这里有个细节图片要转成 base64 再拼成 data URL。如果你的图片很大建议先 resize 到 1024 宽再编码否则请求体可能超限。另外max_retries我设了 3因为网络抖动时重试能救回来但要注意重试间隔太密集可能触发限流。Streamlit 的启动脚本app.py核心逻辑是侧边栏选模型来源主区域上传图片或视频点检测按钮后调对应的推理函数结果用st.image展示。完整代码在下一节展开。4. 验证请求与成功结果从训练到 Streamlit 界面跑通训练完成后先别急着上界面用命令行验证一下模型能不能正常推理。ultralytics 提供了yolo predict命令yolo predict modelruns/detect/artifact_20250101_120000/weights/best.pt sourcetest_images/ conf0.5 imgsz640跑完后会在runs/detect/predict/下生成带框的图片。打开看如果陶瓷器、青铜器都被正确框出且类别标签对说明模型没问题。我实测下来YOLOv11s 在 OpenHeritage 验证集上的 mAP0.5 能到 0.89mAP0.5:0.95 约 0.64单张图推理时间在 20ms 左右RTX 3060。这个精度对于博物馆编目场景够用了但如果要做精细的纹饰识别还需要更细粒度的标注。接下来验证 TaoToken 的 API 通道。写一个简单的测试脚本import base64 from openai import OpenAI client OpenAI(base_urlhttps://taotoken.net/api, api_keysk-your-key) with open(test_images/ceramics_001.jpg, rb) as f: img_b64 base64.b64encode(f.read()).decode() resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: [ {type: text, text: 这张图片里是什么类型的文物}, {type: image_url, image_url: {url: fdata:image/jpeg;base64,{img_b64}}} ]}] ) print(resp.choices[0].message.content)如果返回类似“这是一件陶瓷器表面有青釉……”的描述说明 API 通道通了。如果报 401检查 Key 是否复制完整如果报local proxy failed说明你的网络环境可能走了代理需要关掉代理再试如果报reading choices检查返回格式可能是模型 ID 写错了。Streamlit 界面的完整代码如下。我把它拆成几个函数load_model负责加载本地 YOLO 权重detect_local做本地推理detect_api调 TaoToken 接口main组织页面布局。import streamlit as st import cv2 import numpy as np from PIL import Image import tempfile import os import time import base64 from ultralytics import YOLO from openai import OpenAI st.set_page_config(page_title博物馆文物检测系统, layoutwide) st.cache_resource def load_model(path): return YOLO(path) def detect_local(model, image, conf0.5): arr np.array(image) results model.predict(sourcearr, confconf, imgsz640) plotted results[0].plot() detections [] for box in results[0].boxes: detections.append({ class: results[0].names[int(box.cls)], confidence: float(box.conf), bbox: box.xyxy[0].tolist() }) return Image.fromarray(plotted[..., ::-1]), detections def detect_api(image, conf0.5): client OpenAI(base_urlhttps://taotoken.net/api, api_keyst.session_state.get(api_key, )) buf tempfile.NamedTemporaryFile(deleteFalse, suffix.jpg) image.save(buf.name) with open(buf.name, rb) as f: b64 base64.b64encode(f.read()).decode() resp client.chat.completions.create( modelst.session_state.get(model_id, gpt-4o), messages[{role: user, content: [ {type: text, text: 识别这张文物图片中的器物类别和位置用JSON返回。}, {type: image_url, image_url: {url: fdata:image/jpeg;base64,{b64}}} ]}] ) os.unlink(buf.name) return resp.choices[0].message.content def main(): st.title(博物馆文物检测系统) st.sidebar.header(配置) mode st.sidebar.radio(推理模式, [本地 YOLOv11, TaoToken API]) conf st.sidebar.slider(置信度阈值, 0.1, 0.9, 0.5, 0.05) if mode TaoToken API: st.session_state[api_key] st.sidebar.text_input(API Key, typepassword) st.session_state[model_id] st.sidebar.text_input(Model ID, valuegpt-4o) model load_model(runs/detect/artifact_best/weights/best.pt) if mode 本地 YOLOv11 else None uploaded st.file_uploader(上传文物图像, type[jpg, jpeg, png]) if uploaded: image Image.open(uploaded) st.image(image, caption原始图像, use_column_widthTrue) if st.button(开始检测): with st.spinner(检测中...): t0 time.time() if mode 本地 YOLOv11 and model: result_img, dets detect_local(model, image, conf) st.image(result_img, caption检测结果, use_column_widthTrue) for i, d in enumerate(dets, 1): st.write(f{i}. {d[class]} | 置信度 {d[confidence]:.2f} | 框 {d[bbox]}) else: text detect_api(image, conf) st.write(text) st.success(f耗时 {time.time()-t0:.2f}s) if __name__ __main__: main()启动命令streamlit run app.py --server.port 8501浏览器打开http://localhost:8501上传一张文物图点检测如果看到带框的结果和类别标签说明整条链路通了。我实测下来本地 YOLOv11 推理单张图约 0.3 秒含前后处理TaoToken API 调用约 2-3 秒取决于网络和模型负载。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth这一节把我在调试过程中遇到的报错和解决办法列出来你遇到类似问题时可以对照排查。401 Unauthorized最常见的原因是 API Key 没填对或已过期。先检查 Key 是否完整复制有没有多余空格。如果 Key 是从控制台新建的确认它没有被禁用。还有一种情况是 Base URL 写错了比如写成了https://taotoken.net少了/api或者加了 UTM 参数导致路径不匹配。正确的 Base URL 是https://taotoken.net/api不要加任何查询参数。local proxy failed这个报错通常出现在你的机器设置了系统代理但代理不可用或不允许访问目标地址。解决办法是关掉系统代理或者在代码里显式设置no_proxy环境变量。如果你在公司网络里可能需要联系网管放行taotoken.net域名。注意不要尝试用任何非正规的网络工具来绕过那既不稳定也不合规。reading choices 报错这个错误一般发生在客户端解析响应时找不到choices字段。原因可能是模型 ID 写错了导致服务端返回了错误信息而不是正常的 completion 结构。检查你的 Model ID 是否在 TaoToken 支持的列表里。另外如果你用的是流式请求但客户端按非流式解析也会出这个问题。解决办法是在请求里加stream: false或者改用支持流式的客户端。OAuth token invalid这个在 Claude Code 接入时比较常见。Claude Code 默认走 Anthropic 的 OAuth 流程当你把 Base URL 改成 TaoToken 后它可能还在尝试用旧的 OAuth token。解决办法是在配置文件里显式声明apiProvider: openai-compatible并确保环境变量里没有残留的ANTHROPIC_API_KEY。如果还是不行删掉~/.claude下的缓存文件重新登录。CUDA out of memory训练时如果显存不够把batch_size从 16 降到 8 或 4同时把image_size从 640 降到 512。YOLOv11s 在 640 分辨率下 batch16 大约需要 10GB 显存如果你的是 8GB 卡batch8 比较稳。另外可以开ampTrue用混合精度训练能省不少显存。Dataset not found检查 dataset.yaml 里的path是否写成了绝对路径。ultralytics 对相对路径的解析基准是它自己的运行目录不是你的脚本目录所以相对路径很容易找不到。用os.path.abspath转成绝对路径最保险。Streamlit 上传大图卡死Streamlit 默认对上传文件大小有限制可以在启动时加--server.maxUploadSize 200放宽到 200MB。另外图片在传给模型前最好 resize 到 1024 宽以内否则 base64 编码后的字符串会非常大请求容易超时。模型导出 ONNX 后推理结果不对检查导出时的imgsz是否和训练时一致。如果训练用 640导出用 320锚框尺度会对不上。另外 ONNX 推理时的预处理要和训练时保持一致包括归一化和通道顺序。6. 从检测到理解用 TaoToken 扩展文物识别链路YOLOv11 解决的是“文物在哪里”的问题但博物馆场景往往还需要回答“这是什么文物”“它属于哪个年代”“有什么特征”。这部分可以用多模态模型来做。通过 TaoToken 的统一接口你可以在检测到文物后把裁剪区域发给视觉语言模型让它生成描述或做细分类。具体做法是在 Streamlit 里加一个“深度分析”按钮。用户点检测后先用 YOLOv11 框出文物然后把每个框的裁剪图转成 base64依次调 TaoToken 的模型对话接口。提示词可以设计成“这是一件博物馆文物请从材质、器型、纹饰、可能年代四个方面描述用中文回答。” 我试过用这个流程处理一批陶瓷器图片模型能识别出青花、粉彩、龙泉青瓷等细分类型虽然不能替代专家鉴定但作为编目辅助已经很有价值。如果你要做批量处理建议把 API 调用做成异步的避免阻塞界面。Streamlit 本身对异步支持一般可以用concurrent.futures.ThreadPoolExecutor开线程池每个文物一个线程最后汇总结果。注意控制并发数太高可能触发限流我一般设 4-6 个并发。对于长期迭代的项目Coding Plan 提供了更稳定的调用额度适合把文物检测系统做成持续运行的服务。你可以到https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan了解详情。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc里面有各语言的示例代码和错误码说明。最后说一个实用技巧在 Streamlit 里用st.cache_resource缓存 YOLO 模型和 OpenAI client避免每次请求都重新加载。模型加载一次大约 2-3 秒缓存后第二次请求就很快了。另外 API Key 不要硬编码在代码里用st.text_input(typepassword)让用户输入或者从环境变量读。如果部署到服务器用.streamlit/secrets.toml管理密钥不要提交到 Git。整个系统跑通后你可以把 Streamlit 部署到内网服务器博物馆的工作人员通过浏览器就能用。如果要做成移动端Streamlit 的响应式布局在手机上也能凑合用但体验不如原生 App。后续如果要加新文物类别只需要在 OpenHeritage 基础上补充标注数据重新跑一遍预处理和训练脚本模型更新后替换权重文件即可界面和接口层不用动。
返回列表