Ultralytics YOLO27: Join the waitlist.
Get Started

Scene Text OBB Dataset

The Scene Text OBB dataset combines COCO-Text and HierText into a single-class word-detection dataset for Oriented Bounding Box (OBB) training. It contains natural-scene and document images with rotated, curved, densely packed, handwritten, and printed text.

Key Features#

  • 65,325 images and 1,288,991 legible word annotations after conversion.
  • One text class with normalized polygon labels compatible with Ultralytics YOLO OBB models.
  • COCO-Text contributes varied natural scenes; HierText contributes dense text from natural scenes and documents.
  • Automatic source downloads, annotation conversion, and cross-platform image linking through the dataset YAML.
  • Source polygons with more than four points are preserved and reduced to minimum-area rectangles by the OBB data pipeline.

Dataset Structure#

The converted dataset has the following splits. Counts include only legible words with a contour area of at least one pixel; images without their source files are excluded.

SplitCOCO-Text images / wordsHierText images / wordsCombined images / words
Train43,686 / 65,9198,281 / 862,57751,967 / 928,496
Val4,927 / 7,2111,724 / 178,3826,651 / 185,593
Test5,073 / 7,7141,634 / 167,1886,707 / 174,902
Total53,686 / 80,84411,639 / 1,208,14765,325 / 1,288,991

COCO-Text uses its official training split. Its official validation images are partitioned deterministically into approximately balanced validation and test subsets using the image ID. HierText retains its official train, validation, and test splits, with validation renamed to val.

Images with no retained words remain in the dataset with empty label files and serve as background images. The converted directories use this structure:

datasets/
└── scene-text-yolo/
    ├── coco-text/
    │   ├── train/
    │   │   ├── images/
    │   │   └── labels/
    │   ├── val/
    │   └── test/
    └── hiertext/
        ├── train/
        ├── val/
        └── test/

Each label row contains class 0 followed by normalized polygon coordinates. Images are hard-linked when the filesystem supports it and copied otherwise.

Dataset YAML#

The scene-text-obb.yaml configuration references both converted datasets. On first use, it downloads COCO 2017 images, COCO-Text annotations, HierText annotations, and HierText images before converting them.

scene-text-obb.yaml
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# Scene Text OBB dataset combining COCO-Text and HierText legible word annotations
# COCO-Text: https://bgshih.github.io/cocotext/ (annotations: CC BY 4.0; COCO image terms apply)
# HierText: https://github.com/google-research-datasets/hiertext (CC BY-SA 4.0; Open Images terms apply)
# Documentation: https://docs.ultralytics.com/datasets/obb/scene-text
# Example usage: yolo obb train data=scene-text-obb.yaml model=yolo26n-obb.pt imgsz=640
# parent
# ├── ultralytics
# └── datasets
#     ├── coco
#     ├── coco-text
#     ├── hiertext
#     └── scene-text-yolo

# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: scene-text-yolo # dataset root dir
train:
  - coco-text/train/images # 43686 images, 65919 words
  - hiertext/train/images # 8281 images, 862577 words
val:
  - coco-text/val/images # 4927 images, 7211 words
  - hiertext/val/images # 1724 images, 178382 words
test:
  - coco-text/test/images # 5073 images, 7714 words
  - hiertext/test/images # 1634 images, 167188 words

names:
  0: text

