label-studio

label-studio社区版和企业版比较

基于UIE的命名实体识别下的label-studio-ml教程

Specific-examples-for-pre-annotations

Label Studio是一个开源的,可配置的数据注释工具,其目的是使您能够使用标准化输出格式的最方便的界面标记不同类型的数据

特色:支持多人协作,支持主动学习、支持多种的标注的任务

缺点:在目标检测标注时,好像没有十字辅助线

支持的标注任务,包括了如下所示,具体的见官方git介绍

支持的标注任务

label studio 结合 MMDetection 实现数据集自动标记、模型迭代训练的闭环

Label Studio使用技巧

关键的两个git仓库,其中label-studio的官方文档在这里

  1. label-studio, 进行普通的图片标记工作,如果要使用其提供的辅助预标记功能,则需要进行后续配置
  2. label-studio-ml-backend,主要提供深度学习服务,包括预标记模型训练,结合前端形成模型迭代训练的主动学习效果

版本

1
2
3
4
5
6
7
8
9
10
11
-- 前端
label-studio==1.10.0.post0

-- 后端
label-studio-ml==1.0.9
gunicorn==20.1.0
rq==1.10.1

-- 下面这两个自己会安装
label-studio-converter==0.0.57
label-studio-tools==0.0.3

前后端结合可以达到模型自动标记数据集、数据集更新迭代训练模型的闭环

前端

安装

1
2
3
pip install label-studio
# 如果安装psycopg2==2.9.5 失败, 可以先装下下面的库
sudo yum install python-devel postgresql-devel postgresql-libs gcc

环境安装完成后在任意位置打开命令行,使用以下命令启动 label studio

1
2
# 不指定路径时, 默认放在了 /root/.local/share/label-studio
label-studio --data-dir Label-Studio -p 80

其中 --data-dir 用于指定工作目录, -p 用来指定运行端口,运行成功后会当前目录会生成 Label-Studio 目录

浏览器打开label studio工作界面,创建用户后即可登录使用(这里的创建很简单,符合规范就可以)

文件夹中主要包括了export和media文件夹,以及一个label_studio.sqlite3数据库文件

数据库配置

数据库默认使用的是sqlite3,还可以配置其他的关系库,如PostgreSQL等,通常如果你想标注数百万个任务,或者预计会有很多并发用户,或者你计划在现实生活中的项目中工作,那么使用PostgreSQL数据库

官方提供的设置pg的环境变量好像不起效果,不知道把数据库建哪里去了,这里直接在命令行带上pg库的参数

1
DJANGO_DB=default POSTGRE_NAME=label_studio_db POSTGRE_USER=postgres POSTGRE_PASSWORD=xxx POSTGRE_PORT=5432 POSTGRE_HOST=192.168.123.xx LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/home/huangyc/label_ws label-studio start --data-dir /home/huangyc/label_ws -p 8080

LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT用于指定本地文件的位置,项目设置里面配置本地路径为/home/huangyc/label_ws/media(这只是个例子好像配成/home/huangyc/label_ws/media/upload也是一样的),此时可以访问

1
2
# 注意?d后面的路径是LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT配置的路径后面继续开始
http://192.168.123.xx:8080/data/local-files?d=media/upload/3/demo.jpg

当前目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
(label38) [root@uslave02 label_ws]# tree -d
.
├── export
└── media
├── avatars
├── export
└── upload
├── 1
├── 3
├── demo.jpg
└── 4

8 directories

注意这里需要提前创建好数据库label_studio_db,启动完成控制台输出如下的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
label-studio start --data-dir /home/huangyc/label_ws -p 8080
=> Database and media directory: /root/.local/share/label-studio
=> Static URL is set to: /static/
=> Database and media directory: /home/huangyc/label_ws
=> Static URL is set to: /static/
Starting new HTTPS connection (1): pypi.org:443
https://pypi.org:443 "GET /pypi/label-studio/json HTTP/1.1" 200 56156
Initializing database..
Performing system checks...

[2023-03-29 05:01:46,560] [django::register_actions_from_dir::97] [INFO] No module named 'data_manager.actions.__pycache_'
[2023-03-29 05:01:46,560] [django::register_actions_from_dir::97] [INFO] No module named 'data_manager.actions.__pycache_'
System check identified no issues (1 silenced).
March 29, 2023 - 05:01:46
Django version 3.2.16, using settings 'label_studio.core.settings.label_studio'
Starting development server at http://0.0.0.0:8080/
Quit the server with CONTROL-C.

