computer-vision-demo / index.html
hvanu's picture
Upload index.html
bb756c3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ML Vision Playground</title>
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<link rel="stylesheet" href="assets/style.css" />
</head>
<body x-data="app()" :class="{'dark': darkMode}">
<div class="container">
<header>
<h1>ML Vision Playground</h1>
</header>
<div class="applications-container mb-2">
<!-- Object Detection Application -->
<!-- Object detection -->
<div
class="application-tab"
:class="{'active': activeApplication === 'objdet'}"
>
<div class="application-header" @click="toggleApplication('objdet')">
<div class="application-title">
<img src="assets/obj-detect.svg" height="20px" />
Object detection
</div>
<img
src="assets/arrow.svg"
height="20px"
class="application-arrow"
/>
</div>
<div class="application-content">
<!-- api creds -->
<div class="application-meta-input card mb-2">
<div class="form-container">
<div class="input-group">
<label for="url"
>URL
<span class="helper-text"
>Enter the base URL for the API.</span
></label
>
<input
type="text"
id="url"
placeholder="https://example.com/api"
value="https://api.app.deeploy.ml/workspaces/5aff1650-8d8c-4d0c-b4da-87146360ba82/deployments/8df40f37-11cc-473f-aff5-2112de56f84e/predict"
/>
</div>
<div class="input-group">
<label for="apiKey"
>API Key
<span class="helper-text"
>Your secret API key.
</span></label
>
<input
type="text"
id="apiKey"
placeholder="DPT23exmplg4FJfgfFREsada0lMUgJs0kcC9wxd"
/>
</div>
</div>
</div>
<div class="grid grid-cols-2">
<!-- Input Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Input Image</h2>
<p class="card-description">
Upload or capture an image to analyze
</p>
</div>
<div class="tabs">
<div
@click="activeTab = 'upload'"
:class="{'active': activeTab === 'upload'}"
class="tab"
>
Upload
</div>
<div
@click="activeTab = 'camera'"
:class="{'active': activeTab === 'camera'}"
class="tab"
>
Camera
</div>
</div>
<div
x-show="imageUrl"
class="preview-image-container"
style="max-width: 100%; height: auto"
>
<canvas
id="previewCanvas"
x-ref="previewCanvas"
class="preview-image"
style="max-width: 100%; height: auto"
x-effect="imageUrl && (() => {
const canvas = $refs.previewCanvas;
const img = new Image();
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
};
img.src = imageUrl;
})()"
></canvas>
</div>
<!-- <canvas id="canvas" x-ref="canvas"></canvas> -->
<div
x-show="activeTab === 'upload' && !imageUrl"
class="upload-tab"
>
<div
@click="$refs.fileInput.click()"
@dragover.prevent="dragOver = true"
@dragleave="dragOver = false"
@drop.prevent="handleDrop($event)"
:class="{'has-image': imageUrl, 'border-primary': dragOver}"
class="preview-area"
>
<input
type="file"
@change="handleFileSelect"
x-ref="fileInput"
accept="image/*"
class="hidden"
/>
<template x-if="!imageUrl">
<div class="preview-placeholder">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p>Click to upload or drag and drop</p>
<p class="text-sm">Supports JPG, PNG, WEBP</p>
</div>
</template>
</div>
<!-- <div class="btn-group">
<button @click="clearImage" x-show="imageUrl" class="btn btn-secondary">
Clear
</button>
</div> -->
</div>
<div
x-show="activeTab === 'camera' && !imageUrl"
class="camera-tab"
>
<div class="preview-area">
<video
x-ref="video"
autoplay
playsinline
class="hidden"
></video>
<canvas x-ref="canvas" class="hidden"></canvas>
<div class="preview-placeholder">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<p>Camera Feed</p>
</div>
</div>
<div class="btn-group">
<button @click="startCamera" class="btn btn-primary">
Start Camera
</button>
<button
@click="captureImage"
class="btn btn-primary"
:disabled="!cameraActive"
>
Capture
</button>
<button
@click="stopCamera"
class="btn btn-secondary"
:disabled="!cameraActive"
>
Stop
</button>
</div>
</div>
<div class="btn-group">
<button
@click="clearImage"
x-show="imageUrl"
class="btn btn-secondary"
>
Clear
</button>
</div>
<!-- Collapsible section to select sample images from thumbnails -->
<div class="sample-images-container mt-2">
<div
class="collapsible-header"
@click="sampleImagesOpen = !sampleImagesOpen"
>
<span>Sample Images</span>
<img
src="assets/arrow.svg"
height="20px"
:class="{'rotate-180': sampleImagesOpen}"
/>
</div>
<div
x-show="sampleImagesOpen"
x-transition
class="sample-images-content"
>
<div class="sample-image-grid">
<template x-for="(image, type) in sampleData" :key="type">
<span @click="loadSample(type)" class="sample-image">
<img
:src="image.url"
alt=""
width="80px"
style="max-height: 100px"
/>
</span>
</template>
</div>
</div>
</div>
<!-- Confidence Interval Slider -->
<div class="confidence-slider mt-2">
<label
for="confidence-slider"
class="block text-sm font-medium text-gray-700"
>Confidence Interval</label
>
<input
id="confidence-slider"
type="range"
min="0"
max="100"
step="1"
x-model="confidenceThreshold"
/>
<p class="text-sm mt-1">
Selected: <span x-text="confidenceThreshold"></span>%
</p>
</div>
<!-- Predict Button -->
<div class="mt-2">
<button
@click="predictImage()"
class="btn btn-primary w-100"
:disabled="!imageUrl"
>
Predict
</button>
</div>
</div>
<!-- Processing Card -->
<!-- Processing Card - results -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Output</h2>
<p class="card-description">
Select an application to analyze your image
</p>
</div>
<div class="application-content">
<!-- Preview content same as before -->
<span id="objdet-status-display"></span>
<textarea
id="objdet-output-display"
class="text-output-area"
></textarea>
<div class="btn-group mt-2">
<button
@click="clearResults"
class="btn btn-secondary"
:disabled="!results.length"
>
Clear Results
</button>
</div>
</div>
</div>
</div>
</div>
<!-- end -->
</div>
<!-- OCR -->
<div
class="application-tab"
:class="{'active': activeApplication === 'ocr'}"
>
<div class="application-header" @click="toggleApplication('ocr')">
<div class="application-title">
<img src="assets/text.svg" height="20px" />
Text Recognition (OCR)
</div>
<img
src="assets/arrow.svg"
height="20px"
class="application-arrow"
/>
</div>
<div class="application-content">
<!-- api creds -->
<div class="application-meta-input card mb-2">
<div class="form-container">
<div class="input-group">
<!-- Add the loader component here -->
<label for="url-ocr"
>URL
<span class="helper-text"
>Enter the base URL for the API.</span
></label
>
<input
type="text"
id="url-ocr"
placeholder="https://example.com/api"
value="https://api.app.deeploy.ml/workspaces/5aff1650-8d8c-4d0c-b4da-87146360ba82/deployments/796f79bd-bab8-495e-bc90-6654903312fe/predict"
/>
</div>
<div class="input-group">
<label for="apiKey-ocr"
>API Key
<span class="helper-text"
>Your secret API key.
</span></label
>
<input
type="text"
id="apiKey-ocr"
placeholder="DPT23exmplg4FJfgfFREsada0lMUgJs0kcC9wxd"
/>
</div>
</div>
</div>
<!-- app -->
<div class="grid grid-cols-2">
<!-- Input Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Input Image</h2>
<p class="card-description">
Upload or capture an image to analyze
</p>
</div>
<div class="tabs">
<div
@click="activeTab = 'upload'"
:class="{'active': activeTab === 'upload'}"
class="tab"
>
Upload
</div>
<div
@click="activeTab = 'camera'"
:class="{'active': activeTab === 'camera'}"
class="tab"
>
Camera
</div>
</div>
<div
x-show="imageUrlOcr"
class="preview-image-container"
style="max-width: 100%; height: auto"
>
<canvas
id="previewCanvasOcr"
x-ref="previewCanvasOcr"
class="preview-image"
style="max-width: 100%; height: auto"
x-effect="imageUrlOcr && (() => {
const canvas = $refs.previewCanvasOcr;
const img = new Image();
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
};
img.src = imageUrlOcr;
})()"
></canvas>
</div>
<!-- <canvas id="canvas" x-ref="canvas"></canvas> -->
<div
x-show="activeTab === 'upload' && !imageUrlOcr"
class="upload-tab"
>
<div
@click="$refs.fileInputOcr.click()"
@dragover.prevent="dragOver = true"
@dragleave="dragOver = false"
@drop.prevent="handleDropOcr($event)"
:class="{'has-image': imageUrlOcr, 'border-primary': dragOver}"
class="preview-area"
>
<input
type="file"
@change="handleFileSelectOcr"
x-ref="fileInputOcr"
accept="image/*"
class="hidden"
/>
<template x-if="!imageUrlOcr">
<div class="preview-placeholder">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p>Click to upload or drag and drop</p>
<p class="text-sm">Supports JPG, PNG, WEBP</p>
</div>
</template>
</div>
<!-- <div class="btn-group">
<button @click="clearImage" x-show="imageUrl" class="btn btn-secondary">
Clear
</button>
</div> -->
</div>
<div
x-show="activeTab === 'camera' && !imageUrlOcr"
class="camera-tab"
>
<div class="preview-area">
<video
x-ref="video"
autoplay
playsinline
class="hidden"
></video>
<canvas x-ref="canvas" class="hidden"></canvas>
<div class="preview-placeholder">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<p>Camera Feed</p>
</div>
</div>
<div class="btn-group">
<button @click="startCamera" class="btn btn-primary">
Start Camera
</button>
<button
@click="captureImage"
class="btn btn-primary"
:disabled="!cameraActive"
>
Capture
</button>
<button
@click="stopCamera"
class="btn btn-secondary"
:disabled="!cameraActive"
>
Stop
</button>
</div>
</div>
<div class="btn-group">
<button
@click="clearImage"
x-show="imageUrlOcr"
class="btn btn-secondary"
>
Clear
</button>
</div>
<!-- Collapsible section to select sample images from thumbnails -->
<div class="sample-images-container mt-2">
<div
class="collapsible-header"
@click="sampleImagesOpen = !sampleImagesOpen"
>
<span>Sample Images</span>
<img
src="assets/arrow.svg"
height="20px"
:class="{'rotate-180': sampleImagesOpen}"
/>
</div>
<div
x-show="sampleImagesOpen"
x-transition
class="sample-images-content"
>
<div class="sample-image-grid">
<template
x-for="(image, type) in sampleDataOcr"
:key="type"
>
<span @click="loadSampleOcr(type)" class="sample-image">
<img
:src="image.url"
alt=""
width="80px"
style="max-height: 100px"
/>
</span>
</template>
</div>
</div>
</div>
<!-- Confidence Interval Slider -->
<!-- <div class="confidence-slider mt-2">
<label
for="confidence-slider"
class="block text-sm font-medium text-gray-700"
>Confidence Interval</label
>
<input
id="confidence-slider"
type="range"
min="0"
max="100"
step="1"
x-model="confidenceThreshold"
/>
<p class="text-sm mt-1">
Selected: <span x-text="confidenceThreshold"></span>%
</p>
</div> -->
<!-- Predict Button -->
<div class="mt-2">
<button
@click="predictOcr()"
class="btn btn-primary w-100"
:disabled="!imageUrlOcr"
>
Predict
</button>
</div>
</div>
<!-- Processing Card -->
<!-- Processing Card - results -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Output</h2>
<p class="card-description">
Select an application to analyze your image
</p>
</div>
<div class="application-content">
<!-- Preview content same as before -->
<span id="ocr-status-display"></span>
<textarea
id="ocr-output-display"
class="text-output-area"
></textarea>
<div class="btn-group mt-2">
<button
@click="clearResults"
class="btn btn-secondary"
>
Clear Results
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ocr
// detection / yolov11
// whisper
// medical (yolov11)
//
$ = (id) => document.getElementById(id);
function fetchWrapper(url, statusElPrefix, options = {}) {
// Get or create UI elements
const statusSpan = $(`${statusElPrefix}-status-display`);
const jsonTextarea = $(`${statusElPrefix}-output-display`);
// Update status to loading
statusSpan.textContent = "Loading...";
statusSpan.className = "status loading";
// Clear previous results
jsonTextarea.value = "";
// Make the fetch request
return fetch(url, options)
.then((response) => {
// Display status code
statusSpan.textContent = `Status: ${response.status} ${response.statusText}`;
statusSpan.className = response.ok
? "status success"
: "status error";
// Check if response is JSON
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return response.json().then((data) => {
// Format and display JSON
jsonTextarea.value = JSON.stringify(data, null, 2);
drawBoundingBoxesObjdet(data);
return data; // Return data for further processing
});
} else {
// Handle non-JSON responses
return response.text().then((text) => {
jsonTextarea.value = text;
return text; // Return text for further processing
});
}
})
.catch((error) => {
// Handle network errors
statusSpan.textContent = `Error: ${error.message}`;
statusSpan.className = "status error";
jsonTextarea.value = `Failed to fetch: ${error.message}`;
console.error("Fetch error:", error);
throw error; // Re-throw to allow caller to handle error
});
}
// Draw bounding boxes on the image
function drawBoundingBoxesObjdet(data) {
// Check if we have predictions to display
if (!data || !data.detections || !data.detections.length) return;
// Get canvas and create context for drawing
const canvasPreview = $("previewCanvas");
const ctx = canvasPreview.getContext("2d");
// Draw each bounding box
for (const box of data.detections) {
// Set styling for the box
ctx.strokeStyle = "rgba(255, 0, 0, 0.8)";
ctx.lineWidth = 2;
// Draw the rectangle
ctx.strokeRect(
box.x_min,
box.y_min,
box.x_max - box.x_min,
box.y_max - box.y_min
);
// Add label text with confidence
const label = `${box.class_name} (${Math.round(
box.confidence * 100
)}%)`;
ctx.fillStyle = "rgba(255, 0, 0, 0.8)";
// Scale font based on image dimensions (640px is the base)
const scale = Math.max(1, Math.min(2, canvasPreview.width / 640));
ctx.font = `${Math.round(14 * scale)}px Arial`;
const textWidth = ctx.measureText(label).width;
ctx.fillRect(box.x_min, box.y_min - 20, textWidth + 10, 20);
ctx.fillStyle = "white";
ctx.fillText(label, box.x_min + 5, box.y_min - 5);
};
}
document.addEventListener("alpine:init", () => {
Alpine.data("app", () => ({
darkMode: false,
activeTab: "upload",
activeModel: "object",
activeApplication: "object",
imageUrl: null,
imageUrlOcr: null,
dragOver: false,
cameraActive: false,
boundingBoxes: [],
results: [],
confidenceThreshold: 50, // Default confidence threshold,
sampleImagesOpen: false,
toggleApplication(app) {
this.activeApplication =
this.activeApplication === app ? null : app;
this.clearResults();
},
predictOcr() {
console.log("predictOcr called");
if (!this.imageUrlOcr) {
console.log("no imageUrlOcr");
return;
}
const baseUrl = $("url-ocr").value;
const apiKey = $("apiKey-ocr").value;
const imageUrlOcr = this.imageUrlOcr;
if (!baseUrl) {
alert("Please enter an API URL");
return;
}
if (!apiKey) {
alert("Please enter an API Key");
return;
}
// Fetch the blob first, then make the API call
fetch(imageUrlOcr)
.then((response) => response.blob())
.then((blob) => {
// Convert blob to base64
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
// Extract the base64 data (remove the data:image/xyz;base64, prefix)
const base64data = reader.result.split(",")[1];
// use fetchWrapper to make the API call using application/json body
fetchWrapper(`/api/req`, "ocr", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"x-req-path": `${baseUrl}`,
},
body: JSON.stringify({
instances: [],
image_base64: base64data,
}),
});
};
})
.catch((error) => {
console.error("Error preparing image:", error);
alert("Error preparing image for upload");
});
},
predictImage() {
if (!this.imageUrl) return;
const baseUrl = $("url").value;
const apiKey = $("apiKey").value;
const imageUrl = this.imageUrl;
if (!baseUrl) {
alert("Please enter an API URL");
return;
}
if (!apiKey) {
alert("Please enter an API Key");
return;
}
// Fetch the blob first, then make the API call
fetch(imageUrl)
.then((response) => response.blob())
.then((blob) => {
// use fetchWrapper to make the API call using application/octet-stream body
fetchWrapper(`/api/req`, "objdet", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${apiKey}`,
"x-req-path": `${baseUrl}`,
},
body: blob,
});
})
.catch((error) => {
console.error("Error preparing image:", error);
alert("Error preparing image for upload");
});
},
// Sample data for demo purposes
sampleData: {
catdog: {
url: "https://images.unsplash.com/photo-1606098216818-40939b7c98ad?w=600",
},
document: {
url: "https://images.unsplash.com/photo-1546410531-bb4caa6b424d?w=600",
},
food: {
url: "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=600",
},
people: {
url: "https://images.unsplash.com/photo-1527529482837-4698179dc6ce?w=600",
},
people2: {
url: "assets/img/AdobeStock_84708957.jpg",
},
confuse: {
url: "assets/img/confuse.jpeg",
},
},
sampleDataOcr: {
ausweis: {
url: "assets/img/Deutscher_Personalausweis_(2010_Version).jpg",
},
textbook: { url: "assets/img/computer_vision_textbook_001.jpeg" },
handwritten: { url: "assets/img/sample_handwritten.png" },
nlpass: { url: "assets/img/sample_nl_passport.jpg" },
},
handleFileSelect(e) {
const file = e.target.files[0];
if (file?.type.match("image.*")) {
this.imageUrl = URL.createObjectURL(file);
this.clearResults();
}
},
handleFileSelectOcr(e) {
const file = e.target.files[0];
if (file?.type.match("image.*")) {
this.imageUrlOcr = URL.createObjectURL(file);
this.clearResults();
}
},
handleDrop(e) {
this.dragOver = false;
const file = e.dataTransfer.files[0];
if (file?.type.match("image.*")) {
this.imageUrl = URL.createObjectURL(file);
this.clearResults();
}
},
handleDropOcr(e) {
this.dragOver = false;
const file = e.dataTransfer.files[0];
if (file?.type.match("image.*")) {
this.imageUrlOcr = URL.createObjectURL(file);
this.clearResults();
}
},
clearImage() {
if (this.imageUrl) {
URL.revokeObjectURL(this.imageUrl);
this.imageUrl = null;
}
this.clearResults();
},
async startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
});
this.$refs.video.srcObject = stream;
this.$refs.video.classList.remove("hidden");
this.cameraActive = true;
} catch (err) {
console.error("Error accessing camera:", err);
alert("Could not access camera. Please check permissions.");
}
},
captureImage() {
const video = this.$refs.video;
const canvas = this.$refs.canvas;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0);
this.imageUrl = canvas.toDataURL("image/png");
this.clearResults();
},
stopCamera() {
const stream = this.$refs.video.srcObject;
if (stream) {
stream.getTracks().forEach((track) => track.stop());
this.$refs.video.srcObject = null;
this.$refs.video.classList.add("hidden");
this.cameraActive = false;
}
},
loadSample(type) {
this.imageUrl = this.sampleData[type].url;
this.clearResults();
},
loadSampleOcr(type) {
this.imageUrlOcr = this.sampleDataOcr[type].url;
// this.clearResults();
},
processImage(appname) {
if (!this.imageUrl) return;
this.clearResults();
// API call
},
clearResults() {
const jsonTextareaOcr = $(`ocr-output-display`);
const jsonTextareaObjdect = $(`objdet-output-display`);
// Clear previous results
jsonTextareaOcr.value = "";
jsonTextareaObjdect.value = "";
},
}));
});
</script>
</div>
</body>
</html>