# Download COCO 2017 images and both annotation sources, then convert legible words to YOLO OBB format (~24 GB)
download: |
  import gzip
  import hashlib
  import json
  import os
  import shutil
  from pathlib import Path
  from tempfile import TemporaryDirectory

  import cv2
  import numpy as np

  from ultralytics.data.utils import check_det_dataset
  from ultralytics.utils import TQDM
  from ultralytics.utils.downloads import download

  def polygon_label(vertices, width, height):
      polygon = np.asarray(vertices, dtype=np.float32).reshape(-1, 2)
      if len(polygon) < 3 or cv2.contourArea(polygon) < 1:
          return ""
      polygon[:, 0] = polygon[:, 0].clip(0, width - 1)
      polygon[:, 1] = polygon[:, 1].clip(0, height - 1)
      polygon /= np.array([width, height], dtype=np.float32)
      return "0 " + " ".join(f"{x:.7f} {y:.7f}" for x, y in polygon)

  def link_image(source, destination):
      destination.parent.mkdir(parents=True, exist_ok=True)
      try:
          os.link(source, destination)
      except OSError:
          shutil.copy2(source, destination)

  def write_labels(label_dir, stem, labels):
      label_dir.mkdir(parents=True, exist_ok=True)
      (label_dir / f"{stem}.txt").write_text("\n".join(labels) + ("\n" if labels else ""), encoding="utf-8")

  def convert_coco_text(parent, destination):
      source_dir = parent / "coco-text"
      annotations = source_dir / "cocotext.v2.json"
      if not annotations.is_file():
          download(
              "https://github.com/bgshih/cocotext/releases/download/dl/cocotext.v2.zip",
              dir=source_dir,
              delete=True,
          )
      coco = check_det_dataset("coco.yaml")
      images = Path(coco["path"]) / "images"
      data = json.loads(annotations.read_text(encoding="utf-8"))
      for image_id, image in TQDM(data["imgs"].items(), desc="Converting COCO-Text"):
          split = (
              "train"
              if image["set"] == "train"
              else ("val" if hashlib.sha256(str(image["id"]).encode()).digest()[0] < 128 else "test")
          )
          filename = image["file_name"].replace("COCO_train2014_", "", 1)
          source = images / "train2017" / filename
          if not source.exists():
              source = images / "val2017" / filename
          if not source.exists():
              continue
          image_dir = destination / split / "images"
          link_image(source, image_dir / filename)
          im = cv2.imread(str(source))
          if im is None:
              raise ValueError(f"Unreadable image: {source}")
          height, width = im.shape[:2]
          labels = []
          for ann_id in data["imgToAnns"].get(image_id, []):
              ann = data["anns"][str(ann_id)]
              if ann["legibility"] == "legible" and (label := polygon_label(ann["mask"], width, height)):
                  labels.append(label)
          write_labels(destination / split / "labels", Path(filename).stem, labels)

  def convert_hiertext(parent, destination):
      source = parent / "hiertext" / "source"
      splits = {"train": "train", "validation": "val", "test": "test"}
      base = "https://raw.githubusercontent.com/google-research-datasets/hiertext/main"
      if not all((source / "gt" / f"{split}.jsonl.gz").is_file() for split in splits):
          download([f"{base}/gt/{split}.jsonl.gz" for split in splits], dir=source / "gt", unzip=False, threads=3)
      if not all((source / split).is_dir() for split in splits):
          download(
              [f"https://open-images-dataset.s3.amazonaws.com/ocr/{split}.tgz" for split in splits],
              dir=source,
              delete=True,
              threads=3,
          )
      for source_split, split in splits.items():
          with gzip.open(source / "gt" / f"{source_split}.jsonl.gz", "rt", encoding="utf-8") as f:
              rows = json.load(f)["annotations"]
          for row in TQDM(rows, desc=f"Converting HierText {source_split}"):
              image_id = row["image_id"]
              source_image = source / source_split / f"{image_id}.jpg"
              if not source_image.exists():
                  continue
              image = destination / split / "images" / f"{image_id}.jpg"
              link_image(source_image, image)
              im = cv2.imread(str(image))
              if im is None:
                  raise ValueError(f"Unreadable image: {source_image}")
              height, width = im.shape[:2]
              labels = []
              for paragraph in row["paragraphs"]:
                  for line in paragraph["lines"]:
                      for word in line["words"]:
                          if word["legible"] and (label := polygon_label(word["vertices"], width, height)):
                              labels.append(label)
              write_labels(destination / split / "labels", image_id, labels)

  final = Path(yaml["path"])
  parent = final.parent
  parent.mkdir(parents=True, exist_ok=True)
  with TemporaryDirectory(dir=parent, prefix=".scene-text-") as tmp:
      destination = Path(tmp)
      convert_coco_text(parent, destination / "coco-text")
      convert_hiertext(parent, destination / "hiertext")
      destination.rename(final)
Download size and licensing

The sources require approximately 24 GB of downloads. Allow at least 50 GB of free storage during the initial download and extraction, including temporary COCO archives. If hard links are unavailable, copied images require additional storage. Review the licensing terms before using or redistributing the converted data.

Usage#

Train a YOLO26 OBB model at 640-pixel image size with the following commands. Existing COCO and HierText source files under the Ultralytics datasets_dir are reused.

Train Example
from ultralytics import YOLO

model = YOLO("yolo26n-obb.pt")
results = model.train(data="scene-text-obb.yaml", epochs=100, imgsz=640)

Applications#

Use this dataset to train word-level text detectors for document processing, receipt understanding, sign translation, industrial inspection, and OCR pipelines. Detection is only one stage of OCR: pair the detector with a text recognizer when transcription is required.

Licensing#

The combined dataset uses source-specific terms rather than a single unconditional license:

  • COCO-Text annotations: CC BY 4.0. The underlying COCO image terms remain applicable.
  • HierText annotations: CC BY-SA 4.0. Redistributed adaptations require attribution, a change notice, and licensing under the same or a compatible license.
  • HierText images: Sourced from Open Images. The Open Images terms list images as CC BY 2.0 and instruct users to verify individual image licenses.

These annotation licenses permit commercial use subject to their conditions, but the dataset is not public domain. Preserve source attribution and review the applicable image terms when redistributing data.

Citations and Acknowledgments#

If you use this dataset in your work, cite both source datasets:

Citation
@article{veit2016cocotext,
  title={COCO-Text: Dataset and Benchmark for Text Detection and Recognition in Natural Images},
  author={Veit, Andreas and Matera, Tomas and Neumann, Lukas and Matas, Jiri and Belongie, Serge},
  journal={arXiv preprint arXiv:1601.07140},
  year={2016}
}

@inproceedings{long2022towards,
  title={Towards End-to-End Unified Scene Text Detection and Layout Analysis},
  author={Long, Shangbang and Qin, Siyang and Panteleev, Dmitry and Bissacco, Alessandro and Fujii, Yasuhisa and Raptis, Michalis},
  booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition},
  year={2022}
}

We thank the COCO-Text and HierText teams for creating and maintaining these datasets.

FAQ#

  • No. The conversion produces single-class word-detection polygons for OBB training. Use the original COCO-Text and HierText annotations if your pipeline also requires transcripts.

  • COCO-Text provides train and validation annotations. This configuration retains the official train split and deterministically divides the official validation images into validation and test subsets. HierText keeps its official splits.

  • Yes. Place it under the Ultralytics datasets_dir in the standard coco/ layout, including images/train2017, images/val2017, and the COCO split text files. The download script reuses it instead of downloading COCO again.

Contributors

Comments

Ask a question, share your results, or help someone else.