此时数据库label_studio_db里面会建好所有的表,比如htx_user存的就是注册的用户信息

如果要后台执行,可以新建一个start.sh脚本

1
DJANGO_DB=default POSTGRE_NAME=label_studio_db POSTGRE_USER=postgres POSTGRE_PASSWORD=xxx POSTGRE_PORT=5432 POSTGRE_HOST=192.168.123.xx label-studio start --data-dir /home/huangyc/label_ws -p 8080

然后执行nohup sh start.sh > log_start.log &

导入现有标注

将现有标注转为json格式可以用label-studio-converter工具,安装命令为pip install label-studio-converter

转换的例子,目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Q:.
├─images
├─ 1.jpg
├─ 2.jpg
└─labels
├─ 1.txt
├─ 2.txt
├─ classes.txt # 注意这里的classes.txt里面的类别顺序一定要正确,如果是从label-studio前端导出的,好像顺序会有问题,可以用notes.json去获取正确的顺序

# 代码为
annotation_labels = json_load_op('notes.json')['categories']
annotation_labels = sorted(annotation_labels, key=lambda k: int(k['id']))
annotation_labels = [anno['name'] for anno in annotation_labels]

命令行为

1
label-studio-converter import yolo -i project-1 -o ls-tasks.json --image-root-url /data/local-files?d=tmp/images

输出ls-tasks.jsonls-tasks.label_config.xml两个文件,第一个是数据的索引和标签信息,第二个是项目的标签配置文件

此时要把文件上传到LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT/tmp/images下才可以看到

如果这里转换出来的路径不对,可以修改label_studio_converter\imports\yolo.py.py下的

1
2
3
4
5
6
7
8
9
10
11
12
13
task = {
"data": {
# eg. '../../foo+you.py' -> '../../foo%2Byou.py'
"image": f"{image_root_url}{sp}{image_file_base}" # 改完后的
},
# 'annotations' or 'predictions'
out_type: [
{
"result": [],
"ground_truth": False,
}
]
}

不然会访问不到数据

如果数据没有标注信息,可以将数据传到LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT/tmp/images下,并生成如下json文件,并在项目界面导入该文件即可

1
2
3
4
[
{"image":"/data/local-files?d=tmp/images/175.jpg"},
{"image":"/data/local-files?d=tmp/images/696.jpg"}
]

Cloudreve数据导入

cloudreve私有云盘配置后台运行

安装配置Cloudreve

Cloudreve可助你即刻构建出兼备自用或公用的网盘服务,通过多种存储策略的支持、虚拟文件系统等特性实现灵活的文件管理体验

Linux 下,直接解压并执行主程序即可:

1
2
3
4
5
6
7
8
#解压获取到的主程序
tar -zxvf cloudreve_VERSION_OS_ARCH.tar.gz

# 赋予执行权限
chmod +x ./cloudreve

# 启动 Cloudreve
nohup ./cloudreve > start.log &

Windows下,直接解压获取到的 zip 压缩包,启动 cloudreve.exe 即可

Cloudreve在首次启动时,会创建初始管理员账号,请注意保管管理员密码,此密码只会在首次启动时出现。如果您忘记初始管理员密码,需要删除同级目录下的cloudreve.db,重新启动主程序以初始化新的管理员账户。或者使用./cloudreve --database-script ResetAdminPassword重置密码

Cloudreve默认会监听5212端口。你可以在浏览器中访问http://服务器IP:5212进入 Cloudreve。

以上步骤操作完后,最简单的部署就完成了。你可能需要一些更为具体的配置,才能让 Cloudreve 更好的工作

登录管理员,编辑存储策略下的Default storage policy,将存储文件名改为{originname},不然会默认会带上一些前缀

Cloudreve的文件实际上是存储在/home/huangyc/cloudreve/uploads/1下,在此目录下新建软链接label_ws到label-studio的工作目录下,至此

1
2
3
[root@uslave02 1]# ln -s /home/huangyc/label_ws/uploads label_ws
[root@uslave02 1]# pwd
/home/huangyc/cloudreve/uploads/1

此时,界面网页上可以看到label_ws文件夹,后续的项目文件可以传到这里,至此Cloudreve配置完成

label-studio项目设置

