bounding-box-demo / index.html
mrdbourke's picture
add dots on boxes
55cae17 verified
<!DOCTYPE html>
<html>
<head>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui;
background-color: #f5f5f5;
min-height: 100vh;
padding: 1rem;
}
.page-title {
text-align: center;
padding: 0.5rem 0;
color: #333;
margin-bottom: 1.5rem;
font-size: 1.8rem;
}
.main-container {
max-width: 1200px;
margin: 0 auto;
}
/* Top Section: Image and Controls Side by Side */
.top-section {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
/* Image Section */
.image-section {
flex: 0 0 640px;
display: flex;
flex-direction: column;
align-items: center;
background: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-container {
position: relative;
width: 100%;
}
#image {
display: block;
width: 100%;
height: auto;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
#canvas {
position: absolute;
top: 0;
left: 0;
cursor: crosshair;
}
.labels-container {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
width: 100%;
height: 100%;
}
/* Controls Section */
.controls-section {
flex: 1;
background: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.coordinates {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.format-controls {
margin-bottom: 20px;
}
.format-controls h2 {
margin-bottom: 10px;
color: #333;
font-size: 1.2rem;
}
.radio-group {
margin-bottom: 15px;
padding: 10px;
background: #f9f9f9;
border-radius: 4px;
border: 1px solid #ddd;
}
.radio-option {
margin-bottom: 8px;
}
.radio-option label {
margin-left: 5px;
color: #555;
}
.format-controls h2:last-of-type {
margin-top: 20px;
}
.format {
margin-bottom: 15px;
}
.format-title {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.coords {
font-family: monospace;
background: #f9f9f9;
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
min-height: 20px;
}
.clear-button-container {
display: flex;
justify-content: center;
padding-top: 10px;
}
.clear-button {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.clear-button:hover {
background-color: #d32f2f;
}
.label {
position: absolute;
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 3px 5px;
border-radius: 3px;
font-size: 11px;
pointer-events: none;
white-space: nowrap;
}
/* Bottom Section: Model Explanation Table */
.bottom-section {
background: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.model-explanation table {
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
font-size: 0.9rem;
}
.model-explanation th,
.model-explanation td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.model-explanation th {
background-color: #f2f2f2;
position: sticky;
top: 0;
z-index: 1;
}
/* Responsive Design */
@media (max-width: 1200px) {
.top-section {
flex-direction: column;
}
.image-section,
.controls-section,
.bottom-section {
width: 100%;
max-width: 100%;
margin: 0 auto;
}
.image-section,
.controls-section {
flex: none;
}
}
@media (max-height: 900px) {
body {
overflow: auto;
}
}
</style>
</head>
<body>
<h1 class="page-title">Bounding Box Coordinate Tool</h1>
<div class="main-container">
<!-- Top Section: Image and Controls -->
<div class="top-section">
<!-- Image Section -->
<div class="image-section">
<div class="image-container">
<img id="image" src="demo-image.jpg"
alt="Image created by Flux Pro with prompt 'an image of a living room with plenty of different furniture objects'"
width="640">
<canvas id="canvas"></canvas>
<div id="labels-container" class="labels-container"></div>
</div>
<p>Try drawing a box on the image by left clicking and dragging.</p>
<p id="image-dimensions"></p>
</div>
<!-- Controls Section -->
<div class="controls-section">
<div class="coordinates">
<div class="format-controls">
<h2>Display Format</h2>
<div class="radio-group">
<div class="radio-option">
<input type="radio" id="xyxy-radio" name="format" value="xyxy" checked>
<label for="xyxy-radio">XYXY (x1, y1, x2, y2)</label>
</div>
<div class="radio-option">
<input type="radio" id="xywh-radio" name="format" value="xywh">
<label for="xywh-radio">XYWH (x, y, width, height)</label>
</div>
<div class="radio-option">
<input type="radio" id="normalized-radio" name="format" value="normalized">
<label for="normalized-radio">Normalized XYWH (0-1)</label>
</div>
<div class="radio-option">
<input type="radio" id="center-radio" name="format" value="center">
<label for="center-radio">Center XYWH (cx, cy, w, h)</label>
</div>
</div>
<h2>Bounding Box Coordinates</h2>
<div class="format">
<div class="format-title">XYXY (x1, y1, x2, y2) → top left: (x1, y1), bottom right: (x2, y2)
</div>
<div id="xyxy" class="coords">No box drawn</div>
</div>
<div class="format">
<div class="format-title">XYWH (x, y, width, height) → top left: (x, y), width and height of
box</div>
<div id="xywh" class="coords">No box drawn</div>
</div>
<div class="format">
<div class="format-title">Normalized XYWH (pixel values in range of 0-1)</div>
<div id="normalized" class="coords">No box drawn</div>
</div>
<div class="format">
<div class="format-title">Center XYWH (cx, cy, w, h) → (center x, center y, width, height)
</div>
<div id="center" class="coords">No box drawn</div>
</div>
</div>
<div class="clear-button-container">
<button id="clear-button" class="clear-button">Clear Box</button>
</div>
</div>
</div>
</div>
<!-- Bottom Section: Model Explanation Table -->
<div class="bottom-section">
<div class="model-explanation">
<table>
<thead>
<tr>
<th>Model</th>
<th>Summary</th>
<th>Bounding Box Format</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>YOLO</strong></td>
<td>Real-time object detection system. Training format uses normalized coordinates for
better generalization across different image sizes.</td>
<td><strong>Normalized XYWH</strong> (x, y, w, h) in range [0,1]</td>
</tr>
<tr>
<td><strong>Faster R-CNN</strong></td>
<td>Region proposal network based detection. Typically uses absolute coordinates during
inference, but may use normalized coordinates during training.</td>
<td><strong>XYXY</strong> (x1, y1, x2, y2) or normalized coordinates</td>
</tr>
<tr>
<td><strong>SSD</strong></td>
<td>Single Shot MultiBox Detector uses normalized coordinates for anchor boxes and
predictions to handle multiple scales efficiently.</td>
<td><strong>Normalized XYWH</strong> (x, y, w, h) in range [0,1]</td>
</tr>
<tr>
<td><strong>RetinaNet</strong></td>
<td>Dense object detector with focal loss. Uses normalized coordinates for anchor boxes and
predictions.</td>
<td><strong>Normalized XYWH</strong> (x, y, w, h) in range [0,1]</td>
</tr>
<tr>
<td><strong>CornerNet</strong></td>
<td>Anchor-free detector that predicts keypoint heatmaps. Uses normalized coordinates for
better scale invariance.</td>
<td><strong>Normalized Corners</strong> (x1, y1, x2, y2) in range [0,1]</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const image = document.getElementById('image');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const labelsContainer = document.getElementById('labels-container');
let isDrawing = false;
let startX = 0;
let startY = 0;
let lastCoords = null;
let labelElements = [];
function initCanvas() {
canvas.width = image.width;
canvas.height = image.height;
canvas.style.width = image.width + 'px';
canvas.style.height = image.height + 'px';
labelsContainer.style.width = image.width + 'px';
labelsContainer.style.height = image.height + 'px';
document.getElementById('image-dimensions').textContent = 'Image dimensions: ' + image.width + ' x ' + image.height;
}
if (image.complete) {
initCanvas();
} else {
image.onload = initCanvas;
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
clearLabels();
}
function clearLabels() {
labelElements.forEach(label => labelsContainer.removeChild(label));
labelElements = [];
}
function createLabel(text, x, y, positionClass) {
const label = document.createElement('div');
label.classList.add('label', positionClass);
label.textContent = text;
label.style.left = `${x}px`;
label.style.top = `${y}px`;
const containerRect = labelsContainer.getBoundingClientRect();
label.style.visibility = 'hidden';
labelsContainer.appendChild(label);
const tempRect = label.getBoundingClientRect();
label.style.visibility = 'visible';
if (x + tempRect.width > containerRect.width) {
label.style.left = `${containerRect.width - tempRect.width - 5}px`;
}
if (y + tempRect.height > containerRect.height) {
label.style.top = `${containerRect.height - tempRect.height - 5}px`;
}
labelsContainer.appendChild(label);
labelElements.push(label);
}
function drawDot(x, y) {
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = 'black';
ctx.fill();
}
// Draw dots at the positions corresponding to the selected format.
function drawDots(x1, y1, x2, y2) {
const format = document.querySelector('input[name="format"]:checked').value;
if (format === 'center') {
const centerX = x1 + (x2 - x1) / 2;
const centerY = y1 + (y2 - y1) / 2;
drawDot(centerX, centerY);
drawDot(x1, y1);
} else {
drawDot(x1, y1);
drawDot(x2, y2);
}
}
function createLabels(x1, y1, x2, y2) {
const width = x2 - x1;
const height = y2 - y1;
const format = document.querySelector('input[name="format"]:checked').value;
let labels = [];
switch (format) {
case 'xyxy':
labels = [
{ text: `(${Math.round(x1)}, ${Math.round(y1)})`, x: x1, y: y1, position: 'top-left' },
{ text: `(${Math.round(x2)}, ${Math.round(y2)})`, x: x2, y: y2, position: 'bottom-right' }
];
break;
case 'xywh':
labels = [
{ text: `x: ${Math.round(x1)}, y: ${Math.round(y1)}`, x: x1, y: y1, position: 'top-left' },
{ text: `w: ${Math.round(width)}, h: ${Math.round(height)}`, x: x1, y: y1 - 15, position: 'top-left' }
];
break;
case 'normalized':
const normalizedX = (x1 / canvas.width).toFixed(3);
const normalizedY = (y1 / canvas.height).toFixed(3);
const normalizedW = (width / canvas.width).toFixed(3);
const normalizedH = (height / canvas.height).toFixed(3);
labels = [
{ text: `x: ${normalizedX}, y: ${normalizedY}`, x: x1, y: y1, position: 'top-left' },
{ text: `w: ${normalizedW}, h: ${normalizedH}`, x: x1, y: y1 - 15, position: 'top-left' }
];
break;
case 'center':
const centerX = Math.round(x1 + width / 2);
const centerY = Math.round(y1 + height / 2);
labels = [
{ text: `cx: ${centerX}, cy: ${centerY}`, x: centerX, y: centerY, position: 'top-left' },
{ text: `w: ${Math.round(width)}, h: ${Math.round(height)}`, x: x1, y: y1 - 15, position: 'top-left' }
];
break;
}
labels.forEach(label => {
createLabel(label.text, label.x, label.y, label.position);
});
}
function drawBox(x1, y1, x2, y2) {
clearCanvas();
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
const width = x2 - x1;
const height = y2 - y1;
ctx.strokeRect(x1, y1, width, height);
drawDots(x1, y1, x2, y2);
createLabels(x1, y1, x2, y2);
lastCoords = { x1, y1, x2, y2 };
}
function updateCoordinates(x1, y1, x2, y2) {
if (x1 === null || y1 === null || x2 === null || y2 === null) {
document.getElementById('xyxy').textContent = 'No box drawn';
document.getElementById('xywh').textContent = 'No box drawn';
document.getElementById('normalized').textContent = 'No box drawn';
document.getElementById('center').textContent = 'No box drawn';
return;
}
const width = x2 - x1;
const height = y2 - y1;
document.getElementById('xyxy').textContent =
`[${Math.round(x1)}, ${Math.round(y1)}, ${Math.round(x2)}, ${Math.round(y2)}]`;
document.getElementById('xywh').textContent =
`[${Math.round(x1)}, ${Math.round(y1)}, ${Math.round(width)}, ${Math.round(height)}]`;
const normalizedX = (x1 / canvas.width).toFixed(3);
const normalizedY = (y1 / canvas.height).toFixed(3);
const normalizedW = (width / canvas.width).toFixed(3);
const normalizedH = (height / canvas.height).toFixed(3);
document.getElementById('normalized').textContent =
`[${normalizedX}, ${normalizedY}, ${normalizedW}, ${normalizedH}]`;
const centerX = x1 + width / 2;
const centerY = y1 + height / 2;
document.getElementById('center').textContent =
`[${Math.round(centerX)}, ${Math.round(centerY)}, ${Math.round(width)}, ${Math.round(height)}]`;
}
document.querySelectorAll('input[name="format"]').forEach(radio => {
radio.addEventListener('change', () => {
if (lastCoords) {
drawBox(lastCoords.x1, lastCoords.y1, lastCoords.x2, lastCoords.y2);
updateCoordinates(lastCoords.x1, lastCoords.y1, lastCoords.x2, lastCoords.y2);
}
});
});
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
clearCanvas();
updateCoordinates(null, null, null, null);
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
drawBox(startX, startY, currentX, currentY);
updateCoordinates(startX, startY, currentX, currentY);
});
canvas.addEventListener('mouseup', (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
drawBox(startX, startY, endX, endY);
updateCoordinates(startX, startY, endX, endY);
isDrawing = false;
});
canvas.addEventListener('mouseleave', () => {
isDrawing = false;
});
document.getElementById('clear-button').addEventListener('click', () => {
clearCanvas();
lastCoords = null;
updateCoordinates(null, null, null, null);
});
</script>
</body>
</html>