十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

自建众包图像标注系统:从设计到部署避开Zip解压与编码坑

自建众包图像标注系统:从设计到部署避开Zip解压与编码坑 简介这是一套面向人工智能与计算机视觉方向开发者、高校科研人员及数据工程实践者的众包图像标注平台开源实现解决小团队或教学场景下高质量标注数据集构建难、协作流程缺失的问题。资源共411个文件压缩包大小10MB以149个Java后端服务代码为主含任务调度、用户权限、标注审核等核心模块辅以55个JavaScript前端交互逻辑、39个JPG/PNG原始样本图与标注示例、12个HTML页面模板及85个PNG界面资源完整覆盖从图片上传、众包分发、多人协同标注到结果质检的全流程。已有119人学习下载源码结构清晰包含Gradle构建配置、多时间戳标注日志样本如2018-04-28_19-13-40系列及典型数据处理工具脚本可直接部署学习系统架构设计也可快速定制适配目标场景的数据采集与标注规范。 标注听上去是个脏活累活但如果做AI视觉项目它就是你绕不过去的护城河。我最近把一个内部用了很久的众包图片数据集标注网站完整整理了一遍代码、部署脚本、说明文档一起打了个压缩包分享出去。这不是什么大厂级产品就是一个能真正跑起来、能支撑多人同时干活、能导出标准格式数据集的开源众包标注系统。这篇文章就当一份完整交付说明把网站的设计思路、技术选型、质量控制、以及打包交付时容易踩的坑一次讲透。适合正在做目标检测、图像分割项目但又不想用商用标注平台、想自己掌握数据主权的人参考。1. 这个网站的诞生背景数据标注有多痛众包标注就多有必要1.1 一个人标注的绝望我之前做过一个工地质安监测的项目需要标注几万张图片里的安全帽、反光衣、挖掘机。第一版方案是拿LabelImg本地一张一张画框画到三千张的时候我就崩溃了。不是技术上有多难是这种纯粹的重复劳动极其消耗耐心而且人一疲劳漏标错标的概率直线上升生成的数据集还得回头去质检隐性成本非常高。更麻烦的是协作。团队里四个人每个人电脑上都有一份图片副本各自标完用微信传来传去文件名还经常带着最新最终版2这种后缀。等收集回来我一合并发现同一个标注标准在不同人手里根本没法统一有人把遮挡超过一半的物体也画了框有人只画完整的有人框得紧贴边界有人习惯留一圈白边。这样的数据集拿去训练模型不飘才怪。1.2 为什么选众包模式而不是采购商用平台市面上的商用标注平台我也试用过功能确实全但问题也很现实。一是按量收费几万张图算下来成本不低二是数据隐私工地的监控截图、医疗影像这类数据往往不允许托管到第三方平台三是定制化程度低我想要的检测框属性标签组合在通用平台上操作起来反而不顺手。所以决定自己做一套轻量级的众包标注系统。这里的众包不一定是面向互联网的众包也可以是公司内部多人协作、学生团队分工标注、或者外包标注团队接入。核心诉求就三条多人在线并发标注、任务能拆分能追踪、导出格式直接喂给训练脚本。数据全部存在自己的服务器上完全可控。1.3 需求收敛我不是在做一个LabelImg网页版明确了背景之后我把需求收敛成一张表这也决定了整个网站的骨架需求说明优先级多人账号与角色管理员、标注员、审核员高图片批次管理按批次/类别导入图片高在线标注工具矩形框、多边形、分类标签、属性开关高任务分派与进度自动分配任务看每个人的完成量高质量控制黄金题、冗余标注、差异复核高数据导出COCO / YOLO / Pascal VOC / CSV高移动端兼容能用就行不强求低现在回头看当时最大的一个正确决定就是把质量控制当成一级功能来做而不是后期补丁。后面我单开一章讲这部分因为这是众包标注网站和单人标注工具最本质的区别。2. 核心功能拆解从注册登录到导出数据集一条完整流水线2.1 角色权限与任务分派逻辑整个系统有三类角色。管理员负责导入图片、配置标注规范、发布任务、审阅异常结果标注员只看到分配给自己的任务按规范画框打标签看不到别人成果审核员可以对结果进行抽检或全检退回不合格的重新标注。任务分派这块我一开始用的是最简单的平均分配第1张给A第2张给B轮流来。后来发现不行因为图片难度不均有的人分到全是密集小目标图片一天干不了几张进度条很难看。换成按队列领取模式后好很多——每个人打开任务页系统从公共队列里弹一张图给他标注完就回收再弹下一张。配合每张图的预计耗时系数管理员先跑一遍估算能粗略做到负载均衡。2.2 标注画布核心交互就四个动作标注器是网页的核心我用Canvas实现了矩形框与多边形两种标注方式。矩形框最简单鼠标按下为起点拖拽松开为对角点右侧浮出表单选择类别和属性。多边形稍微麻烦一点点击落点、双击闭合再用橡皮筋线预览当前边的走向避免画歪。这里有一个细节很值得说标注坐标一律存归一化值而不是像素值。因为同一张图标注员可能在不同分辨率的屏幕上看导出后再切图、缩放、数据增强都方便。归一化就是从0到1的坐标比例真要做还原乘以图片宽高就行。标注数据在浏览器端组织成JSON每张图能标任意数量的目标。保存请求会把整个JSON一并发到后端而不是每画一个框就请求一次。因为频繁请求会在标注高峰期把数据库打爆本地攒着、标完一张图提交一次这对用户体验和后端压力都是最优解。2.3 审核流程设置争议区审核员进入审核界面时不是看全部标签而是看系统标记的异常区域。我给每张图算了一个信心分依据包括标注员的个人通过率、该图是否被多人标注且结果差异大、是否有黄金题判定失败。信心分高的图直接进入通过队列信心分低的图审核员逐张看可以用保留修改退回三个操作处理。退回的图会回到任务池由原标注员或其他标注员重新标注同时给原标注员记一次负面记录。这个机制保证了数据集里混入烂数据的概率被压到极低。2.4 数据导出不能只有一种格式模型训练框架不同需要的标注格式也不一样。我实现了四种导出COCO JSON最常见的检测/分割数据集格式Detection2、MMDetection原生支持。YOLO txt每张图对应一个同名txt文件每行是类别id x_center y_center w h归一化。Pascal VOC XML老牌格式很多经典脚本依赖它。CSV简单属性分类任务直接打平表方便表格处理。导出时还会附带一份classes.txt或categories.json以及一张数据分布统计表包含每个类别的目标数量、每张图的平均目标数、异常样本列表。这东西训练前看一遍能帮你提前发现不少数据问题。3. 技术选型与关键实现细节3.1 后端Flask还是FastAPI我最终选了Flask。原因很实在项目规模不大、团队对Flask最熟、生态成熟文档多。要是新项目让我重新选一次我可能会用FastAPI因为异步性能更好、自带API文档但Flask这套在实际跑下来的稳定性完全够用。数据库先用SQLite起步因为在标注项目早期并发量远没有到数据库瓶颈SQLite零配置、单文件备份方便非常适合开发期和内部小团队使用。如果标注员人数上了几十、并发写任务记录变多再迁移到PostgreSQL或MySQL就行。热词里有人问mysql-8.0.46-winx64.zip怎么装那个zip解压后确实要在命令行初始化但我的建议很简单初期用SQLite等真的扛不住了再上MySQL不要一上来就给自己加维护负担。3.2 前端原生JS加Canvas不引重型框架标注页面的交互复杂度其实很高但我不想引入前端工程化的整套链路所以用了原生JavaScript模块化加Canvas渲染。所有页面共用一个后端模板标注页面单独加载标注脚本。图片加载用懒加载加按需加载任务队列里一次只加载当前图片和下一张图片不然浏览器内存会爆。标注框的渲染我维护了一个标注对象数组每次鼠标操作后重新绘制整个画布。很多人担心这样性能不行但在2560像素宽以内的图片上实测几个密集场景也就是几毫秒的绘制时间根本没有问题。真正影响性能的是大图缩放和Canvas的像素比适配这里我做了Retina屏适配把Canvas的物理像素设为显示像素乘devicePixelRatio否则在高分屏上标注框会有明显的模糊感。3.3 关键代码保存标注与导出COCO保存标注的后端接口大概是这样的app.route(/api/annotation, methods[POST]) def save_annotation(): data request.get_json() image_id data[image_id] boxes data[boxes] # [{category, x, y, w, h}, ...] annotator g.user.id # 同一张图同一人重复提交则覆盖 existence Annotation.query.filter_by(image_idimage_id, annotatorannotator).first() if existence: existence.payload json.dumps(boxes, ensure_asciiFalse) existence.updated_at datetime.utcnow() else: db.session.add(Annotation(image_idimage_id, annotatorannotator, payloadjson.dumps(boxes), statuspending)) db.session.commit() return {code: 0}导出COCO的转换逻辑里最关键的一环是把归一化坐标还原成像素坐标。原始图片尺寸存在images表里如果不存这个信息导出时必须再读一遍图片文件才能拿到宽高非常耽误时间。所以导入图片时就要用Pillow读取并记录宽高后面导出就顺畅了。这个经验看起来不起眼实际帮我们省了很多次返工。3.4 部署方式一个zip包能做到什么程度整个项目打成一个zip包包内结构是annotation-site/ ├── app.py # Flask入口 ├── models.py # 数据库模型 ├── requirements.txt # Python依赖 ├── static/ │ ├── css/ │ ├── js/annotator.js # 标注画布逻辑 │ └── uploads/ # 图片存储目录 ├── templates/ │ ├── login.html │ ├── dashboard.html │ └── annotate.html ├── tools/ │ ├── import_images.py # 批量导入图片脚本 │ └── export_dataset.py # 导出COCO/YOLO/VOC/CSV └── README.md在Linux服务器上解压后三个命令就能跑起来unzip annotation-site.zip -d /data/www/ cd /data/www/annotation-site pip install -r requirements.txt python app.py默认监听5000端口浏览器打开就能用。README里写了如何用gunicorn加nginx做生产级部署也写了Windows下的waitress方案。这已经是我能给出的最轻量部署路径了整个项目跑通不超过十分钟。4. 众包质量控制的四个关键手段以及我踩过的坑4.1 黄金题埋雷式质检黄金题是众包平台最经典的质量控制手段。我在每次导入图片时允许管理员额外上传一组已知正确答案的图片系统会把它们随机混入普通标注任务中标注员根本不知道哪张是测试。每完成一张黄金题系统立即对比结果对框的命中率用IoU两个框的交并比评估。IoU大于0.7认为是基本正确类别对且IoU大于0.5算合格。一个标注员如果连续多张黄金题不过关系统会自动降低他的任务配额管理员会收到提醒。跑了一个月后我们发现这个方法对标注质量的保障比事后抽检有效得多因为问题在被批量制造前就被拦截了。4.2 冗余标注与一致性合并黄金题控制的是个体可靠性但有些图本身就有歧义——比如密集人群里两个人界限不清比如夜间图目标只有半边。这种图怎么设黄金题都不够所以我们对随机抽取的15%图片做三人重复标注。三人标完后系统做一次基于IoU的聚类合并所有框两两计算IoUIoU大于0.5的框归为一个簇簇内框取平均坐标作为最终值同时统计这三个人的分歧度。如果某个簇只包含一个人的框而另两人都没标大概率是那个人标错了直接标为待审核。这个机制最大的价值是它能自动告诉你哪些图难。我们把每张图的争议指数记在表里训练完模型后发现那些争议大的图恰恰是模型最容易预测错的图。所以后来我们主动加刷争议大的图让更多人标注、讨论确定规则数据集质量又上了一个台阶。4.3 标注员评分与奖惩每个标注员有四个维度的分数总标注量、平均单张耗时、黄金题通过率、被审核退回次数。前两者反映效率后两者反映质量。我写了一个简单的加权公式把这些分数综合成一个0到100的信用分。信用分主要用来做调度信用分高的标注员优先分到难图信用分低的只能领简单图低于60分自动暂停。这套机制听起来有点管理味但对众包团队非常管用。把标准定好、规则透明标注员自己也会更认真因为刷量不刷质的收益会越来越低。4.4 我之前踩的坑标准定义不清晰这个坑几乎是所有标注项目都逃不掉的。我们第一次做标注规范时写在物体外接矩形上画框边距适当结果适当这两个字毁了一个批次——有人留1像素有人留15像素。后面改成画框时贴住目标的外边界目标边缘包含在框内留白不超过目标宽度的5%还配了三张示例图标准框、太紧的框、太松的框。从那以后标注一致性肉眼可见地提升。这个教训说明众包网站的功能再强也只是工具。标注规范的清晰程度直接决定了最终数据集的可用性。给标注员看的规范文档写得像写给小学生看一样清楚一点都不丢人。5. 交付那个zip包拆包、解压与边界问题5.1 为什么用zip而不是其他压缩格式项目包我选了zip格式最重要的原因是它最通用。Windows双击能解压macOS自带能解压Linux有unzip命令连手机上都有各种工具支持。相比rar需要额外装WinRAR、7z需要装7-Zipzip是零门槛的交付格式这也是最适合面向不确定用户群体的分发方式。但zip也有一个隐藏的坑如果用户下载不完整比如网盘中转时文件截断、或者被微信/QQ传文件时损坏解压时就会看到一个很经典的报错——file is not a zip file。5.2 file is not a zip file到底是什么问题这个报错我在很多地方见过有人问。它的本质是解压程序读取文件头或文件尾时发现里面的内容不符合zip格式的魔数。zip文件的开头固定是PK\x03\x04也就是PK两个字符后跟03 04结尾要有end of central directory记录。如果你用文本编辑器打开一个报错的zip看到的是HTML——那就说明下载到的根本不是zip而是服务器返回的404页面被浏览器悄悄存成了.zip后缀。排查方法很简单在Linux下执行file suspicious.zip如果输出显示HTML document或者data基本可以断定文件已经损坏或根本不是zip。这时候最好的办法是重新下载而不是用什么修复工具硬修。如果损坏不严重可以试一下zip -FF damaged.zip --out repaired.zip来做恢复但成功率并不高尤其是尾部记录丢失的情况下。有一个热词叫could not find EOCD也是同一类问题——EOCD就是zip文件末尾的end of central directory程序找不到它就认为不是合法的zip文件。说白了都是文件不完整。开局一张图内容全靠拼zip也是一样头尾都缺了就不该指望它能完整恢复。5.3 中文文件名乱码的来龙去脉zip包里的中文文件名乱码是个历史悠久的问题。zip规范早期并没有强制规定文件名编码Windows的老压缩工具用GBK编码存文件名Linux和macOS解压时默认按UTF-8解码于是中文名就变成一串锟斤拷或者乱码。解决方式有两种打包时用较新的压缩软件让zip带上UTF-8的标志位zip 3.0以上版本支持解压时指定编码。Linux下可以用unzip -O GBK来解老zipunzip -O GBK archive.zip -d target_dir我这次打包时已经确认过项目包的文件名都是UTF-8编码用户在Linux/macOS/Windows新版解压软件下打开应该都不会乱码。但如果哪天你从网上下载的老项目包乱码了先别急着删试试上面这个命令。5.4 分卷zip、加密zip与解密助手项目包有时候因为体积大会被微信、QQ这类工具拆分成多个文件出现xxx.z01、xxx.z02这种后缀。遇到这种情况千万不要只解压那个.zip它只是第一个分卷。正确做法是让所有分卷放在同一目录下用7-Zip或WinRAR打开第一个.zip分卷工具会自动读取后续分卷。命令行下也可以用zip -s 0 split.zip --out merged.zip把分卷合并成单文件再正常解压。至于加密zip我的原则是开源源码包不应该加密加密是在为难自己的用户。如果确实有敏感数据要发建议单独加密单个文件而不是整个包。网上那些zip解密助手软件除非是你自己记得密码只是忘了否则绝大多数情况是在浪费时间和承担风险。真忘了密码还有思路是把你记得的部分位数减到最少然后用hashcat之类的工具做掩码攻击但这不是正常人该走的路。理性建议是压缩包设密码前先想清楚密码管理用密码管理器不要真的指望事后能破解回来。5.5 GitHub下载的zip怎么装进conda环境热词里有人问GitHub下载的zip如何安装在conda base环境中这类问题也常见于拿到我这种项目包之后的安装阶段。如果是纯Python项目解压后一般看三步有没有requirements.txt有就执行pip install -r requirements.txt有没有setup.py或pyproject.toml有就执行pip install -e .开发模式安装如果什么依赖描述都没有那说明这个包根本不是用来安装的是直接运行源码的。在conda环境里也一样先在目标环境激活状态下安装别装到base里。除非你确定要在base里用否则长期来看给每个项目建独立环境才是可持续的习惯。我项目包里的README已经写了一行初始化命令照着做就行。6. 一次完整的部署验收实测说了这么多光有设计和代码还不够得真跑一遍才知道行不行。我在全新的CentOS服务器上做了一次从零部署的验收。第一步上传zip包到服务器执行unzip annotation-site.zip。这一步顺便验证了包本身没有损坏文件名没有乱码。第二步创建虚拟环境并安装依赖。机器上有Python 3.9python3 -m venv venv然后pip install -r requirements.txt装Flask、SQLAlchemy、Pillow等十几个依赖耗时约两分钟。第三步初始化数据库。项目自带一个init_db.py脚本创建管理员账号导入测试图片。因为图片上传目录也是脚本自动建的这一步没报错。第四步启动服务。开发模式直接python app.py生产模式我用gunicorn起了四个worker前面挂nginx做静态文件服务和反向代理。整个流程里最耗时的其实是图片导入。我测试导入了一万张工地监控截图Pillow逐张读取尺寸再存入数据库用时约五分钟。后来我在import_images.py里加入了多线程读取把时间压缩到了两分钟以内。对图片数据集的导入这算是值得花的力气。随后我创建了三个标注员账号各自登录同时在线标注同一批任务。测试结果三个浏览器窗口并发提交后端响应稳定没有出现死锁或者数据丢失。管理员后台的进度看板和每个人的工作量统计也都实时更新。导出功能跑了COCO和YOLO两种格式直接丢进YOLOv8的训练脚本里跑了一个epoch标注格式完全正确。说实话这套系统拿来做一个正式的数据标注平台肯定比不了商业产品那么多花哨的功能。但对于数据集规模在几万到几十万张、团队人数在几十人以内的内部众包场景它是完全够用的而且所有数据都在自己手里后续要加什么接口都方便。我的个人经验与后续扩展方向如果让我给准备自己搭众包标注网站的人提一个建议我会说先定义清楚你的数据管线和质量标准再写代码。比如导出格式需要哪些、黄金题比例多高、不同类别的IoU阈值是多少这些东西在画布实现之前就要定好。画布是衣服数据管线是骨架骨架歪了衣服再好看也没用。从代码层面这个项目后续还有几个很自然的扩展方向。一是引入标注预填充模型用YOLO或SAM先跑一遍自动标注标注员只负责修正速度可以提升好几倍二是把任务队列改成基于Redis的分布式队列进一步支撑更大规模的众包三是加入评审争议的讨论区让标注员在遇到歧义图时可以互相留言把规则沉淀成文档。这些扩展都有现成库可以接架构上不冲突。最后再说一个关于zip交付的小技巧在你准备把项目包发出去之前先在另一台机器上执行一次unzip -t package.zip做完整性测试。这个命令会逐文件校验CRC任何损坏都会提前暴露。别问我为什么强调这个我第一次发包时就是直接拖进微信传输结果对方下载后报file is not a zip file一查是传输过程中文件被截断了。从那以后发布前校验成了我雷打不动的习惯。本文还有配套的精品资源点击获取
返回列表