watermeter / script.js
fique5's picture
Upload folder using huggingface_hub
3b315f3 verified
Raw
History Blame Contribute Delete
11.1 kB
const MODEL_PATH = "https://huggingface.co/fique5/watermeter/resolve/main/best.onnx";;
const INPUT_SIZE = 640;
const CLASS_NAMES = [
"meter",
"window",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"u"
];
const SCORE_THRESHOLD = 0.25;
const IOU_THRESHOLD = 0.45;
const MAX_BOXES = 120;
let session = null;
let selectedImage = null;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const imageInput = document.getElementById("imageInput");
const browseBtn = document.getElementById("browseBtn");
const dropZone = document.getElementById("dropZone");
const predictBtn = document.getElementById("predictBtn");
const modelStatus = document.getElementById("modelStatus");
const meterReading = document.getElementById("meterReading");
const confidence = document.getElementById("confidence");
const detectionsList = document.getElementById("detectionsList");
const imageInfo = document.getElementById("imageInfo");
async function loadModel() {
modelStatus.textContent = "Loading model...";
modelStatus.className = "badge loading";
try {
session = await ort.InferenceSession.create(MODEL_PATH, {
executionProviders: ["wasm"]
});
modelStatus.textContent = "Model ready";
modelStatus.className = "badge ready";
} catch (error) {
console.error(error);
modelStatus.textContent = "Model failed";
modelStatus.className = "badge error";
detectionsList.innerHTML =
"<p class=\"small-text\">Unable to load the ONNX model. Make sure best.onnx is present in the repository root.</p>";
}
}
function setDropZoneState(active) {
if (active) {
dropZone.classList.add("drag-over");
} else {
dropZone.classList.remove("drag-over");
}
}
browseBtn.addEventListener("click", () => imageInput.click());
imageInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
loadImage(file);
}
});
dropZone.addEventListener("dragover", (event) => {
event.preventDefault();
setDropZoneState(true);
});
dropZone.addEventListener("dragleave", () => setDropZoneState(false));
dropZone.addEventListener("drop", (event) => {
event.preventDefault();
setDropZoneState(false);
const file = event.dataTransfer.files[0];
if (file) {
loadImage(file);
}
});
window.addEventListener("dragover", (event) => {
event.preventDefault();
});
window.addEventListener("drop", (event) => {
event.preventDefault();
});
function loadImage(file) {
const img = new Image();
img.onload = () => {
selectedImage = img;
canvas.width = img.width;
canvas.height = img.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
imageInfo.textContent = `${img.width}px × ${img.height}px`;
meterReading.textContent = "--";
confidence.textContent = "--";
detectionsList.innerHTML =
"<p class=\"small-text\">Ready to analyze. Press Analyze Image.</p>";
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
}
predictBtn.addEventListener("click", runPrediction);
async function runPrediction() {
if (!selectedImage) {
alert("Please upload an image first.");
return;
}
if (!session) {
alert("Model is not ready yet. Wait until the model finishes loading.");
return;
}
modelStatus.textContent = "Running inference...";
modelStatus.className = "badge loading";
try {
const prediction = await predictImage(selectedImage);
drawDetectionResults(prediction.detections);
updatePredictionUI(prediction);
modelStatus.textContent = "Ready";
modelStatus.className = "badge ready";
} catch (error) {
console.error(error);
modelStatus.textContent = "Inference failed";
modelStatus.className = "badge error";
detectionsList.innerHTML =
"<p class=\"small-text\">Inference failed. Check your model path and image format.</p>";
}
}
async function predictImage(image) {
const {tensor, ratio, pad, originalWidth, originalHeight} = prepareInput(image);
const inputName = session.inputNames[0];
const feeds = {};
feeds[inputName] = new ort.Tensor("float32", [1, 3, INPUT_SIZE, INPUT_SIZE], tensor);
const results = await session.run(feeds);
const outputName = session.outputNames[0];
const rawOutput = results[outputName];
const detections = decodeOutput(
rawOutput.data,
rawOutput.dims,
ratio,
pad,
originalWidth,
originalHeight
);
return {
detections,
reading: extractMeterReading(detections),
averageConfidence: computeAverageConfidence(detections)
};
}
function prepareInput(image) {
const letterbox = letterboxImage(image, INPUT_SIZE);
const imageData = letterbox.imageData;
const floatArray = new Float32Array(1 * 3 * INPUT_SIZE * INPUT_SIZE);
for (let y = 0; y < INPUT_SIZE; y++) {
for (let x = 0; x < INPUT_SIZE; x++) {
const idx = (y * INPUT_SIZE + x) * 4;
const r = imageData.data[idx] / 255;
const g = imageData.data[idx + 1] / 255;
const b = imageData.data[idx + 2] / 255;
const pos = y * INPUT_SIZE + x;
floatArray[pos] = r;
floatArray[INPUT_SIZE * INPUT_SIZE + pos] = g;
floatArray[2 * INPUT_SIZE * INPUT_SIZE + pos] = b;
}
}
return {
tensor: floatArray,
ratio: letterbox.ratio,
pad: letterbox.pad,
originalWidth: image.width,
originalHeight: image.height
};
}
function letterboxImage(image, size) {
const offscreen = document.createElement("canvas");
offscreen.width = size;
offscreen.height = size;
const ctxOff = offscreen.getContext("2d");
ctxOff.fillStyle = "#000";
ctxOff.fillRect(0, 0, size, size);
const ratio = Math.min(size / image.width, size / image.height);
const newWidth = Math.round(image.width * ratio);
const newHeight = Math.round(image.height * ratio);
const padX = Math.round((size - newWidth) / 2);
const padY = Math.round((size - newHeight) / 2);
ctxOff.drawImage(image, 0, 0, image.width, image.height, padX, padY, newWidth, newHeight);
return {
imageData: ctxOff.getImageData(0, 0, size, size),
ratio,
pad: { x: padX, y: padY }
};
}
function decodeOutput(data, dims, ratio, pad, originalWidth, originalHeight) {
const [batch, numBoxes, attributes] = dims;
const detections = [];
for (let i = 0; i < numBoxes; i++) {
const offset = i * attributes;
const x = data[offset];
const y = data[offset + 1];
const w = data[offset + 2];
const h = data[offset + 3];
const objectness = data[offset + 4];
let bestClass = -1;
let bestScore = 0;
for (let c = 0; c < CLASS_NAMES.length; c++) {
const classScore = data[offset + 5 + c];
const score = objectness * classScore;
if (score > bestScore) {
bestScore = score;
bestClass = c;
}
}
if (bestScore < SCORE_THRESHOLD) {
continue;
}
const x1 = (x - w / 2 - pad.x) / ratio;
const y1 = (y - h / 2 - pad.y) / ratio;
const x2 = (x + w / 2 - pad.x) / ratio;
const y2 = (y + h / 2 - pad.y) / ratio;
detections.push({
classIndex: bestClass,
label: CLASS_NAMES[bestClass],
score: bestScore,
x1: clamp(x1, 0, originalWidth),
y1: clamp(y1, 0, originalHeight),
x2: clamp(x2, 0, originalWidth),
y2: clamp(y2, 0, originalHeight)
});
}
return nonMaxSuppression(detections, IOU_THRESHOLD, MAX_BOXES);
}
function nonMaxSuppression(detections, iouThreshold, maxBoxes) {
const results = [];
const sorted = detections.sort((a, b) => b.score - a.score);
while (sorted.length && results.length < maxBoxes) {
const current = sorted.shift();
results.push(current);
for (let i = sorted.length - 1; i >= 0; i--) {
if (current.classIndex !== sorted[i].classIndex) {
continue;
}
if (intersectionOverUnion(current, sorted[i]) > iouThreshold) {
sorted.splice(i, 1);
}
}
}
return results;
}
function intersectionOverUnion(a, b) {
const x1 = Math.max(a.x1, b.x1);
const y1 = Math.max(a.y1, b.y1);
const x2 = Math.min(a.x2, b.x2);
const y2 = Math.min(a.y2, b.y2);
const width = Math.max(0, x2 - x1);
const height = Math.max(0, y2 - y1);
const intersection = width * height;
const union =
(a.x2 - a.x1) * (a.y2 - a.y1) +
(b.x2 - b.x1) * (b.y2 - b.y1) -
intersection;
return union === 0 ? 0 : intersection / union;
}
function drawDetectionResults(detections) {
if (!selectedImage) {
return;
}
canvas.width = selectedImage.width;
canvas.height = selectedImage.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(selectedImage, 0, 0);
detections.forEach((detection) => {
const width = detection.x2 - detection.x1;
const height = detection.y2 - detection.y1;
ctx.strokeStyle = detection.classIndex === 0 ? "#00d4ff" : "#ffb703";
ctx.lineWidth = Math.max(2, Math.round(canvas.width / 360));
ctx.strokeRect(detection.x1, detection.y1, width, height);
const label = `${detection.label} ${(detection.score * 100).toFixed(1)}%`;
ctx.font = `${Math.max(12, Math.round(canvas.width / 60))}px Inter`;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(0, 0, 0, 0.65)";
const textWidth = ctx.measureText(label).width + 16;
const textHeight = parseInt(ctx.font, 10) + 10;
const textX = detection.x1;
const textY = Math.max(0, detection.y1 - textHeight - 4);
ctx.fillRect(textX, textY, textWidth, textHeight);
ctx.fillStyle = "#ffffff";
ctx.fillText(label, textX + 8, textY + 5);
});
}
function extractMeterReading(detections) {
const digits = detections.filter(
(item) => item.classIndex >= 2 && item.classIndex <= 11
);
const unknown = detections.some((item) => item.classIndex === 12);
if (!digits.length) {
if (unknown) {
return "Unreadable";
}
return "No digits detected";
}
const ordered = digits.sort((a, b) => a.x1 - b.x1);
return ordered.map((item) => item.label).join("");
}
function computeAverageConfidence(detections) {
const digits = detections.filter(
(item) => item.classIndex >= 2 && item.classIndex <= 11
);
if (!digits.length) {
return 0;
}
const sum = digits.reduce((acc, item) => acc + item.score, 0);
return sum / digits.length;
}
function updatePredictionUI(prediction) {
const { detections, reading, averageConfidence } = prediction;
meterReading.textContent = reading;
confidence.textContent = averageConfidence
? `${(averageConfidence * 100).toFixed(1)}%`
: "--";
if (!detections.length) {
detectionsList.innerHTML =
"<p class=\"small-text\">No objects detected in this image.</p>";
return;
}
detectionsList.innerHTML = detections
.slice(0, 20)
.map(
(item) =>
`<div class="detection-card"><strong>${item.label}</strong><span>Score: ${(item.score * 100).toFixed(1)}%</span></div>`
)
.join("");
}
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
loadModel();