这里可以将label-studio项目的云存储位置设置为本地/home/huangyc/label_ws/uploads,然后导入一下json文件到项目

1
2
3
[
{"image":"/data/local-files?d=uploads/tricycle_samples/灭鬼之刃.png"}
]

最后通过Cloudreve,把文件灭鬼之刃.png上传到label_ws/tricycle_samples下就可以看到图片了,注意:这里的label_ws相当于/home/huangyc/label_ws/uploads,这就是软链接的魅力

新建标注项目

在 label studio 前端主页中选择创建项目,主要流程

  1. 填写项目基本信息
  2. 导入数据
  3. 选择标记模板: label studio内置了很多常见的深度学习标记模板

以下是目标检测的模板

1
2
3
4
5
6
7
8
9
10
<View>
<Image name="image" value="$image" zoom="true" showMousePos="true" zoomControl="true" rotateControl="true"/>
<View>
<Filter toName="label" minlength="0" name="filter"/>
<RectangleLabels name="label" toName="image">
<Label value="tricycle" background="#ca9eff" category="0"/>
<Label value="person" background="#50b01c" category="1"/>
</RectangleLabels>
</View>
</View>

category很重要,能保证标签的顺序,这里要从0开始

此时我们已经可以通过 label studio 进行普通的图片标记工作,如果要使用其提供的辅助预标记功能,则需要进行后续配置

labelstudio-ui

后端

介绍

label studio ml是label studio的后端配置,其主要提供了一种能够快速将AI模型封装为label studio可使用的预标记服务(提供模型预测服务)

label-studio后端配置

安装: 依赖了rq库和redis库,注意有些库的版本不能过高

1
pip install label-studio-ml

模型创建

创建后端模型

导入相关库

1
2
3
4
5
6
7
8
9
10
11
12
13
import os

from PIL import Image
from label_studio_ml import model
from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.utils import get_env
from label_studio_tools.core.utils.io import get_local_path

model.LABEL_STUDIO_ML_BACKEND_V2 = True

os.environ['HOSTNAME'] = 'http://192.168.123.xx:8080'
os.environ['API_KEY'] = 'TOKEN,在项目的settings里面复制'
os.environ['LABEL_STUDIO_ML_BACKEND_V2'] = 'True'

模型类

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
class MyModel(LabelStudioMLBase):

def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
# 按 mmdetection 的方式加载模型及权重
print(kwargs)
print("初始化完成")

def predict(self, tasks, **kwargs):
# 获取待标记图片
print("开始预测")
images = [get_local_path(task['data']['image'], hostname=HOSTNAME, access_token=API_KEY) for task in tasks]

results = []
all_scores = []

# 这里只是示例, 返回结果都一样
for img_path in images:
for _ in range(1):
w, h = Image.open(img_path).size
pixel_x, pixel_y, pixel_width, pixel_height = convert_to_ls(0, 0, 200, 250, w, h)

result = {
'id': '0', # 必须为 str,否则前端不显示
'from_name': 'label',
'to_name': 'image',
'type': 'rectanglelabels',
'value': {
'rectanglelabels': ['tricycle'],
'x': pixel_x, # xy 为左上角坐标点
'y': pixel_y,
'width': pixel_width, # width,height 为宽高
'height': pixel_height
},
'score': 0.95
}
results.append(result)
all_scores.append(0.95)

print(tasks)
print(kwargs)
avg_score = sum(all_scores) / max(len(all_scores), 1)
results = [{
'result': results,
'score': avg_score
}]
return results

def fit(self, completions, num_epochs=5, **kwargs):
""" 模型训练 """
print("开始训练")
if self.gen_train_data(project_id):
# 训练模型
return {'model_path': r'\runs\detect\TriCycle\weights\best.pt'}
else:
raise "gen_train_data error"

def gen_train_data(self, project_id):
""" 获取数据训练数据 """
print("获取数据 project_id", project_id)
import zipfile
import glob
download_url = f'{HOSTNAME.rstrip("/")}/api/projects/{project_id}/export?export_type=COCO&download_all_tasks=false&download_resources=true'
response = requests.get(download_url, headers={'Authorization': f'Token {API_KEY}'})
zip_path = os.path.join(conf['workdir'], "train.zip")
train_path = os.path.join(conf['workdir'], "train")

