Add project files

This commit is contained in:
Suman
2026-08-04 17:09:29 +05:30
parent 464a68cceb
commit ac76cc86bc
372 changed files with 12221 additions and 0 deletions

988
static/js/main.js Normal file
View File

@@ -0,0 +1,988 @@
(() => {
const templatesData = JSON.parse(document.getElementById("templates-data").textContent);
const templateFileByName = {};
templatesData.forEach(t => { templateFileByName[t.name] = t.filename; });
const fileInput = document.getElementById("file-input");
const dropzone = document.getElementById("dropzone");
const dropzoneEmpty = document.getElementById("dropzone-empty");
const previewImg = document.getElementById("preview-img");
const submitBtn = document.getElementById("submit-btn");
const clearBtn = document.getElementById("clear-btn");
const errorMsg = document.getElementById("error-msg");
const loading = document.getElementById("loading");
const loadingText = document.getElementById("loading-text");
const resultsSection = document.getElementById("results");
const totalTimeEl = document.getElementById("total-time");
const overallCard = document.getElementById("overall-card");
const queryRow = document.getElementById("query-row");
const methodGrid = document.getElementById("method-grid");
const inputPalette = document.getElementById("input-palette");
const colorGrid = document.getElementById("color-grid");
const shapeGrid = document.getElementById("shape-grid");
const shapeVisual = document.getElementById("shape-visual");
const shapeOverall = document.getElementById("shape-overall");
const shapeVisualRow = document.getElementById("shape-visual-row");
const textureGrid = document.getElementById("texture-grid");
const textureVisual = document.getElementById("texture-visual");
const textureOverall = document.getElementById("texture-overall");
const textureVisualRow = document.getElementById("texture-visual-row");
const textureGlcmTable = document.getElementById("texture-glcm-table");
const weightedSub = document.getElementById("weighted-sub");
const weightedCard = document.getElementById("weighted-card");
const weightedRanked = document.getElementById("weighted-ranked");
const weightedFlowerCheck = document.getElementById("weighted-flower-check");
const weightedFlowerCheckLoading = document.getElementById("weighted-flower-check-loading");
const weightedFlowerCheckBody = document.getElementById("weighted-flower-check-body");
const weightedFlowerCheckError = document.getElementById("weighted-flower-check-error");
const wfcCountValue = document.getElementById("wfc-count-value");
const wfcClipValue = document.getElementById("wfc-clip-value");
const familyGridSection = document.getElementById("family-grid-section");
const familyGridTemplateName = document.getElementById("family-grid-template-name");
const familyGridOverall = document.getElementById("family-grid-overall");
const familyGridRow = document.getElementById("family-grid-row");
const familyGridMatches = document.getElementById("family-grid-matches");
const verifySection = document.getElementById("verify-section");
const verifyTemplateName = document.getElementById("verify-template-name");
const verifyGlass = document.getElementById("verify-glass");
const countFlowersBtn = document.getElementById("count-flowers-btn");
const flowerCountLoading = document.getElementById("flower-count-loading");
const flowerCountError = document.getElementById("flower-count-error");
const flowerCountResult = document.getElementById("flower-count-result");
const flowerCountImage = document.getElementById("flower-count-image");
const flowerCountTotal = document.getElementById("flower-count-total");
const flowerCountClusters = document.getElementById("flower-count-clusters");
const flowerCountTime = document.getElementById("flower-count-time");
const yoloCountImage = document.getElementById("yolo-count-image");
const yoloCountTotal = document.getElementById("yolo-count-total");
const vaseCompare = document.getElementById("vase-compare");
const vaseCompareError = document.getElementById("vase-compare-error");
const vaseCompareBody = document.getElementById("vase-compare-body");
const vaseCropInput = document.getElementById("vase-crop-input");
const vaseCropTemplate = document.getElementById("vase-crop-template");
const vaseCropTemplateName = document.getElementById("vase-crop-template-name");
const vaseVerdictPill = document.getElementById("vase-verdict-pill");
const vaseDinoBar = document.getElementById("vase-dino-bar");
const vaseDinoValue = document.getElementById("vase-dino-value");
const vaseClipBar = document.getElementById("vase-clip-bar");
const vaseClipValue = document.getElementById("vase-clip-value");
const vaseCombinedBar = document.getElementById("vase-combined-bar");
const vaseCombinedValue = document.getElementById("vase-combined-value");
const flowerCountMismatch = document.getElementById("flower-count-mismatch");
const flowerCountMismatchText = document.getElementById("flower-count-mismatch-text");
let selectedFile = null;
let currentRequestId = null;
let currentWeightedBest = null;
function showError(msg) {
errorMsg.textContent = msg;
errorMsg.hidden = !msg;
}
function setSelectedFile(file) {
if (!file) return;
if (!file.type.startsWith("image/")) {
showError("Please choose an image file.");
return;
}
showError("");
selectedFile = file;
const reader = new FileReader();
reader.onload = e => {
previewImg.src = e.target.result;
previewImg.hidden = false;
dropzoneEmpty.hidden = true;
};
reader.readAsDataURL(file);
submitBtn.disabled = false;
clearBtn.hidden = false;
}
fileInput.addEventListener("change", () => setSelectedFile(fileInput.files[0]));
["dragover", "dragenter"].forEach(evt =>
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.classList.add("dragover");
})
);
["dragleave", "drop"].forEach(evt =>
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.classList.remove("dragover");
})
);
dropzone.addEventListener("drop", e => {
const file = e.dataTransfer.files[0];
if (file) setSelectedFile(file);
});
clearBtn.addEventListener("click", e => {
e.preventDefault();
selectedFile = null;
fileInput.value = "";
previewImg.hidden = true;
dropzoneEmpty.hidden = false;
submitBtn.disabled = true;
clearBtn.hidden = true;
showError("");
resultsSection.hidden = true;
});
const LOADING_MESSAGES = [
"Removing background…",
"Extracting SIFT & ORB keypoints…",
"Running SuperPoint + LightGlue…",
"Running LoFTR…",
"Scoring templates…",
];
function cycleLoadingMessages() {
let i = 0;
loadingText.textContent = LOADING_MESSAGES[0];
return setInterval(() => {
i = (i + 1) % LOADING_MESSAGES.length;
loadingText.textContent = LOADING_MESSAGES[i];
}, 1400);
}
submitBtn.addEventListener("click", async () => {
if (!selectedFile) return;
showError("");
submitBtn.disabled = true;
resultsSection.hidden = true;
loading.hidden = false;
const msgTimer = cycleLoadingMessages();
try {
const form = new FormData();
form.append("image", selectedFile);
const res = await fetch("/api/match", { method: "POST", body: form });
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Something went wrong.");
}
renderResults(data);
} catch (err) {
showError(err.message || String(err));
} finally {
clearInterval(msgTimer);
loading.hidden = true;
submitBtn.disabled = false;
}
});
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function uploadUrl(requestId, filename) {
return `/uploads/${requestId}/${filename}`;
}
function renderResults(data) {
currentRequestId = data.request_id;
currentWeightedBest = data.weighted_best;
flowerCountResult.hidden = true;
flowerCountError.hidden = true;
flowerCountLoading.hidden = true;
vaseCompare.hidden = true;
flowerCountMismatch.hidden = true;
weightedFlowerCheck.hidden = true;
countFlowersBtn.disabled = false;
countFlowersBtn.textContent = "Count flowers";
totalTimeEl.textContent =
`Total processing time: ${data.total_time_sec.toFixed(2)}s ` +
`(background removal: ${data.bg_removal_time_sec.toFixed(2)}s)`;
// --- weighted final match (rendered first -- it now leads #results so
// the final verdict is visible without scrolling past every card) ---
renderWeighted(data);
// --- overall best ---
overallCard.innerHTML = "";
if (data.overall_best) {
const filename = templateFileByName[data.overall_best];
if (filename) {
const img = el("img", "overall-thumb");
img.src = `/template_image/${encodeURIComponent(filename)}`;
img.alt = data.overall_best;
overallCard.appendChild(img);
}
const textWrap = el("div", "overall-text");
textWrap.appendChild(el("p", "label", "Overall best match"));
textWrap.appendChild(el("h3", null, data.overall_best));
overallCard.appendChild(textWrap);
} else {
overallCard.appendChild(el("p", null, "No confident match found across any method."));
}
// --- query row: original + background removed ---
queryRow.innerHTML = "";
queryRow.appendChild(buildQueryTile(
uploadUrl(data.request_id, data.upload_original_file), "Your upload"
));
queryRow.appendChild(buildQueryTile(
uploadUrl(data.request_id, data.upload_nobg_file), "Background removed"
));
// --- per-method cards (Color is excluded here -- it has its own
// dedicated section below and only participates in the weighted
// verdict, not this grid) ---
methodGrid.innerHTML = "";
Object.entries(data.methods).forEach(([methodKey, m]) => {
if (methodKey === "Color") return;
methodGrid.appendChild(buildMethodCard(data.request_id, methodKey, m));
});
// --- color space section (own display; also weighted into the verdict) ---
renderColorAnalysis(data);
// --- shape matching + texture matching (independent sections, no
// score/verdict involvement at all) ---
renderShapeAnalysis(data);
renderTextureAnalysis(data);
// --- color family grid (visual, tied to the weighted-best template) ---
renderFamilyGrid(data);
resultsSection.hidden = false;
resultsSection.scrollIntoView({ behavior: "smooth", block: "start" });
// --- AI verification (fires after everything above is already on
// screen; a slow or unreachable external endpoint should never block
// or affect the core match results) ---
if (data.weighted_best) {
runVerification(data, data.weighted_best);
runFlowerSummary(data.request_id, data.weighted_best);
}
}
async function runFlowerSummary(requestId, template) {
weightedFlowerCheck.hidden = false;
weightedFlowerCheckLoading.hidden = false;
weightedFlowerCheckBody.hidden = true;
weightedFlowerCheckError.hidden = true;
try {
const res = await fetch("/api/flower_summary", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_id: requestId, template }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Flower check failed.");
}
if (data.error) {
throw new Error(data.error);
}
const fc = data.flower_count_comparison;
wfcCountValue.textContent = fc ? `${fc.input_count} vs ${fc.template_count}` : "n/a";
wfcCountValue.title = fc ? fc.message : "";
wfcClipValue.textContent = data.flower_clip_similarity_pct != null
? `${data.flower_clip_similarity_pct}%` : "n/a";
weightedFlowerCheckLoading.hidden = true;
weightedFlowerCheckBody.hidden = false;
} catch (err) {
weightedFlowerCheckLoading.hidden = true;
weightedFlowerCheckError.textContent = err.message || String(err);
weightedFlowerCheckError.hidden = false;
}
}
async function runVerification(matchData, template) {
verifySection.hidden = false;
verifyTemplateName.textContent = template;
verifyGlass.innerHTML = "";
const loadingWrap = el("div", "verify-loading");
loadingWrap.appendChild(el("div", "spinner"));
loadingWrap.appendChild(el("p", null, "Cross-checking with an AI vision model…"));
verifyGlass.appendChild(loadingWrap);
try {
const res = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_id: matchData.request_id, template }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Verification failed.");
}
renderVerification(matchData, template, data);
} catch (err) {
verifyGlass.innerHTML = "";
verifyGlass.appendChild(el("p", "verify-error",
`AI verification is unavailable right now (${err.message || err}).`));
}
}
const DESCRIPTION_PREVIEW_LEN = 220;
function combinedDescription(data) {
const hasDiscrepancies = data.discrepancies && data.discrepancies.toLowerCase() !== "none";
if (data.description && hasDiscrepancies) {
return `${data.description} Discrepancies: ${data.discrepancies}`;
}
if (data.description) return data.description;
if (hasDiscrepancies) return `Discrepancies: ${data.discrepancies}`;
return "No discrepancies were found between the two images.";
}
function renderVerification(matchData, template, data) {
verifyGlass.innerHTML = "";
// --- the two photos being compared ---
const imagesRow = el("div", "verify-images");
const uploadTile = el("div", "verify-image-tile");
const uploadImg = el("img");
uploadImg.src = uploadUrl(matchData.request_id, matchData.upload_original_file);
uploadImg.alt = "Your upload";
uploadTile.appendChild(uploadImg);
uploadTile.appendChild(el("span", null, "Your upload"));
imagesRow.appendChild(uploadTile);
imagesRow.appendChild(el("span", "verify-vs", "vs"));
const templateTile = el("div", "verify-image-tile");
const templateFilename = templateFileByName[template];
if (templateFilename) {
const templateImg = el("img");
templateImg.src = `/template_image/${encodeURIComponent(templateFilename)}`;
templateImg.alt = template;
templateTile.appendChild(templateImg);
}
templateTile.appendChild(el("span", null, template));
imagesRow.appendChild(templateTile);
verifyGlass.appendChild(imagesRow);
// --- Match: Yes/No/Partial ---
const matchKey = (data.match || "unknown").toLowerCase();
const matchValue = { yes: "Yes", no: "No", partial: "Partial" }[matchKey] || "Unclear";
const matchLine = el("p", `verify-match-line ${matchKey}`);
matchLine.appendChild(el("span", "match-label", "Match: "));
matchLine.appendChild(el("span", "match-value", matchValue));
if (data.confidence) {
matchLine.appendChild(el("span", "verify-confidence", `Confidence: ${data.confidence}`));
}
verifyGlass.appendChild(matchLine);
// --- Description: truncated, with a Read more / Show less toggle ---
const fullText = combinedDescription(data);
const descWrap = el("p", "verify-description");
const label = el("span", "match-label", "Description: ");
const textSpan = el("span", "verify-description-text");
descWrap.appendChild(label);
descWrap.appendChild(textSpan);
verifyGlass.appendChild(descWrap);
if (fullText.length <= DESCRIPTION_PREVIEW_LEN) {
textSpan.textContent = fullText;
} else {
let expanded = false;
const toggle = el("a", "read-more-toggle", "Read more");
toggle.href = "#";
const renderText = () => {
textSpan.textContent = expanded
? fullText + " "
: fullText.slice(0, DESCRIPTION_PREVIEW_LEN).trim() + "… ";
toggle.textContent = expanded ? "Show less" : "Read more";
};
toggle.addEventListener("click", e => {
e.preventDefault();
expanded = !expanded;
renderText();
});
renderText();
descWrap.appendChild(toggle);
}
if (data.pixel_precheck) {
const { hash_distance, verdict } = data.pixel_precheck;
verifyGlass.appendChild(el("p", "verify-footnote",
`Pixel pre-check: ${verdict} (hash distance ${hash_distance})`));
}
}
countFlowersBtn.addEventListener("click", async () => {
if (!currentRequestId) return;
countFlowersBtn.disabled = true;
flowerCountError.hidden = true;
flowerCountResult.hidden = true;
flowerCountLoading.hidden = false;
try {
const res = await fetch("/api/count_flowers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_id: currentRequestId, template: currentWeightedBest }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Flower counting failed.");
}
renderFlowerCount(data);
} catch (err) {
flowerCountError.textContent = err.message || String(err);
flowerCountError.hidden = false;
} finally {
flowerCountLoading.hidden = true;
countFlowersBtn.disabled = false;
countFlowersBtn.textContent = "Recount";
}
});
function renderFlowerCount(data) {
const sam = data.sam || {};
const yolo = data.yolo || {};
if (sam.error && yolo.error) {
flowerCountError.textContent = sam.error || yolo.error;
flowerCountError.hidden = false;
return;
}
if (!sam.error) {
flowerCountImage.hidden = false;
flowerCountImage.src = `${uploadUrl(data.request_id, sam.visual_file)}?t=${Date.now()}`;
flowerCountTotal.textContent = `${sam.total_count} flower${sam.total_count === 1 ? "" : "s"} detected`;
flowerCountClusters.innerHTML = "";
(sam.clusters || []).forEach(c => {
const chip = el("span", "flower-cluster-chip");
const swatch = el("span", "swatch");
swatch.style.background = rgbCss(c.color_rgb);
chip.appendChild(swatch);
chip.appendChild(el("span", null, `${c.count}`));
flowerCountClusters.appendChild(chip);
});
if ((sam.clusters || []).length > 1) {
flowerCountClusters.appendChild(
el("span", "flower-cluster-hint", `~${sam.clusters.length} distinct kinds (by color)`)
);
}
} else {
flowerCountImage.hidden = true;
flowerCountTotal.textContent = `SAM unavailable: ${sam.error}`;
flowerCountClusters.innerHTML = "";
}
if (!yolo.error) {
yoloCountImage.hidden = false;
yoloCountImage.src = `${uploadUrl(data.request_id, yolo.visual_file)}?t=${Date.now()}`;
const parts = [`${yolo.flower_count} flower region${yolo.flower_count === 1 ? "" : "s"}`];
if (yolo.vase_count) parts.push(`${yolo.vase_count} vase`);
if (yolo.ribbon_count) parts.push(`${yolo.ribbon_count} ribbon/bow`);
yoloCountTotal.textContent = parts.join(" · ");
} else {
yoloCountImage.hidden = true;
yoloCountTotal.textContent = `YOLO-World unavailable: ${yolo.error}`;
}
flowerCountTime.hidden = false;
flowerCountTime.textContent = `Processed in ${data.time_sec.toFixed(2)}s`;
flowerCountResult.hidden = false;
renderFlowerCountMismatch(data.flower_count_comparison);
renderVaseComparison(data.vase_comparison);
}
function renderFlowerCountMismatch(fc) {
if (!fc) {
flowerCountMismatch.hidden = true;
return;
}
flowerCountMismatch.hidden = false;
flowerCountMismatchText.textContent = fc.error
? `Flower-count comparison unavailable: ${fc.error}`
: fc.message;
flowerCountMismatch.classList.toggle("is-mismatch", !fc.error && fc.diff !== 0);
}
function renderVaseComparison(vc) {
if (!vc) {
vaseCompare.hidden = true;
return;
}
vaseCompare.hidden = false;
if (vc.error) {
vaseCompareError.textContent = vc.error;
vaseCompareError.hidden = false;
vaseCompareBody.hidden = true;
return;
}
vaseCompareError.hidden = true;
vaseCompareBody.hidden = false;
vaseCropInput.src = `${uploadUrl(currentRequestId, vc.input_crop_file)}?t=${Date.now()}`;
vaseCropTemplate.src = `${uploadUrl(currentRequestId, vc.template_crop_file)}?t=${Date.now()}`;
vaseCropTemplateName.textContent = vc.template;
const verdictLabel = { same: "Same vase", uncertain: "Uncertain", different: "Different vase" }[vc.verdict]
|| vc.verdict;
vaseVerdictPill.textContent = `${verdictLabel}${vc.combined_pct}% combined similarity`;
vaseVerdictPill.className = `vase-verdict-pill ${vc.verdict}`;
vaseDinoBar.style.width = `${vc.dino_similarity_pct}%`;
vaseDinoValue.textContent = `${vc.dino_similarity_pct}%`;
vaseClipBar.style.width = `${vc.clip_similarity_pct}%`;
vaseClipValue.textContent = `${vc.clip_similarity_pct}%`;
vaseCombinedBar.style.width = `${vc.combined_pct}%`;
vaseCombinedValue.textContent = `${vc.combined_pct}%`;
}
function rgbCss(rgb) {
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
}
function renderColorAnalysis(data) {
const analysis = data.color_analysis || { input_dominant_colors: [], templates: [] };
inputPalette.innerHTML = "";
if (analysis.input_dominant_colors.length) {
inputPalette.appendChild(el("span", "palette-label", "Your photo's colors:"));
analysis.input_dominant_colors.forEach(c => {
const chip = el("span", "swatch-chip");
const swatch = el("span", "swatch");
swatch.style.background = rgbCss(c.rgb);
chip.appendChild(swatch);
chip.appendChild(document.createTextNode(`${c.pct}%`));
inputPalette.appendChild(chip);
});
} else if (data.color_analysis_error) {
inputPalette.appendChild(el("p", "verify-error",
`Color analysis unavailable (${data.color_analysis_error}).`));
}
colorGrid.innerHTML = "";
analysis.templates.forEach(t => {
colorGrid.appendChild(buildColorCard(t));
});
}
function buildColorCard(t) {
const card = el("div", "color-card");
const head = el("div", "color-card-head");
const filename = templateFileByName[t.template];
if (filename) {
const img = el("img");
img.src = `/template_image/${encodeURIComponent(filename)}`;
img.alt = t.template;
head.appendChild(img);
}
head.appendChild(el("span", "name", t.template));
card.appendChild(head);
const matchRow = el("div", "color-match-row");
const track = el("div", "score-bar-track");
const fill = el("div", "score-bar-fill");
fill.style.width = `${Math.max(2, t.match_pct)}%`;
track.appendChild(fill);
matchRow.appendChild(track);
matchRow.appendChild(el("span", "match-pct", `${t.match_pct}% match`));
card.appendChild(matchRow);
const pairs = el("div", "color-pairs");
t.color_pairs.forEach(p => {
const pair = el("div", "color-pair");
const swatches = el("div", "pair-swatches");
const inputSwatch = el("span", "swatch");
inputSwatch.style.background = rgbCss(p.input_rgb);
inputSwatch.title = `Your photo — ${p.input_pct}%`;
swatches.appendChild(inputSwatch);
swatches.appendChild(el("span", "pair-arrow", "→"));
const templateSwatch = el("span", "swatch");
templateSwatch.style.background = rgbCss(p.template_rgb);
templateSwatch.title = `${t.template}${p.template_pct}%`;
swatches.appendChild(templateSwatch);
pair.appendChild(swatches);
pair.appendChild(el("span", "pair-similarity", `${p.similarity}% alike`));
pairs.appendChild(pair);
});
card.appendChild(pairs);
return card;
}
function renderShapeAnalysis(data) {
const analysis = data.shape_analysis || { results: [], visuals: null, error: null };
shapeGrid.innerHTML = "";
if (!analysis.results.length && analysis.error) {
shapeGrid.appendChild(el("p", "verify-error", `Shape analysis unavailable (${analysis.error}).`));
} else {
analysis.results.forEach(t => shapeGrid.appendChild(buildShapeCard(t)));
}
const best = analysis.results[0];
if (analysis.visuals && best && data.weighted_best) {
shapeVisual.hidden = false;
shapeOverall.innerHTML = "";
const wrap = el("div");
const bestForWeighted = analysis.results.find(r => r.template === data.weighted_best) || best;
wrap.appendChild(el("span", "big-pct", `${bestForWeighted.match_pct}%`));
wrap.appendChild(el("span", "big-pct-label", `Overall shape match vs ${data.weighted_best}`));
shapeOverall.appendChild(wrap);
shapeVisualRow.innerHTML = "";
shapeVisualRow.appendChild(buildGridTile(
uploadUrl(data.request_id, analysis.visuals.input_file), "Your upload"));
shapeVisualRow.appendChild(buildGridTile(
uploadUrl(data.request_id, analysis.visuals.template_file), data.weighted_best));
shapeVisualRow.appendChild(buildGridTile(
uploadUrl(data.request_id, analysis.visuals.overlay_file), "Overlap"));
} else {
shapeVisual.hidden = true;
}
}
function buildShapeCard(t) {
const card = el("div", "shape-card");
const head = el("div", "shape-card-head");
const filename = templateFileByName[t.template];
if (filename) {
const img = el("img");
img.src = `/template_image/${encodeURIComponent(filename)}`;
img.alt = t.template;
head.appendChild(img);
}
head.appendChild(el("span", "name", t.template));
card.appendChild(head);
const matchRow = el("div", "shape-match-row");
const track = el("div", "score-bar-track");
const fill = el("div", "score-bar-fill");
fill.style.width = `${Math.max(2, t.match_pct)}%`;
track.appendChild(fill);
matchRow.appendChild(track);
matchRow.appendChild(el("span", "match-pct", `${t.match_pct}% match`));
card.appendChild(matchRow);
card.appendChild(el("p", "shape-subscores",
`Hu-moment similarity ${t.hu_similarity_pct}% · silhouette overlap ${t.iou_pct}%`));
return card;
}
function renderTextureAnalysis(data) {
const analysis = data.texture_analysis || { results: [], visuals: null, error: null };
textureGrid.innerHTML = "";
if (!analysis.results.length && analysis.error) {
textureGrid.appendChild(el("p", "verify-error", `Texture analysis unavailable (${analysis.error}).`));
} else {
analysis.results.forEach(t => textureGrid.appendChild(buildTextureCard(t)));
}
const best = analysis.results[0];
const bestForWeighted = data.weighted_best
? analysis.results.find(r => r.template === data.weighted_best)
: null;
if (analysis.visuals && best && bestForWeighted) {
textureVisual.hidden = false;
textureOverall.innerHTML = "";
const wrap = el("div");
wrap.appendChild(el("span", "big-pct", `${bestForWeighted.match_pct}%`));
wrap.appendChild(el("span", "big-pct-label", `Overall texture match vs ${data.weighted_best}`));
textureOverall.appendChild(wrap);
textureVisualRow.innerHTML = "";
textureVisualRow.appendChild(buildGridTile(
uploadUrl(data.request_id, analysis.visuals.input_file), "Your upload (LBP)"));
textureVisualRow.appendChild(buildGridTile(
uploadUrl(data.request_id, analysis.visuals.template_file), `${data.weighted_best} (LBP)`));
textureGlcmTable.innerHTML = "";
const head = el("div", "row head");
head.appendChild(el("span", "prop-name", "GLCM property"));
head.appendChild(el("span", "value", "Yours"));
head.appendChild(el("span", "value", data.weighted_best));
head.appendChild(el("span", "value", "Alike"));
textureGlcmTable.appendChild(head);
Object.keys(bestForWeighted.input_glcm_features).forEach(prop => {
const a = bestForWeighted.input_glcm_features[prop];
const b = bestForWeighted.template_glcm_features[prop];
const scale = Math.max(Math.abs(a), Math.abs(b), 1e-9);
const alike = Math.max(0, 100 * (1 - Math.abs(a - b) / scale));
const row = el("div", "row");
row.appendChild(el("span", "prop-name", prop));
row.appendChild(el("span", "value", String(a)));
row.appendChild(el("span", "value", String(b)));
row.appendChild(el("span", "value", `${alike.toFixed(1)}%`));
textureGlcmTable.appendChild(row);
});
} else {
textureVisual.hidden = true;
}
}
function buildTextureCard(t) {
const card = el("div", "texture-card");
const head = el("div", "texture-card-head");
const filename = templateFileByName[t.template];
if (filename) {
const img = el("img");
img.src = `/template_image/${encodeURIComponent(filename)}`;
img.alt = t.template;
head.appendChild(img);
}
head.appendChild(el("span", "name", t.template));
card.appendChild(head);
const matchRow = el("div", "texture-match-row");
const track = el("div", "score-bar-track");
const fill = el("div", "score-bar-fill");
fill.style.width = `${Math.max(2, t.match_pct)}%`;
track.appendChild(fill);
matchRow.appendChild(track);
matchRow.appendChild(el("span", "match-pct", `${t.match_pct}% match`));
card.appendChild(matchRow);
card.appendChild(el("p", "texture-subscores",
`LBP pattern ${t.lbp_similarity_pct}% · GLCM statistics ${t.glcm_similarity_pct}%`));
return card;
}
function buildGridTile(src, label) {
const tile = el("div", "grid-tile");
const img = el("img");
img.src = src;
img.alt = label;
tile.appendChild(img);
tile.appendChild(el("div", "tile-label", label));
return tile;
}
function renderFamilyGrid(data) {
const fg = data.family_grid;
if (!fg || fg.error || !fg.input_grid_file) {
familyGridSection.hidden = true;
return;
}
familyGridSection.hidden = false;
familyGridTemplateName.textContent = fg.template;
familyGridOverall.innerHTML = "";
if (fg.overall_area_match_pct != null) {
const wrap = el("div");
wrap.appendChild(el("span", "big-pct", `${fg.overall_area_match_pct}%`));
wrap.appendChild(el("span", "big-pct-label", "Overall area match"));
familyGridOverall.appendChild(wrap);
}
familyGridRow.innerHTML = "";
familyGridRow.appendChild(
buildGridTile(uploadUrl(data.request_id, fg.input_grid_file), "Your upload")
);
familyGridRow.appendChild(
buildGridTile(uploadUrl(data.request_id, fg.template_grid_file), fg.template)
);
familyGridMatches.innerHTML = "";
(fg.matches || []).forEach(m => {
const chip = el("div", "family-match");
chip.appendChild(el("span", "rank", `Region #${m.rank}`));
const swatches = el("div", "pair-swatches");
const inputSwatch = el("span", "swatch");
inputSwatch.style.background = rgbCss(m.input_rgb);
const templateSwatch = el("span", "swatch");
templateSwatch.style.background = rgbCss(m.template_rgb);
swatches.appendChild(inputSwatch);
swatches.appendChild(el("span", "pair-arrow", "→"));
swatches.appendChild(templateSwatch);
chip.appendChild(swatches);
chip.appendChild(el("span", "area-pct", `${m.area_match_pct}% area match`));
chip.appendChild(el("span", "sub-pct", `${m.input_pct}% vs ${m.template_pct}%`));
familyGridMatches.appendChild(chip);
});
}
function renderWeighted(data) {
const weights = data.method_weights || {};
const labelFor = method => (data.methods[method] && data.methods[method].label) || method;
weightedSub.textContent = "Weights — " + Object.entries(weights)
.map(([method, w]) => `${labelFor(method)} ${Math.round(w * 100)}%`)
.join(" · ");
weightedCard.innerHTML = "";
const ranked = data.weighted_scores || [];
if (data.weighted_best && ranked.length) {
const top = ranked[0];
// Input vs. matched template, side by side -- purely a display
// addition, doesn't touch which template won.
const thumbs = el("div", "weighted-thumbs");
const inputImg = el("img");
inputImg.src = uploadUrl(data.request_id, data.upload_nobg_file);
inputImg.alt = "Your upload";
thumbs.appendChild(inputImg);
thumbs.appendChild(el("span", "arrow", "→"));
const filename = templateFileByName[data.weighted_best];
if (filename) {
const templateImg = el("img");
templateImg.src = `/template_image/${encodeURIComponent(filename)}`;
templateImg.alt = data.weighted_best;
thumbs.appendChild(templateImg);
}
weightedCard.appendChild(thumbs);
const textWrap = el("div", "weighted-text");
textWrap.appendChild(el("p", "label", "Weighted final match"));
textWrap.appendChild(el("h4", null, data.weighted_best));
textWrap.appendChild(el("p", "weighted-total", `Total weighted score: ${top.weighted_score}`));
// Shape/texture are informational only here too -- reusing the
// scores already computed for their own sections, not recomputed
// and not fed back into the weighting.
const shapeBest = ((data.shape_analysis && data.shape_analysis.results) || [])
.find(r => r.template === data.weighted_best);
const textureBest = ((data.texture_analysis && data.texture_analysis.results) || [])
.find(r => r.template === data.weighted_best);
if (shapeBest || textureBest) {
const parts = [];
if (shapeBest) parts.push(`Shape ${shapeBest.match_pct}%`);
if (textureBest) parts.push(`Texture ${textureBest.match_pct}%`);
textWrap.appendChild(el("p", "weighted-supporting", parts.join(" · ")));
}
// Normalized confidence: how decisively the winner beat the
// runner-up, as a % of the winner's own score -- a display-only
// derivation from the already-final ranked list. It cannot change
// weighted_best; it only labels how strong/weak that result is.
const runnerUp = ranked.length > 1 ? ranked[1].weighted_score : 0;
const marginPct = top.weighted_score > 0
? Math.max(0, Math.min(100, Math.round(((top.weighted_score - runnerUp) / top.weighted_score) * 100)))
: 0;
const verdict = marginPct >= 50 ? "Strong match"
: marginPct >= 20 ? "Moderate match"
: "Weak match";
textWrap.appendChild(el("span", "verdict-pill", `${verdict}${marginPct}% confidence`));
weightedCard.appendChild(textWrap);
const breakdown = el("div", "weighted-breakdown");
Object.entries(top.breakdown).forEach(([method, contribution]) => {
const methodResults = (data.methods[method] && data.methods[method].results) || [];
const rawRow = methodResults.find(r => r.template === data.weighted_best);
const rawScore = rawRow ? rawRow.score : 0;
const row = el("div", "row");
row.appendChild(el("span", null, labelFor(method)));
row.appendChild(el("span", null,
`${Math.round((weights[method] || 0) * 100)}% × ${rawScore} = ${contribution}`));
breakdown.appendChild(row);
});
weightedCard.appendChild(breakdown);
} else {
weightedCard.appendChild(el("p", null, "No scores to weight yet."));
}
weightedRanked.innerHTML = "";
const maxScore = Math.max(1, ...ranked.map(r => r.weighted_score));
ranked.forEach((r, idx) => {
const row = el("div", `score-bar-row${idx === 0 ? " top" : ""}`);
row.appendChild(el("span", "name", r.template));
const track = el("div", "score-bar-track");
const fill = el("div", "score-bar-fill");
fill.style.width = `${Math.max(2, (r.weighted_score / maxScore) * 100)}%`;
track.appendChild(fill);
row.appendChild(track);
row.appendChild(el("span", "value", String(r.weighted_score)));
weightedRanked.appendChild(row);
});
}
function buildQueryTile(src, label) {
const tile = el("div", "query-tile");
const img = el("img");
img.src = src;
img.alt = label;
tile.appendChild(img);
tile.appendChild(el("div", "tile-label", label));
return tile;
}
function buildMethodCard(requestId, methodKey, m) {
const card = el("div", "method-card");
const head = el("div", "method-card-head");
head.appendChild(el("h4", null, m.label));
head.appendChild(el("span", "time-badge", `${m.time_sec.toFixed(2)}s`));
card.appendChild(head);
if (m.best) {
const bestRow = el("div", "best-row");
if (m.best_image_file) {
const img = el("img");
img.src = uploadUrl(requestId, m.best_image_file);
img.alt = m.best.template;
bestRow.appendChild(img);
}
const info = el("div");
info.appendChild(el("div", "best-name", m.best.template));
info.appendChild(el("div", "best-score",
`Score ${m.best.score} · ${m.best.confidence}% inliers`));
const pill = el("span",
`confidence-pill ${m.is_confident ? "confident" : "weak"}`,
m.is_confident ? "Confident match" : "Weak match");
info.appendChild(pill);
bestRow.appendChild(info);
card.appendChild(bestRow);
} else if (m.error) {
card.appendChild(el("p", "error-msg", `This method failed: ${m.error}`));
} else {
card.appendChild(el("p", "best-score", "No match found."));
}
const bars = el("div", "score-bars");
const maxScore = Math.max(1, ...m.results.map(r => r.score));
m.results.forEach((r, idx) => {
const row = el("div", `score-bar-row${idx === 0 ? " top" : ""}`);
row.appendChild(el("span", "name", r.template));
const track = el("div", "score-bar-track");
const fill = el("div", "score-bar-fill");
fill.style.width = `${Math.max(2, (r.score / maxScore) * 100)}%`;
track.appendChild(fill);
row.appendChild(track);
row.appendChild(el("span", "value", String(r.score)));
bars.appendChild(row);
});
card.appendChild(bars);
return card;
}
})();