SQLite DB 기반으로 COCO annotation 서브셋 추출하기

2 min read

소스코드

COCO 형식 대용량 annotation JSON을 다루다 보면 특정 디렉토리의 이미지 라벨이나 특정 클래스 라벨만 추출해야 하는 상황이 발생한다.

Objects365처럼 용량이 큰 데이터셋의 한계:

  • JSON 전체 로딩/파싱 비용 증가
  • 단순 스크립트로는 메모리 사용량 급증
  • 반복적인 필터링 작업의 비효율성

이에 annotation 데이터를 SQLite DB로 변환한 뒤, 필요할 때 DB 쿼리로 서브셋을 추출하는 방식을 적용했다.

본 문서에서는 다음 사항을 한 번에 처리하는 스크립트를 정리한다:

  • 특정 이미지 폴더 기준 이미지 및 annotation 필터링
  • 필요 시 특정 category 추가 필터링
  • 대용량 처리를 위한 chunk 단위 쿼리

사용 사례

주요 사용 목적은 다음과 같다.

  • 특정 폴더에 위치한 이미지 기준으로 COCO JSON 생성
  • patch 단위로 JSON을 분할하여 관리 (patch_0, patch_1, …)
  • 필요 시 category name이나 id 기준으로 클래스 필터링

예시:

  • Objects365 데이터셋에서 특정 이미지 세트만 추출
  • 클래스 일부만 포함한 경량 서브셋 생성
  • 학습용 train/val 분할 JSON 생성

전체 흐름

스크립트의 전체 동작 흐름은 다음과 같다.

  1. SQLite DB에서 category 테이블 로드
  2. category name → id 매핑 (선택)
  3. images 테이블에서 이미지 폴더 기준으로 데이터 필터링
  4. annotations 테이블에서 image_id 기준으로 chunk 단위 조회
  5. (선택) category_id 기준 추가 필터링
  6. COCO 형식 JSON으로 재구성 후 저장

이미지 기준 필터링

우선 대상 이미지 폴더에 존재하는 파일명을 기준으로 DB images 테이블에서 해당 이미지들만 선별한다.

JSON 전체를 순회하는 대신 DB 쿼리와 파일명 매칭을 활용해 속도를 개선했다.

def fetch_filtered_images(cursor, image_path: Path):
    target_images = set(os.listdir(image_path))
    images = []
    image_ids = []

    cursor.execute(
        "SELECT id, file_name, width, height, license, flickr_url, coco_url, date_captured FROM images"
    )

    for row in tqdm(cursor, desc="Filter DB Images"):
        (
            img_id,
            file_path,
            width,
            height,
            license,
            flickr_url,
            coco_url,
            date_captured,
        ) = row

        file_name = Path(file_path).name
        if file_name in target_images:
            image_ids.append(img_id)
            images.append(
                {
                    "id": img_id,
                    "file_name": file_name,
                    "width": width,
                    "height": height,
                    "license": license,
                    "flickr_url": flickr_url,
                    "coco_url": coco_url,
                    "date_captured": date_captured,
                }
            )

    return images, image_ids

대용량 annotations chunk 처리

SQLite는 한 SQL 문장에 바인딩할 수 있는 변수 개수 제한(SQLITE_MAX_VARIABLE_NUMBER)과 문장 길이 제한(SQLITE_MAX_SQL_LENGTH)이 존재한다. IN (...) 절에 ID가 수천 개 이상 포함되면 제한으로 인해 쿼리가 실패할 수 있다. 이 문제를 해결하기 위해 image_id 목록을 chunk 단위로 분할하여 조회하도록 구현했다.

def fetch_filtered_annotations(
    cursor, image_ids, target_category_ids=None, chunk_size=1000
):
    annotations = []

    for i in tqdm(
        range(0, len(image_ids), chunk_size),
        desc="Filter DB annotations",
    ):
        chunk_ids = image_ids[i : i + chunk_size]
        image_placeholders = ",".join("?" * len(chunk_ids))

        params = list(chunk_ids)
        sql = f"""
            SELECT id, image_id, category_id, bbox, area, iscrowd, segmentation
            FROM annotations
            WHERE image_id IN ({image_placeholders})
        """

        if target_category_ids:
            cat_placeholders = ",".join("?" * len(target_category_ids))
            sql += f" AND category_id IN ({cat_placeholders})"
            params.extend(target_category_ids)

        cursor.execute(sql, params)

적용 효과는 다음과 같다:

  • 대용량 이미지 세트에서도 안정적인 처리 가능
  • 메모리 사용량 제어 용이

category 기준 추가 필터링

필요 시 category name이나 category id 기준으로 annotation을 추가로 필터링할 수 있다.

  • target_category_names
  • target_category_ids

중 하나를 인자로 입력하면 내부에서 category_id 기준으로 변환해 쿼리에 반영한다.

def load_category_mapping(cursor):
    mapping = {}
    cursor.execute("SELECT id, name FROM categories")
    for cat_id, name in cursor:
        mapping[name] = cat_id
    return mapping

COCO JSON 생성

최종적으로 다음 데이터를 모아 COCO JSON을 구성한다.

  • images
  • annotations
  • categories

필요한 category만 남긴 뒤 patch 단위로 JSON 파일을 생성한다.

def build_coco_json(images, annotations, categories):
    return {
        "images": images,
        "annotations": annotations,
        "categories": categories,
        "info": {"description": "Filtered COCO dataset from DB"},
        "license": [],
    }
def save_coco_json(coco_data, output_dir: Path, patch_number, split_name):
    output_dir = output_dir / split_name
    output_dir.mkdir(parents=True, exist_ok=True)

    output_json = output_dir / f"patch_{patch_number}.json"
    with open(output_json, "w", encoding="utf-8") as f:
        json.dump(coco_data, f, indent=4, ensure_ascii=False)

    print(f"[OK] Saved: {output_json}")

정리

COCO 형식 대용량 annotation JSON을 통째로 메모리에 로드하는 대신 SQLite DB로 변환해 서브셋을 쿼리하면:

  • 속도
  • 메모리 사용량
  • 반복 작업 자동화

측면에서 처리 효율을 개선할 수 있다.