with open(zip_path, 'wb') as file:
file.write(response.content) # 通过二进制写文件的方式保存获取的内容
file.flush()
f = zipfile.ZipFile(zip_path) # 创建压缩包对象
f.extractall(train_path) # 压缩包解压缩
f.close()
os.remove(zip_path)
if not os.path.exists(os.path.join(train_path, "images", str(project_id))):
os.makedirs(os.path.join(train_path, "images", str(project_id)))
for img in glob.glob(os.path.join(train_path, "images", "*.jpg")):
basename = os.path.basename(img)
shutil.move(img, os.path.join(train_path, "images", str(project_id), basename))
return True

使用步骤

启动后端服务: 分为三步,生成服务代码+启动服务+连接服务+训练模型

第一步:生成服务代码

1
label-studio-ml init backend/model --script label_studio_backend/yolo_detection.py --force

label-studio-ml init 命令提供了一种根据后端模型自动生成后端服务代码的功能, model 为输出目录, --script 指定后端模型路径, --force 表示覆盖生成。该命令执行成功后会在 backend 目录下生成 model 目录

主要包括了_wsgi.py、docker-compose.yml、Dockerfile、yolo_detection.py等文件

第二步:启动服务

如果有依赖的文件,需要自己复制到 model 目录下,接着启动后端服务

1
2
3
label-studio-ml start backend/model --host 0.0.0.0 -p 8888
# or
python backend/model/_wsgi.py --host 0.0.0.0 -p 8888 # 方便debug

启动成功的话,控制台会输出如下的日志

1
2
3
4
5
6
7
8
9
10
=> ROOT =  Q:\pyCharmWS\object_detection\smart_city_management\label_studio_backend\backend\yolo-detector
=> LABEL STUDIO HOSTNAME = http://192.168.123.xx:8080
* Serving Flask app "label_studio_ml.api" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
[2023-03-29 14:21:59,286] [WARNING] [werkzeug::_log::225] * Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
[2023-03-29 14:21:59,286] [INFO] [werkzeug::_log::225] * Running on http://10.10.0.xx:8888/ (Press CTRL+C to quit)

PS: 测试的时候发现,直接执行_wsgi.py文件一样可以启动后端,跟踪label_studio_mlserver.py文件可以看到执行核心代码

1
2
3
4
5
6
7
8
9
10
def main():
args, subargs = get_args()

if args.command == 'init':
create_dir(args)
elif args.command == 'start':
start_server(args, subargs)
elif args.command == 'deploy':
if args.provider == 'gcp':
deploy_to_gcp(args)

第三步:连接服务

在我们创建的前端项目中依次选择 Settings -> Machine Learning -> Add model ,然后输入后端地址 http://10.100.143.xxx:8888/(这里是后端的地址和端口),点击保存

此时我们从前端项目中打开待标记图片,前端会自动请求后端对其进行标记(调用后端的 predict 方法),等待片刻后即可看见预标记结果,我们只需要大致核对无误后点击 submit 即可

如果觉得每次打开图片都需要等待片刻才会收到后端预测结果比较费时,可以在 Settings -> Machine Learning 设置中选择打开 Retrieve predictions when loading a task automatically ,此后前端会在我们每次打开项目时自动对所有任务进行自动预测,基本能够做到无等待

第四步:训练模型

在 Settings -> Machine Learning 中点击后端服务的 Start Training 按钮,即可调用后端模型使用已标记信息进行训练

也可以 Settings -> Machine Learning 中允许模型自动训练,但训练频率过高会影响程序效率

目标检测任务注意事项

predict返回的坐标为convert_to_ls转换后的坐标值

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
# convert from LS percent units to pixels 
def convert_from_ls(result):
if 'original_width' not in result or 'original_height' not in result:
return None

value = result['value']
w, h = result['original_width'], result['original_height']

if all([key in value for key in ['x', 'y', 'width', 'height']]):
return w * value['x'] / 100.0, \
h * value['y'] / 100.0, \
w * value['width'] / 100.0, \
h * value['height'] / 100.0

# convert from pixels to LS percent units
def convert_to_ls(x, y, width, height, original_width, original_height):
return x / original_width * 100.0, y / original_height * 100.0, \
width / original_width * 100.0, height / original_height * 100


# convert from LS
output = convert_from_ls(task['annotations'][0]['result'][0])
if output is None:
raise Exception('Wrong convert')
pixel_x, pixel_y, pixel_width, pixel_height = output
print(pixel_x, pixel_y, pixel_width, pixel_height)

