YOLO 학습을 수행할 때 데이터를 얻기 위해 Roboflow나 Ai Hub에서 데이터셋을 얻어오곤 하는데 종종 데이터를 추가로 가공해야 할 일이 생긴다. 예를 들어 자동차 관련 데이터셋을 다운로드 받았는데 오토바이는 라벨링이 안되어있다던가.. 안전 관련된 데이터셋을 받았는데 사람이 라벨링이 안되어있다던가..
그럴 때 마다 Roboflow를 이용해서 편하게 라벨링 작업을 하는데 문제는 공공 데이터셋이나 사내 비공개 데이터셋을 쓰면 보안 문제로 사용이 제한될 때가 있다. 물론, 자체 MLOps나 라벨링 프로세스가 갖춰진 회사라면 별 문제가 되지 않겠지만... 그래서 얼마전부터 라벨 스튜디오를 내부망에 구축해서 사용해보고 있는데 오토라벨링도 되고 기능도 생각보다 만족스러워서 사용했던 내용을 공유하고자 한다.
본 글에서는 기존에 사용하던 YOLO 형식의 데이터셋을 가져와서 추가 라벨링을 수행하는 과정을 알아보도록 한다.
- YOLO 데이터셋의 이미지 데이터는 로컬 스토리지에 담고 주석 데이터는 JSON으로 변환
- 라벨 스튜디오 프로젝트와 로컬 스토리지 연동
- 기존 주석 데이터를 활용한 추가 라벨링 수행
Docker 컨테이너 생성
설치
먼저 git clone을 통해 label-studio 리포지토리를 설치한다.
git clone https://github.com/HumanSignal/label-studio
cd label-studio
docker-compose.yaml 파일 수정
그리고 docker-compose.yaml 파일을 열고 port와 environment를 수정한다.
nginx
ports 항목에 "80:80" 포트를 추가한다.
volumes 항목의 ./mydata:/label-studio/data 설정을 통해 호스트와 컨테이너 간의 볼륨 마운트 경로를 지정한다.
- ./mydata: 호스트 시스템의 경로 <- 원하는 경로로 바꿀 수 있다.
- ex) - ./label_studio_data:/label-studio/data
- /label-studio/data: 컨테이너 내부의 경로 <- 바꾸면 안된다.
nginx:
build: .
image: heartexlabs/label-studio:latest
restart: unless-stopped
ports:
- "80:80" # <-- 추가
- "8080:8085"
- "8081:8086"
depends_on:
- app
environment:
- LABEL_STUDIO_HOST=${LABEL_STUDIO_HOST:-}
# Optional: Specify SSL termination certificate & key
# Just drop your cert.pem and cert.key into folder 'deploy/nginx/certs'
# - NGINX_SSL_CERT=/certs/cert.pem
# - NGINX_SSL_CERT_KEY=/certs/cert.key
volumes:
- ./mydata:/label-studio/data:rw
- ./deploy/nginx/certs:/certs:ro
# Optional: Override nginx default conf
# - ./deploy/my.conf:/etc/nginx/nginx.conf
command: nginx
app
로컬 스토리지를 사용하기 위해 environment 에 아래의 항목을 추가한다.
LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/label-studio/data
app:
stdin_open: true
tty: true
build: .
image: heartexlabs/label-studio:latest
restart: unless-stopped
expose:
- "8000"
depends_on:
- db
environment:
- DJANGO_DB=default
- POSTGRE_NAME=postgres
- POSTGRE_USER=postgres
- POSTGRE_PASSWORD=
- POSTGRE_PORT=5432
- POSTGRE_HOST=db
- LABEL_STUDIO_HOST=${LABEL_STUDIO_HOST:-}
- LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true # <-- 추가
- LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/label-studio/data # <-- 추가
- JSON_LOG=1
# - LOG_LEVEL=DEBUG
volumes:
- ./mydata:/label-studio/data:rw
command: label-studio-uwsgi
실행
docker compose up -d
docker compose 명령어로 실행해준다.
이 때, PermissionError: [Errno 13] Permission denied: '/label-studio/data/media'와 같은 Permission 에러가 발생한다면
sudo chmod 777 mydata/
위 코드를 통해 docker-compose.yml 파일의 nginx 항목에서 설정했던 볼륨 마운트 경로의 권한을 수정해준 후 다시 실행한다.
프로젝트 생성 및 설정
프로젝트 생성은 아래 링크를 참조하여 생성하는데 단, 사전 주석 데이터를 불러오기 위해서는 미리 Data Import를 하면 안된다.
https://starlane.tistory.com/9#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%20%EC%83%9D%EC%84%B1-1
볼륨 마운트 경로에 로컬 스토리지 생성
호스트 컴퓨터의 label-studio/mydata 폴더 안에 추가할 데이터 이름을 가진 폴더를 만든다.
최종적으로는 위와 같은 형태가 되어야 하는데 미리 데이터를 넣어놔도 되지만 여기서는 먼저 로컬 스토리지를 등록 한 다음 데이터셋을 넣을 예정이다.
로컬 스토리지 추가
프로젝트의 Settings 버튼을 클릭하여 Cloud Storage 탭을 선택한 다음, Add Source Storage 버튼을 클릭한다.
Storage Type -> Local files 선택
Absolute local path -> 볼륨 마운트 경로 입력
File Filter Regex -> 필요한 경우 필터 입력
Treat every bucket object as a source file -> 로컬 스토리지만 연동 할 때는 체크해야 하지만, 사전 주석 데이터를 추가로 불러올 때는 체크하지 않는다.
설정을 다 했다면 Check Connection 버튼을 통해 볼륨 마운트 경로와 올바르게 연결이 되는지 확인 할 수 있다.
연결이 올바르게 되었다면 Save를 통해 저장한다.
이제 로컬 스토리지에 저장된 이미지 데이터를 불러오고자 할 때, http://localhost:8080/data/local-files/?d=<폴더>/<이미지 파일>.jpg 로 확인 할 수 있다.
사전 주석 데이터 가공
YOLO 포맷 -> JSON 변환
https://github.com/HumanSignal/label-studio-converter
라벨 스튜디오를 설치하면 자동으로 설치되는 Label Studio Converter라는게 있는데 우리는 이것을 사용해서 YOLO 주석을 라벨 스튜디오에서 사용되는 JSON 포맷으로 변환할 것이다.
여기서는 도커로 라벨 스튜디오를 돌리고 있기 때문에 라벨 스튜디오 컨버터가 없을 수도 있는데 pip로 설치하면 된다.
pip install label-studio-converter
라벨 스튜디오 컨버터를 설치했다면 아래와 같이 데이터셋 폴더를 구성한다.
데이터셋 폴더 설정이 끝났다면 아래의 명령어를 통해 JSON 파일을 생성한다.
label-studio-converter import yolo -i /dataset -o output.json --image-root-url "/data/local-files/?d=dataset/"
-i :변환할 YOLO 데이터셋이 있는 경로를 선택한다.
-o : 변환된 JSON 파일의 이름이다.
--image-root-url : 로컬 스토리지와 연동할 이미지 파일의 경로이다. 맨 뒤의 ?d= 부분의 이름을 연동할 로컬 스토리지 폴더 이름으로 지정한다.
라벨 스튜디오 컨버터 명령어 사용이 안 될 경우
pip 설치
pip install ujson PIL label-studio-converter
yolo_to_ls.py 파일 생성
# yolo to ls
import os
import ujson as json
import uuid
import logging
from PIL import Image
from typing import Optional, Tuple
from urllib.request import pathname2url
from label_studio_converter.utils import ExpandFullPath
from label_studio_converter.imports.label_config import generate_label_config
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('root')
default_image_root_url = '/data/local-files/'
def convert_yolo_to_ls(
input_dir,
out_file,
to_name='image',
from_name='label',
out_type="annotations",
image_root_url=default_image_root_url,
image_ext='.jpg,.jpeg,.png',
image_dims: Optional[Tuple[int, int]] = None,
):
logger.debug("Starting YOLO to Label Studio conversion")
logger.debug(f"Input directory: {input_dir}")
logger.debug(f"Output file: {out_file}")
tasks = []
logger.info('Reading YOLO notes and categories from %s', input_dir)
notes_file = os.path.join(input_dir, 'classes.txt')
with open(notes_file) as f:
lines = [line.strip() for line in f.readlines()]
categories = {i: line for i, line in enumerate(lines)}
logger.info(f'Found {len(categories)} categories')
label_config_file = out_file.replace('.json', '') + '.label_config.xml'
generate_label_config(
categories,
{from_name: 'RectangleLabels'},
to_name,
from_name,
label_config_file,
)
logger.info('Converting labels from %s', input_dir)
image_ext = [x.strip() for x in image_ext.split(",")]
logger.info(f'image extensions->, {image_ext}')
for f in os.listdir(input_dir):
image_file_found_flag = False
for ext in image_ext:
if f.endswith(ext):
image_file = f
image_file_base = f[0 : -len(ext)]
image_file_found_flag = True
break
if not image_file_found_flag:
continue
image_root_url += '' if image_root_url.endswith('/') else '/'
task = {
"data": {
"image": image_root_url + str(pathname2url(image_file))
}
}
label_file = os.path.join(input_dir, image_file_base + '.txt')
if os.path.exists(label_file):
task[out_type] = [
{
"result": [],
"ground_truth": False,
}
]
if image_dims is None:
with Image.open(os.path.join(input_dir, image_file)) as im:
image_width, image_height = im.size
else:
image_width, image_height = image_dims
with open(label_file) as file:
lines = file.readlines()
for line in lines:
values = line.split()
label_id, x, y, width, height = values[0:5]
score = float(values[5]) if len(values) >= 6 else None
x, y, width, height = (
float(x),
float(y),
float(width),
float(height),
)
item = {
"id": uuid.uuid4().hex[0:10],
"type": "rectanglelabels",
"value": {
"x": (x - width / 2) * 100,
"y": (y - height / 2) * 100,
"width": width * 100,
"height": height * 100,
"rotation": 0,
"rectanglelabels": [categories[int(label_id)]],
},
"to_name": to_name,
"from_name": from_name,
"image_rotation": 0,
"original_width": image_width,
"original_height": image_height,
}
if score:
item["score"] = score
task[out_type][0]['result'].append(item)
tasks.append(task)
if len(tasks) > 0:
logger.info('Saving Label Studio JSON to %s', out_file)
with open(out_file, 'w') as out:
json.dump(tasks, out)
help_root_dir = ''
if image_root_url == default_image_root_url:
help_root_dir = (
f"Set environment variables LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true and "
f"LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT={input_dir} for Label Studio run,\n"
f"add Local Storage with Absolute local path = {input_dir}"
)
print(
'\n'
f' 1. Create a new project in Label Studio\n'
f' 2. Use Labeling Config from "{label_config_file}"\n'
f' 3. Setup serving for images\n'
f' E.g. you can use Local Storage (or others):\n'
f' https://labelstud.io/guide/storage.html#Local-storage\n'
f' See tutorial here:\nhttps://github.com/HumanSignal/label-studio-converter/tree/master?tab=readme-ov-file#yolo-to-label-studio-converter\n'
f' {help_root_dir}\n'
f' 4. Import "{out_file}" to the project\n'
)
else:
logger.error('No labels converted')
def add_parser(subparsers):
yolo = subparsers.add_parser('yolo')
yolo.add_argument(
'-i',
'--input',
dest='input',
required=True,
help='directory with YOLO where images, labels, notes.json are located',
action=ExpandFullPath,
)
yolo.add_argument(
'-o',
'--output',
dest='output',
help='output file with Label Studio JSON tasks',
default='output.json',
action=ExpandFullPath,
)
yolo.add_argument(
'--to-name',
dest='to_name',
help='object name from Label Studio labeling config',
default='image',
)
yolo.add_argument(
'--from-name',
dest='from_name',
help='control tag name from Label Studio labeling config',
default='label',
)
yolo.add_argument(
'--out-type',
dest='out_type',
help='annotation type - "annotations" or "predictions"',
default='annotations',
)
yolo.add_argument(
'--image-root-url',
dest='image_root_url',
help='root URL path where images will be hosted, e.g.: http://example.com/images',
default=default_image_root_url,
)
yolo.add_argument(
'--image-ext',
dest='image_ext',
help='image extension to search: .jpeg or .jpg, .png',
default='.jpg',
)
yolo.add_argument(
'--image-dims',
dest='image_dims',
type=int,
nargs=2,
help=(
"optional tuple of integers specifying the image width and height of *all* "
"images in the dataset. Defaults to opening the image to determine it's width "
"and height, which is slower. This should only be used in the special "
"case where you dataset has uniform image dimesions. e.g. `--image-dims 600 800` "
"if all your images are of dimensions width=600, height=800"
),
default=None,
)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
add_parser(subparsers)
args = parser.parse_args()
if args.command == "yolo":
convert_yolo_to_ls(
input_dir=args.input,
out_file=args.output,
to_name=args.to_name,
from_name=args.from_name,
out_type=args.out_type,
image_root_url=args.image_root_url,
image_ext=args.image_ext,
image_dims=tuple(args.image_dims) if args.image_dims else None,
)
YOLO 데이터셋 폴더 구조
명령어
python yolo_to_ls.py yolo -i <데이터셋 경로> -o output.json --image-root-url "/data/local-files/?d=<로컬 스토리지 폴더>"
로컬 스토리지 연동
컨버터를 통해 JSON 파일을 만들었다면 이제 로컬 스토리지에 이미지를 넣고, 라벨 스튜디오 프로젝트에서 Import를 통해 만들어진 JSON 파일을 가져온다.
주석 데이터를 성공적으로 불러온 것을 확인 할 수 있다.
'dev' 카테고리의 다른 글
[Jetson] Jetpack6 Jetson 디바이스에 PyQt6, PySide6 설치하기 (0) | 2024.08.13 |
---|---|
NanoVLM: 엣지 디바이스에서 사용할 수 있는 멀티모탈 모델 (0) | 2024.08.12 |
[Label Studio] 라벨 스튜디오 사용해보기 (0) | 2024.07.02 |
YOLOv10: 새로운 실시간 종단간 객체 감지 모델 (0) | 2024.05.30 |
[Jetson] Jetson Orin Nano SSD에 Jetpack 설치 (SDK Manager 사용) (3) | 2024.04.18 |