加载中...
不想等待可以点我关掉

背景

项目组要标记一大批数据,但我们都不是这方面的专家,需要工厂里质检员那些专业人士帮忙,让他们配置labelimg还是我们打包环境给他们都太麻烦了。还有一个问题就是数据同步,师兄他们用的是nas,外网环境的话实现起来就可能有各种问题了。

总的来说,为了摸鱼,额为了更多人协同标注,我们需要一个在线的方便的标注系统。

出于保密需要,本文有些内容无法公开,可能导致部分文段逻辑有问题

背景x2

可以预见的是,如果这套系统能够稳定运行的话,那在以后就算毕业跑路了应该也会留着一直用下去,必须要保证它具备处理大规模数据的能力。而这个系统默认使用内部的SQLite来存储数据,并且使用它自带的本地存储来存数据

显而易见,以上两个都会不可避免的遇到IO瓶颈,我们也不知道这套系统在后期要承受多大的数据量,那最简单的方法就是用业界更成熟的解决方案了,这里我用到的就是PostgreSQL + Minio,应该是这套方案的完全体了

搭建

因为我一开始的时候是想测试我自己另一套方案的,所以说走了很多无用功,并且部署上也有点散,如果你觉得有些地方不太符合你的实际需要请 胆修改

为避免环境依赖导致出现不可预料的问题,本文所有过程均使用Docker完成

PostgreSQL

我这里直接用1panel安装了,安装完创建个数据库

创建数据库
创建数据库
注意

不要用宝塔的,它安装不带pg_trgm依赖

调整数据库连接数上限

在后续可能会遇到数据库连接达到上限的情况

报错
报错

默认是100,可以改高一点:

postgresql.conf
1
max_connections = 1000
进入设置页面调整
进入设置页面调整

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,图形化界面点下就差不多了

但你在添加存储桶时包能遇到这个问题:

报错
报错

代理日志显示:

403
403

这个问题是Nginx在处理HEAD请求时会将其转换为 GET 请求,以便从缓存中获取数据,为了禁用转换,需要设置proxy_cache_convert_headoff

我放一份我的配置文件以供参考

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';
}

注:

  1. copy的话记得改这一行:proxy_set_header Host {Host}:{PORT};
  2. Minio在搭配cloudflare使用时由于cf会改accept-encoding请求头导致SignatureDoesNotMatch,详见解决 MinIO 反代 403 SignatureDoesNotMatch | 潇然工作室
内外网分流

在实际使用过程中,数据集在访问时要通过预签名后的链接来访问,毕竟你也不想被人发现命名规律后把整个数据集爬走吧
Label Studio(下称LS)用的是AWS4-HMAC-SHA256签名算法,并且X-Amz-SignedHeaders包括了host,换句话说这个链接只能在一个域名里使用,这会导致以下问题:

  1. LS后台添加存储时填写的域名必须和实际访问的域名是同一个
  2. 无法通过多个域名实现内外网分流

当然,以上问题都可以通过修改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错误直接砸我脸上,网上也查不到相应的资料,好好好

CSRF错误页面
CSRF错误页面

然后在文档里面某个角落翻到了这个LABEL_STUDIO_HOST的配置,但是配置了以后毫无卵用,因为它是用来生成分享链接的,并且在填了这个变量以后。在内网直接访问ip加端口的时候,也会直接跳转到外网的域名

那问题在哪呢?既然他能这么写就说明他肯定是能跑的,我跑不起来肯定是某些配置没调好,所以我直接在他代码里面搜了下csrf,然后真就有这么个精妙的环境变量配置啊。再加上这个配置项以后就正常了。

相关代码
相关代码

环境变量设置逻辑

大部分环境变量通过这个函数获取

相关代码
相关代码

比如配置环境变量abcd,你可以配置LABEL_STUDIO_abcdHEARTEX_abcdabcd
还有一小部分环境变量直接使用os.getenv获取,有时候配置了不生效可以翻下代码

去掉统计

LS使用Sentry来收集错误,但在我实际测试时挺拖慢整体加载速度的,干脆禁用掉了

相关代码
相关代码

根据代码,需要设置FRONTEND_SENTRY_DSN环境变量为空

禁用注册

设置DISABLE_SIGNUP_WITHOUT_LINKTrue,后续注册时需要通过分享链接注册

创建邀请链接
创建邀请链接

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
# ---------- 阶段 1:构建前端 ----------
FROM node:20 AS frontend-builder

WORKDIR /app

# 1️⃣ 先复制依赖文件以利用缓存(只要 package.json / yarn.lock 不变就不重新安装)
COPY web/package.json web/yarn.lock ./web/

WORKDIR /app/web
RUN yarn install --frozen-lockfile

# 2️⃣ 再复制整个项目,以便构建时能访问外层文件
WORKDIR /app
COPY . .

# 3️⃣ 构建前端
WORKDIR /app/web
RUN yarn build


# ---------- 阶段 2:构建后端 ----------
FROM python:3.12-slim

# Poetry 环境设置
ENV POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
POETRY_HOME="/opt/poetry"

# 使用清华源替换官方 Debian 源
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


# ==========================
# 解决 pip install poetry 的段错误
# ==========================
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/*

# 升级 pip+setuptools,防止编译旧 wheels 崩溃
RUN pip install --upgrade pip setuptools wheel

# 安装 Poetry
RUN pip install --no-cache-dir poetry

WORKDIR /app

# 5️⃣ 复制完整项目代码
COPY . .

# 安装 Python 依赖
RUN poetry install

# 6️⃣ 从前端阶段复制已构建的静态文件
COPY --from=frontend-builder /app/web/dist ./web/dist

# 设置 PyPI 镜像加速
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

预标注

后台有个“模型”页面,用于辅助标注,也可以手动触发对现有数据进行标注

ML
ML

但这玩意折腾太麻烦了,我干脆搓了一个自动标注,核心就是用SAM3,效果还可以
整体分为主控端,采集端,处理端,不知道算不算“微服务”

设计的时候本着决不重复造轮子的原则,用了一大堆业界成熟的解决方法,比如用RabbitMQ来分发任务,在没ACK之前就算天塌下来都能妥善处理信息,要手动实现的话也要费不少功夫

最后

那群写文档的能不能写详细点,什么时候搞下i18n,一堆的bug能不能修下

技术债也有不少,比如导出数据集时的yolo with image是不包括图像的,我看了下代码,好像实现方式是对原始标注文件直接转换,但原始的标注文件又不包括图像。
但是他摆在那里应该是有用的吧,要不然真得大改了

眼巴巴
眼巴巴