# convert back to LS
x, y, width, height = convert_to_ls(pixel_x, pixel_y, pixel_width, pixel_height, 600, 403)
print(x, y, width, height)

例子

Labelstudio的UIE半监督智能标注方案本地版,赶快用起来啦

基于ner的模型实现

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v1.0
@author: huangyc
@file: uie_model.py
@Description:
label-studio-ml init backend/model --script uie_model.py --force
@time: 2023/12/14 17:04
"""
import json
import os
from typing import List, Dict

import requests
from basic_support.logger.logger_config import logger
from label_studio_ml import model
from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.utils import get_env

model.LABEL_STUDIO_ML_BACKEND_V2 = True

os.environ['HOSTNAME'] = 'http://192.168.xx.xx:8080'
os.environ['API_KEY'] = 'd818922361ade7e2d570346d2bd6fe1668fd4e81'
os.environ['LABEL_STUDIO_ML_BACKEND_V2'] = 'True'

HOSTNAME = get_env('HOSTNAME')
API_KEY = get_env('API_KEY')
ROOT = os.path.join(os.path.dirname(__file__))

model_url = 'http://region-46.seetacloud.com:12851/xxx'


def anno(sentences: List[str]) -> List[Dict]:
"""
content = ["2018年10月12日,当事人办理个体工商户营业执照,经营场所位于金湖县园林南路5号自北向南第10间,经营范围为食品经营,新鲜水果零售等。"]
resps = [{'日期时间': [{'text': '2018年10月12日', 'start': 0, 'end': 11, 'probability': 0.9213111485894672}],
'地名': [{'text': '金湖县', 'start': 33, 'end': 36, 'probability': 0.9889604263625813},
{'text': '园林南路', 'start': 36, 'end': 40, 'probability': 0.8983399204114662}]}]

:param sentences:
:return:
"""
x = requests.post(model_url, json=sentences)
if x.status_code == 200:
resps = x.json()['response']
else:
raise Exception("访问异常")

predicts = []
for resp in resps:
result = []
scores = []
for label, v in resp.items():
for iv in v:
score = iv['probability']
scores.append(score)
res = {
'from_name': 'label',
'to_name': 'text',
'type': 'labels',
'value': {
'start': iv['start'],
'end': iv['end'],
'score': score,
'text': iv['text'],
'labels': [label]
}
}
result.append(res)

avg_score = round(sum(scores) / len(scores), 4) if scores else 0.0
predicts.append({"result": result, 'score': avg_score, 'model_version': 'uie-ner-large'})

return predicts


class UieModel(LabelStudioMLBase):
def __init__(self, **kwargs):
super(UieModel, self).__init__(**kwargs)
# 按 mmdetection 的方式加载模型及权重
print(kwargs)
logger.info(f"初始化完成")

def predict(self, tasks, **kwargs):
# 获取待标记图片
sentences = []
for data in tasks:
sentence = data['data']['text']
sentences.append(sentence)

return anno(sentences=sentences)

def fit(self, completions: List[Dict], workdir=None, **kwargs):
"""
模型训练
:param completions: 标注的样本
:param workdir:
:param kwargs:
:return:
"""
logger.info(f"开始构建训练样本")
sample_path = self.gen_train_data_path(completions=completions)

return {'path': workdir, 'model_path': None, 'sample_path': sample_path}

def gen_train_data_path(self, completions: List[Dict]):
"""
获取数据训练数据
:param completions:
:return:
"""
samples = []

for completion in completions:
sentence = completion['data']['text']
m_id = completion['id']
created_at = completion["created_at"]
updated_at = completion["updated_at"]

labels = []
for annotation in completion['annotations']:
for label_info in annotation['result']:
labels.append(label_info['value'])

sample = {"text": sentence, "id": m_id, "label": labels, "created_at": created_at, "updated_at": updated_at}
samples.append(sample)
logger.info(f"构建训练样本完毕,开始输出到文件")

sample_path = "./doccano_ext.jsonl"
with open(sample_path, "w", encoding="utf-8") as outfile:
outfile.write(json.dumps(samples, ensure_ascii=False))

logger.info(f"样本路径为:{sample_path}")

return sample_path

doccano

doccano官网

Roboflow

如何使用 Roboflow 标注关键点