Skip to the content.

Working with Box Coordinates

This guide explains how Quagga2’s coordinate system works and how to properly use box, boxes, and line coordinates for overlay rendering, especially when using inputStream.size to scale processing.

Understanding the Coordinate System

Quagga2 returns box, boxes, and line coordinates in processed canvas coordinates, not original image/video coordinates. This is important to understand when:

Key Concepts

Term Description
Real Size The actual dimensions of the source image/video
Processed Size The scaled dimensions used for barcode detection (controlled by inputStream.size)
Canvas Size The dimensions of the processing canvas (typically matches processed size)

How Coordinates are Generated

  1. Image Scaling: When inputStream.size is set, the image is scaled so the longest side equals that value
  2. Localization: Barcode regions are found in the scaled image
  3. Box Coordinates: Returned coordinates are relative to the scaled/processed image
  4. halfSample Adjustment: If halfSample: true, coordinates are automatically scaled 2x

Converting Coordinates to Original Image Space

When you need to draw boxes on the original video/image (not the processed canvas), you must scale the coordinates.

For Live Video Streams

Quagga.onDetected(function(result) {
  if (!result.box) return;
  
  // Get the video element
  const video = document.querySelector('video');
  const videoWidth = video.videoWidth;   // Real video dimensions
  const videoHeight = video.videoHeight;
  
  // Get processed dimensions from Quagga
  const canvas = Quagga.canvas.dom.image;
  const processedWidth = canvas.width;
  const processedHeight = canvas.height;
  
  // Calculate scale factors
  const scaleX = videoWidth / processedWidth;
  const scaleY = videoHeight / processedHeight;
  
  // Convert box coordinates to video space
  const scaledBox = result.box.map(function(point) {
    return [
      point[0] * scaleX,
      point[1] * scaleY
    ];
  });
  
  // Now use scaledBox for drawing on video overlay
  drawBoxOnVideo(scaledBox);
});

For Static Images with decodeSingle

Quagga.decodeSingle({
  src: './barcode.jpg',
  inputStream: {
    size: 800  // Process at 800px max dimension
  },
  // ... other config
}, function(result) {
  if (!result || !result.box) return;
  
  // Load original image to get real dimensions
  const img = new Image();
  img.onload = function() {
    const realWidth = img.naturalWidth;
    const realHeight = img.naturalHeight;
    
    // Calculate what the processed size was
    const aspectRatio = realWidth / realHeight;
    let processedWidth, processedHeight;
    
    if (aspectRatio > 1) {
      // Landscape: width is the longest side
      processedWidth = 800;
      processedHeight = Math.floor(800 / aspectRatio);
    } else {
      // Portrait: height is the longest side
      processedHeight = 800;
      processedWidth = Math.floor(800 * aspectRatio);
    }
    
    // Calculate scale factors
    const scaleX = realWidth / processedWidth;
    const scaleY = realHeight / processedHeight;
    
    // Convert coordinates
    const scaledBox = result.box.map(function(point) {
      return [
        point[0] * scaleX,
        point[1] * scaleY
      ];
    });
    
    // Use scaledBox for original image operations
    cropBarcodeFromOriginal(scaledBox);
  };
  img.src = './barcode.jpg';
});

Complete Example: Drawing Boxes on Live Video

Here’s a complete example showing how to draw accurate bounding boxes on a live video stream:

// Initialize Quagga with reduced processing size for performance
Quagga.init({
  inputStream: {
    name: "Live",
    type: "LiveStream",
    target: document.querySelector('#scanner-container'),
    constraints: {
      width: 1280,
      height: 720,
      facingMode: "environment"
    },
    size: 640  // Process at 640px for better performance
  },
  locator: {
    patchSize: "medium",
    halfSample: true
  },
  decoder: {
    readers: ["code_128_reader", "ean_reader"]
  }
}, function(err) {
  if (err) {
    console.error(err);
    return;
  }
  Quagga.start();
});

// Handle detections with coordinate scaling
Quagga.onDetected(function(result) {
  const video = document.querySelector('video');
  const overlay = document.querySelector('#overlay-canvas');
  const ctx = overlay.getContext('2d');
  
  // Match overlay to video size
  overlay.width = video.videoWidth;
  overlay.height = video.videoHeight;
  
  // Get processed canvas size
  const processedCanvas = Quagga.canvas.dom.image;
  
  // Calculate scale factors
  const scaleX = video.videoWidth / processedCanvas.width;
  const scaleY = video.videoHeight / processedCanvas.height;
  
  // Clear previous drawings
  ctx.clearRect(0, 0, overlay.width, overlay.height);
  
  // Draw all detected boxes
  if (result.boxes) {
    result.boxes.forEach(function(box) {
      drawScaledBox(ctx, box, scaleX, scaleY, '#00ff00');
    });
  }
  
  // Highlight the successfully decoded box
  if (result.box) {
    drawScaledBox(ctx, result.box, scaleX, scaleY, '#ff0000');
  }
});

function drawScaledBox(ctx, box, scaleX, scaleY, color) {
  ctx.strokeStyle = color;
  ctx.lineWidth = 2;
  ctx.beginPath();
  
  // Scale and draw each point
  const scaledPoints = box.map(p => [p[0] * scaleX, p[1] * scaleY]);
  
  ctx.moveTo(scaledPoints[0][0], scaledPoints[0][1]);
  for (let i = 1; i < scaledPoints.length; i++) {
    ctx.lineTo(scaledPoints[i][0], scaledPoints[i][1]);
  }
  ctx.closePath();
  ctx.stroke();
}

When Coordinates Don’t Need Scaling

If you’re drawing on Quagga’s own overlay canvas (Quagga.canvas.dom.overlay), coordinates are already in the correct space:

Quagga.onDetected(function(result) {
  const ctx = Quagga.canvas.ctx.overlay;
  const canvas = Quagga.canvas.dom.overlay;
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // No scaling needed - coordinates match the overlay canvas
  if (result.box) {
    Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, ctx, {
      color: "#00ff00",
      lineWidth: 2
    });
  }
});

Common Pitfalls

1. Forgetting halfSample Adjustment

If you’re manually calculating processed size, remember that halfSample: true doesn’t affect the returned coordinates (they’re already adjusted).

2. Using Wrong Canvas Reference

// ❌ Wrong - using overlay canvas for scale calculation
const wrongWidth = Quagga.canvas.dom.overlay.width;

// ✅ Correct - using image canvas for scale calculation
const correctWidth = Quagga.canvas.dom.image.width;

3. Assuming Square Pixels

Always calculate scaleX and scaleY separately, as aspect ratios may differ:

// ❌ Wrong - using single scale factor
const scale = videoWidth / canvasWidth;

// ✅ Correct - separate scale factors
const scaleX = videoWidth / canvasWidth;
const scaleY = videoHeight / canvasHeight;

Performance Tips

  1. Use smaller inputStream.size (e.g., 640-800) for live video to reduce CPU usage
  2. Cache scale factors - recalculate only when video dimensions change
  3. Use requestAnimationFrame for smooth overlay rendering
  4. Consider using Quagga’s built-in overlay when possible to avoid manual scaling