February 25, 2026 · 6 min read
Fine-Tuning OWL-ViT on a Custom Object Detection Dataset
Fine-tuning OWL-ViT on a custom dataset requires building a complete detection pipeline because OWL-ViT is fundamentally different from traditional detectors like YOLO or Faster-RCNN. Instead of predicting object classes directly, OWL-ViT predicts bounding boxes together with similarity scores between image regions and text descriptions. During training, the model learns how visual patterns correspond to textual class descriptions, allowing it to detect objects by comparing image features with language features. This guide explains the full pipeline for fine-tuning OWL-ViT on a custom COCO-format dataset, including dataset preparation, model training, loss computation, and inference.

Dataset Acquisition
The dataset is downloaded in COCO format using Roboflow. COCO format provides a structured representation of object detection data where each image is associated with bounding boxes and class labels. This structured format allows consistent training across different datasets and ensures compatibility with transformer-based detection models.
!pip install roboflow
from roboflow import Roboflow
rf = Roboflow(api_key="YOUR_KEY")
project = rf.workspace("workspace-name").project("project-name")
version = project.version(6)
dataset = version.download("coco")This step retrieves the dataset and organizes it into training and validation directories containing images and annotation JSON files. Each annotation contains bounding boxes in pixel coordinates and category identifiers. The dataset structure allows the training pipeline to map images to annotations and class names.
Bounding Box Utility Functions
OWL-ViT predicts bounding boxes in normalized center-coordinate format (cx, cy, w, h), while COCO annotations use pixel-based (x, y, width, height) format. The following functions convert between formats and compute geometric relationships between bounding boxes. These conversions are necessary because training loss calculations require comparing predicted boxes with ground-truth boxes in the same coordinate system.
def box_cxcywh_to_xyxy(boxes: torch.Tensor):
cx, cy, w, h = boxes.unbind(-1)
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
return torch.stack([x1, y1, x2, y2], dim=-1)
This function converts center-based normalized coordinates into corner-based coordinates, which are required for calculating distances and overlaps between predicted and real bounding boxes. Matching bounding boxes in normalized space ensures that the model learns consistent geometric relationships regardless of image resolution.
Generalized IoU Calculation
Intersection over Union measures how well predicted boxes overlap with real boxes. OWL-ViT training uses Generalized IoU, which improves training stability when boxes do not overlap.
def generalized_box_iou(boxes1, boxes2):
lt = torch.max(boxes1[:, None, :2], boxes2[None, :, :2])
rb = torch.min(boxes1[:, None, 2:], boxes2[None, :, 2:])
wh = (rb - lt).clamp(min=0)
inter = wh[..., 0] * wh[..., 1]
area1 = box_area_xyxy(boxes1)[:, None]
area2 = box_area_xyxy(boxes2)[None, :]
union = area1 + area2 - inter + 1e-6
iou = inter / union
This function measures spatial similarity between predicted and ground-truth boxes. Unlike basic IoU, Generalized IoU penalizes predictions that are far away from ground truth. This improves convergence during transformer-based detection training where predictions are initially random.
COCO Dataset Loader
The dataset loader reads images and annotations and converts them into training samples that OWL-ViT can process.
class CocoDetDataset(Dataset):
def __init__(self, images_dir, ann_path, category_id_to_idx):
self.images_dir = images_dir
self.ann = json.load(open(ann_path))
self.category_id_to_idx = category_id_to_idx
self.images = {img["id"]: img for img in self.ann["images"]}self.image_ids = list(self.images.keys())
This class organizes annotations by image and allows the training loop to retrieve samples efficiently. Each training sample includes the image, bounding boxes, and labels.
def __getitem__(self, i):
image_id = self.image_ids[i]
info = self.images[image_id]
path = os.path.join(self.images_dir, info["file_name"])
image = Image.open(path).convert("RGB")This portion loads the image and retrieves annotation metadata.
x1 = x / W
y1 = y / H
x2 = (x + w) / W
y2 = (y + h) / H
Bounding boxes are normalized into the range [0,1]. OWL-ViT predicts normalized boxes, so training requires ground truth boxes in the same format.
OWL-ViT Collator
OWL-ViT requires both images and text descriptions. The collator prepares batches containing both.
class OwlViTCollator:
def __call__(self, batch):
images, targets = zip(*batch)
texts = [self.class_texts for _ in images]
enc = self.processor(
text=texts,
images=list(images),
return_tensors="pt",
padding=True
)
This class converts raw images and class names into tensors suitable for the transformer model. Each image receives the same list of class prompts, allowing OWL-ViT to compare each region against all classes.
Hungarian Matching and Detection Loss
OWL-ViT produces hundreds of candidate detections per image. Hungarian matching determines which predictions correspond to real objects.
class OwlViTDetrLoss(nn.Module):
def forward(self, logits, pred_boxes, targets):
prob = logits.softmax(dim=-1)
pred_boxes_xyxy = box_cxcywh_to_xyxy(pred_boxes)
The model outputs classification logits and predicted boxes. These are converted into probabilities and comparable box formats.
cost_class = -prob[:, tgt_labels]
cost_l1 = torch.cdist(boxes_xyxy, tgt_boxes_xyxy)
cost_giou = -giou
Matching cost is computed using classification similarity, box distance, and overlap quality. Hungarian matching selects the best prediction for each ground truth object.
q_ind, t_ind = linear_sum_assignment(cost)
This ensures each real object is matched to exactly one prediction. Transformer detectors require this matching because predictions are unordered.
loss = classification + L1 + GIoU
Classification loss teaches the model which object category each region belongs to. L1 loss teaches accurate box coordinates. GIoU loss teaches spatial overlap quality.
Model Initialization
OWL-ViT is loaded with pretrained weights.
model = OwlViTForObjectDetection.from_pretrained(
"google/owlvit-base-patch32"
)
Pretrained weights allow the model to start with general visual understanding before adapting to the custom dataset.
Freezing Text Encoder
for n,p in model.named_parameters():
if "text_model" in n:
p.requires_grad=False
The text encoder is frozen to preserve language representations. Only the vision and detection components are updated. This stabilizes training and prevents semantic drift.
Training Loop
The training loop feeds batches into the model and updates weights.
outputs = model(**batch)
logits = outputs.logits
pred_boxes = outputs.pred_boxes
The model produces candidate detections for each image.
losses = criterion(logits,pred_boxes,targets)
accelerator.backward(loss)
Loss gradients are computed and propagated backward through the network.
optimizer.step()
optimizer.zero_grad()
Weights are updated after each batch.
Validation Loop
Validation evaluates performance on unseen data.
outputs=model(**batch)
losses=criterion(
outputs.logits,
outputs.pred_boxes,
targets)
Validation loss indicates generalization ability.
Inference Pipeline
During inference, each image or video frame is processed independently.
inputs=processor(
text=[CLASS_NAMES],
images=image,
return_tensors="pt")
The processor converts images and text into tensors.
outputs=model(**inputs)
The model predicts candidate detections.
probs=torch.softmax(logits,dim=-1)
labels=probs.argmax(dim=-1)
scores=probs.max(dim=-1).values
The highest probability class is selected for each candidate region.
Bounding Box Conversion
x1=(cx-bw/2)*W
y1=(cy-bh/2)*H
x2=(cx+bw/2)*W
y2=(cy+bh/2)*H
Normalized coordinates are converted into pixel coordinates.
Confidence Filtering
if score < SCORE_THRESHOLD:
continue
Low confidence detections are removed.
Non Maximum Suppression
keep=nms(boxes_xyxy,scores,0.45)
NMS removes duplicate detections and keeps only the best bounding box for each object.
Video Processing
Video frames are processed sequentially.
cap=cv2.VideoCapture(INPUT_VIDEO)
Frames are read and passed through the detection pipeline.
detections=detect_frame(frame)
frame=draw(frame,detections)
out.write(frame)
The final annotated frames are saved into an output video.
Complete Training Flow
The complete pipeline consists of:
COCO Dataset
→ Dataset Loader
→ Processor
→ OWL-ViT Model
→ Hungarian Matching
→ Detection Loss
→ Backpropagation
→ Checkpoints
→ Inference
→ Filtering
→ Annotated Output