背景项目组要标记一大批数据,但我们都不是这方面的专家,需要工厂里质检员那些专业人士帮忙,让他们配置labelimg还是我们打包环境给他们都太麻烦了。还有一个问题就是数据同步,师兄他们用的是nas,外网环境的话实现起来就可能有各种问题了。
总的来说,为了摸鱼 ,额为了更多人协同标注,我们需要一个在线的方便的标注系统。
出于保密需要,本文有些内容无法公开,可能导致部分文段逻辑有问题
背景x2可以预见的是,如果这套系统能够稳定运行的话,那在以后就算毕业跑路了应该也会留着一直用下去,必须要保证它具备处理大规模数据的能力。而这个系统默认使用内部的SQLite来存储数据,并且使用它自带的本地存储来存数据
显而易见,以上两个都会不可避免的遇到IO瓶颈,我们也不知道这套系统在后期要承受多大的数据量,那最简单的方法就是用业界更成熟的解决方案了,这里我用到的就是PostgreSQL + Minio,应该是这套方案的完全体了
搭建因为我一开始的时候是想测试我自己另一套方案的,所以说走了很多无用功,并且部署上也有点散,如果你觉得有些地方不太符合你的实际需要请 胆修改
为避免环境依赖导致出现不可预料的问题,本文所有过程均使用Docker完成
PostgreSQL我这里直接用1panel安装了,安装完创建个数据库
调整数据库连接数上限在后续可能会遇到数据库连接达到上限的情况
默认是100,可以改高一点:
postgresql.conf Minio 安装在新版本中移除了前端页面,最后一个有前端页面的版本是minio/minio:RELEASE.2025-04-22T22-12-26Z ,拉取下镜像运行
听说rustfs也不错,如果有兴趣可以折腾下
啥?网络又超时了?可以用下:使用教程 - 境内 Docker 镜像状态监控
注意
在新版本中docker compose已经作为docker的一个插件存在了,并不需要额外安装。
准备一个空间足够大的地方放置Minio,创建docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 services: minio: image: minio/minio:RELEASE.2025-04-22T22-12-26Z container_name: minio restart: unless-stopped ports: - "9000:9000" - "9001:9001" volumes: - ./data:/data - ./certs:/root/.minio/certs environment: - MINIO_ROOT_USER=youruser - MINIO_ROOT_PASSWORD=yourpassword command: server --console-address ":9001" /data networks: default: name: minio_net driver: bridge
随后新建两个桶,分别是作为原存储和目标存储。命名随意。
源存储:用来存储原始数据的位置 目标存储:用来存放标注数据的位置
在侧边栏里找到Administrator > Policies,创建一个新策略,在Raw Policy里填写以下内容(aaaaaa换成你的“原存储”桶名称)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "Version" : "2012-10-17" , "Statement" : [ { "Effect" : "Allow" , "Action" : [ "s3:GetObject" , "s3:ListBucket" , "s3:PutObject" ] , "Resource" : [ "arn:aws:s3:::aaaaaa" , "arn:aws:s3:::aaaaaa/*" ] } ] }
外网访问 设置反向代理内网使用ip访问倒无所谓,外网就没办法了,同时为了实现下文的内外网分流,必须要绑个域名
那就反代一下呗。我用的是1panel,图形化界面点下就差不多了
但你在添加存储桶时包能遇到这个问题:
代理日志显示:
这个问题是Nginx在处理HEAD请求时会将其转换为 GET 请求,以便从缓存中获取数据,为了禁用转换,需要设置proxy_cache_convert_head为off
我放一份我的配置文件以供参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 location ^~ / { proxy_pass http://127.0.0.1:9000; proxy_set_header Host {Host}:{PORT}; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_set_header X-Forwarded-Host $host ; proxy_set_header X-Forwarded-Port $server_port ; proxy_set_header X-Forwarded-Proto $scheme ; proxy_set_header X-Forwarded-Scheme $scheme ; proxy_set_header Upgrade $http_upgrade ; proxy_set_header Connection $http_connection ; proxy_set_header Early-Data $ssl_early_data ; proxy_set_header Accept-Encoding "" ; proxy_http_version 1 .1 ; proxy_ssl_protocols TLSv1.2 TLSv1.3 ; proxy_ssl_session_reuse off ; proxy_ssl_server_name on ; proxy_cache_convert_head off ; add_header Alt-Svc 'h3=":443"; ma=2592000' ; }
注:
copy的话记得改这一行:proxy_set_header Host {Host}:{PORT}; Minio在搭配cloudflare使用时由于cf会改accept-encoding请求头导致SignatureDoesNotMatch,详见解决 MinIO 反代 403 SignatureDoesNotMatch | 潇然工作室 内外网分流在实际使用过程中,数据集在访问时要通过预签名后的链接来访问,毕竟你也不想被人发现命名规律后把整个数据集爬走吧 。 而Label Studio(下称LS)用的是AWS4-HMAC-SHA256签名算法,并且 X-Amz-SignedHeaders包括了host,换句话说这个链接只能在一个域名里使用,这会导致以下问题:
在LS后台添加存储时填写的域名必须和实际访问的域名是同一个 无法通过多个域名实现内外网分流 当然,以上问题都可以通过修改LS代码实现,但我觉得不值得
而为了提供给外网访问,该域名势必要通过内网穿透实现,直接导致我们内网传数据时也要绕一大圈,造成没必要的开销
最优雅的解决方案就是直接跟网络中心报备,然后开防火墙,一步到位。代价就是写一大堆文件,而且给不给批还是另外一回事
退一步就是dns分流,但显而易见还是得跟人家网络中心的人打报告啊
那就来一个最粗暴的解决方法——改host
首先是LS的容器内部,通过添加以下行实现
docker-compose.yml 1 2 3 4 5 6 services: app: ... extra_hosts: - "host.docker.internal:host-gateway" - "your.access.domain:host-gateway"
然后改系统host,该步骤用来方便同服务器下其他程序连接(注意如果没有进行上一步该步操作,会覆盖容器内部的host导致无法连接)
/etc/hosts 1 127.0.0.1 your.access.domain
配置为目标存储虽然你大概率不会用它的原始文件,随便设置个本地存储就行了。不过我为了统一将数据存储在一起,也使用它来作为目标存储
说的高级一点,就是将业务服务与存储服务解耦,提高可维护性与后期扩展能力
所有流程走下来应该都不会碰到问题,直到你真正开始标注的时候狠狠地背刺你
LS 出于安全性考虑,需要配置可信S3域名后前端才可以查看详细的报错信息
该问题来自于LS默认启用了SSE,即服务端加密 ,而Minio默认不支持,可以参考MinIO对象存储加密实践_server side encryption specified but kms is not co-CSDN博客 这篇文章配置
但话又说回来了,我们有没有这个加密的必要?我觉得没有,直接取消加密就行
label_studio/io_storages/s3/models.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class S3ExportStorage(S3StorageMixin, ExportStorage): @catch_and_reraise_from_none def save_annotation(self, annotation): client, s3 = self.get_client_and_resource() logger.debug(f'Creating new object on {self.__class__.__name__} Storage {self} for annotation {annotation}') ser_annotation = self._get_serialized_data(annotation) # get key that identifies this object in storage key = S3ExportStorageLink.get_key(annotation) key = str(self.prefix) + '/' + key if self.prefix else key # put object into storage additional_params = {} self.cached_user = getattr(self, 'cached_user', self.project.organization.created_by) - if flag_set( - 'fflag_feat_back_lsdv_3958_server_side_encryption_for_target_storage_short', - user=self.cached_user, - ): - if self.aws_sse_kms_key_id: - additional_params['SSEKMSKeyId'] = self.aws_sse_kms_key_id - additional_params['ServerSideEncryption'] = 'aws:kms' - else: - additional_params['ServerSideEncryption'] = 'AES256' s3.Object(self.bucket, key).put(Body=json.dumps(ser_annotation), **additional_params) # create link if everything ok S3ExportStorageLink.create(annotation, self)
Label Studio因为这套系统并不只是我们团队内部在用,后期还要给人家在工厂的大佬们用来帮助我们标注和复核,所以这满屏幕的English不太行啊
直接搜索找到了这个仓库 ,但看issues里面说docker跑不起来,不过一看pr有个老哥已经做好了,那就直接用他的 吧
clone到本地,直接docker compose up就跑起来了,但问题是我还希望让他连接到数据库而非内置的sqlite。修改一些环境变量,然后就发现一跑就报错
因为compose里传入的环境变量,是运行时的环境变量,在构建时是无效的,而在构建时又有用到的这个迁移数据库(或者说是初始化/更新数据库)这个操作
那在他这里他就连不上数据库,自然也就无法完成以上的操作,我这里参考了这一篇文章 的解法,直接简单粗暴的改文件,终于跑起来了
ok,那接下来处理一下校外访问吧
我这里直接借助家里的服务进行中转,买别的服务先不说能不能报销吧,速度又慢又限流量
小tips
校园网限速逻辑有点问题,ipv4的上传速度限制在20mbps,但ipv6可以跑满物理连接上限
实测该方案能达到百兆的突发带宽
所以直接反代就ok了。。吗?
登录的时候一个csrf错误直接砸我脸上,网上也查不到相应的资料,好好好
然后在文档里面某个角落 翻到了这个LABEL_STUDIO_HOST的配置,但是配置了以后毫无卵用,因为它是用来生成分享链接的,并且在填了这个变量以后。在内网直接访问ip加端口的时候,也会直接跳转到外网的域名
那问题在哪呢?既然他能这么写就说明他肯定是能跑的,我跑不起来肯定是某些配置没调好,所以我直接在他代码里面搜了下csrf,然后真就有这么个精妙的环境变量配置啊 。再加上这个配置项以后就正常了。
环境变量设置逻辑大部分环境变量通过这个函数获取
比如配置环境变量abcd,你可以配置LABEL_STUDIO_abcd或HEARTEX_abcd或abcd 还有一小部分环境变量直接使用os.getenv获取,有时候配置了不生效可以翻下代码
去掉统计LS使用Sentry来收集错误,但在我实际测试时挺拖慢整体加载速度的,干脆禁用掉了
根据代码,需要设置FRONTEND_SENTRY_DSN环境变量为空
禁用注册设置DISABLE_SIGNUP_WITHOUT_LINK为True,后续注册时需要通过分享链接注册
S3可信域名为了方便前端检查错误,可以配置S3_TRUSTED_STORAGE_DOMAINS环境变量为对应域名
汉化该项目还是有很多不足之处的,我也改了一大堆地方,不过没法开源,凑合着用吧
美化这默认的登录页面不咋样,我干脆改回原版的了,反正不是用来看的,不那么丑就行
Demo可以参考进行配置:
docker-compose.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 services: app: build: . image: label-studio-chinese:latest container_name: label-studio-chinese restart: unless-stopped ports: - "8080:8080" volumes: - label-studio:/root/.local/share/label-studio:rw extra_hosts: - "host.docker.internal:host-gateway" - "your.access.domain:host-gateway" environment: - DJANGO_DB=default - POSTGRE_NAME= - POSTGRE_USER= - POSTGRE_PASSWORD= - POSTGRE_PORT=5432 - POSTGRE_HOST=host.docker.internal - LABEL_STUDIO_HOST=https://xxxx:port - CSRF_TRUSTED_ORIGINS=https://xxxx:port - FRONTEND_SENTRY_DSN= - DISABLE_SIGNUP_WITHOUT_LINK=True - S3_TRUSTED_STORAGE_DOMAINS=xxxx:port volumes: label-studio:
同时,我构建镜像的时候也碰到一堆依赖问题,可以试下我这个
Dockerfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 FROM node:20 AS frontend-builderWORKDIR /app COPY web/package.json web/yarn.lock ./web/ WORKDIR /app/web RUN yarn install --frozen-lockfile WORKDIR /app COPY . . WORKDIR /app/web RUN yarn build FROM python:3.12 -slimENV POETRY_VIRTUALENVS_IN_PROJECT=true \ POETRY_NO_INTERACTION=1 \ POETRY_HOME="/opt/poetry" RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources && \ sed -i 's|security.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libffi-dev \ libssl-dev \ curl \ git \ libjpeg62-turbo-dev \ zlib1g-dev \ libpng-dev \ libfreetype6-dev \ libtiff5-dev \ liblcms2-dev \ && rm -rf /var/lib/apt/lists/* RUN pip install --upgrade pip setuptools wheel RUN pip install --no-cache-dir poetry WORKDIR /app COPY . . RUN poetry install COPY --from=frontend-builder /app/web/dist ./web/dist ENV POETRY_SOURCE_URL=https://pypi.tuna.tsinghua.edu.cn/simple/RUN poetry run python label_studio/manage.py collectstatic --noinput RUN poetry run python label_studio/manage.py migrate EXPOSE 8080 CMD ["poetry" , "run" , "python" , "label_studio/manage.py" , "runserver" , "0.0.0.0:8080" ]
数据导入你可以在项目里直接导入,但这不是啥优雅的方案
或者把数据上传到Minio后在后台进行同步,但这样不利于与其他程序联动
我是在上传到S3后再用api提交任务到LS,好处就是我可以进行预标注那些操作
标注页面设置编辑器有自动补全,也有模板参考,应该不算很难上手,可以参考我的
1 2 3 4 5 6 7 8 9 10 11 12 <View > <View style ="display:flex;align-items:start;gap:8px;flex-direction:column-reverse" > <Image name ="image" value ="$image" zoom ="true" zoomControl ="true" rotateControl ="true" /> <RectangleLabels name ="label" toName ="image" showInline ="true" strokeWidth ="1" > <Label alias ="xxxx" showAlias ="true" value ="yyyy" background ="green" /> </RectangleLabels > </View > </View >
此时alias为实际标注类型,value为前端显示。感觉有bug
官方文档:Label Studio — Customize the Label Studio User Interface
预标注后台有个“模型”页面,用于辅助标注,也可以手动触发对现有数据进行标注
但这玩意折腾太麻烦了,我干脆搓了一个自动标注,核心就是用SAM3,效果还可以 整体分为主控端,采集端,处理端,不知道算不算“微服务”
设计的时候本着决不重复造轮子的原则,用了一大堆业界成熟的解决方法,比如用RabbitMQ来分发任务,在没ACK 之前就算天塌下来都能妥善处理信息,要手动实现的话也要费不少功夫
最后那群写文档的能不能写详细点,什么时候搞下i18n,一堆的bug能不能修下
技术债也有不少,比如导出数据集时的yolo with image是不包括图像的,我看了下代码,好像实现方式是对原始标注文件直接转换,但原始的标注文件又不包括图像。 但是他摆在那里应该是有用的吧,要不然真得大改了