|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<title>ICONCLASS Model Evaluation - davanstrien/iconclass-vlm</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, |
|
|
sans-serif; |
|
|
background: white; |
|
|
padding: 20px; |
|
|
line-height: 1.5; |
|
|
color: #333; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.header { |
|
|
max-width: 800px; |
|
|
margin: 0 auto 30px; |
|
|
padding: 0; |
|
|
border-bottom: 1px solid #e5e5e5; |
|
|
padding-bottom: 20px; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 20px; |
|
|
font-weight: 600; |
|
|
color: #333; |
|
|
margin: 0 0 8px 0; |
|
|
} |
|
|
|
|
|
.subtitle { |
|
|
font-size: 14px; |
|
|
color: #666; |
|
|
margin: 0 0 12px 0; |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.subtitle a { |
|
|
color: #0066cc; |
|
|
text-decoration: none; |
|
|
} |
|
|
|
|
|
.subtitle a:hover { |
|
|
text-decoration: underline; |
|
|
} |
|
|
|
|
|
.description { |
|
|
font-size: 13px; |
|
|
color: #666; |
|
|
margin: 0 0 8px 0; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.stats { |
|
|
font-size: 11px; |
|
|
color: #999; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.gallery { |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.card { |
|
|
background: white; |
|
|
border: 1px solid #e5e5e5; |
|
|
border-radius: 2px; |
|
|
overflow: hidden; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.card img { |
|
|
width: 100%; |
|
|
height: auto; |
|
|
max-height: 500px; |
|
|
object-fit: contain; |
|
|
display: block; |
|
|
border-bottom: 1px solid #e5e5e5; |
|
|
background: #fafafa; |
|
|
} |
|
|
|
|
|
.card-content { |
|
|
padding: 15px; |
|
|
} |
|
|
|
|
|
.raw-toggle { |
|
|
font-size: 11px; |
|
|
color: #999; |
|
|
cursor: pointer; |
|
|
margin-bottom: 12px; |
|
|
user-select: none; |
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", monospace; |
|
|
} |
|
|
|
|
|
.raw-toggle:hover { |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.raw-prediction { |
|
|
display: none; |
|
|
background: #fafafa; |
|
|
padding: 8px; |
|
|
border: 1px solid #e5e5e5; |
|
|
border-radius: 2px; |
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", monospace; |
|
|
font-size: 11px; |
|
|
color: #666; |
|
|
margin-bottom: 15px; |
|
|
word-break: break-all; |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.raw-prediction.visible { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.comparison { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr; |
|
|
gap: 20px; |
|
|
margin-top: 0; |
|
|
} |
|
|
|
|
|
.column { |
|
|
font-size: 13px; |
|
|
} |
|
|
|
|
|
.column-title { |
|
|
font-weight: 400; |
|
|
margin-bottom: 8px; |
|
|
color: #999; |
|
|
font-size: 10px; |
|
|
letter-spacing: 0.5px; |
|
|
text-transform: uppercase; |
|
|
} |
|
|
|
|
|
.label { |
|
|
padding: 3px 0 3px 6px; |
|
|
margin: 2px 0; |
|
|
position: relative; |
|
|
border-left: 2px solid transparent; |
|
|
font-size: 13px; |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.label:hover { |
|
|
background: #fafafa; |
|
|
} |
|
|
|
|
|
|
|
|
.prediction { |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.prediction.invalid { |
|
|
color: #999; |
|
|
text-decoration: line-through; |
|
|
} |
|
|
|
|
|
.prediction.match { |
|
|
border-left-color: #4caf50; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
|
|
|
.ground-truth { |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.ground-truth.matched { |
|
|
border-left-color: #4caf50; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
|
|
|
.match-stats { |
|
|
font-size: 11px; |
|
|
color: #999; |
|
|
margin-top: 10px; |
|
|
padding-top: 10px; |
|
|
border-top: 1px solid #f0f0f0; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
text-align: center; |
|
|
margin: 30px 0; |
|
|
padding-top: 20px; |
|
|
} |
|
|
|
|
|
.load-more { |
|
|
color: #0066cc; |
|
|
text-decoration: none; |
|
|
font-size: 13px; |
|
|
cursor: pointer; |
|
|
background: none; |
|
|
border: none; |
|
|
padding: 0; |
|
|
font-family: inherit; |
|
|
} |
|
|
|
|
|
.load-more:hover { |
|
|
text-decoration: underline; |
|
|
} |
|
|
|
|
|
.load-more:disabled { |
|
|
color: #999; |
|
|
cursor: default; |
|
|
text-decoration: none; |
|
|
} |
|
|
|
|
|
.loading { |
|
|
text-align: center; |
|
|
padding: 10px; |
|
|
color: #999; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.loading.hidden { |
|
|
display: none; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header"> |
|
|
<h1>ICONCLASS Model Evaluation</h1> |
|
|
<div class="subtitle"> |
|
|
Comparing predictions from <a href="https://huggingface.co/davanstrien/iconclass-vlm" target="_blank">davanstrien/iconclass-vlm</a> against ground truth labels |
|
|
</div> |
|
|
<div class="description"> |
|
|
A vision-language model fine-tuned on Qwen2.5-VL-3B for classifying art and cultural heritage images using ICONCLASS notation — a hierarchical classification system for art and iconography. |
|
|
</div> |
|
|
<div class="stats"> |
|
|
Showing <span id="loadedCount">0</span> of <span id="totalCount">-</span> test images |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="gallery" class="gallery"></div> |
|
|
|
|
|
<div class="loading hidden" id="loading">Loading...</div> |
|
|
|
|
|
<div class="controls"> |
|
|
<button id="loadMore" class="load-more">Load more images</button> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const DATASET = "davanstrien/iconclass-sft-predictions"; |
|
|
const CONFIG = "default"; |
|
|
const SPLIT = "test"; |
|
|
const PAGE_SIZE = 10; |
|
|
|
|
|
|
|
|
let currentOffset = 0; |
|
|
let totalRows = null; |
|
|
let isLoading = false; |
|
|
|
|
|
async function loadDatasetPage() { |
|
|
if (isLoading) return; |
|
|
|
|
|
isLoading = true; |
|
|
const loadingDiv = document.getElementById("loading"); |
|
|
loadingDiv.classList.remove("hidden"); |
|
|
|
|
|
try { |
|
|
const response = await fetch( |
|
|
`https://datasets-server.huggingface.co/rows?dataset=${DATASET}&config=${CONFIG}&split=${SPLIT}&offset=${currentOffset}&length=${PAGE_SIZE}` |
|
|
); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
|
|
|
if (data.num_rows_total) { |
|
|
totalRows = data.num_rows_total; |
|
|
document.getElementById("totalCount").textContent = totalRows; |
|
|
} |
|
|
|
|
|
|
|
|
displayRows(data.rows); |
|
|
|
|
|
|
|
|
currentOffset += data.rows.length; |
|
|
document.getElementById("loadedCount").textContent = currentOffset; |
|
|
|
|
|
|
|
|
const loadMoreBtn = document.getElementById("loadMore"); |
|
|
if (currentOffset >= totalRows) { |
|
|
loadMoreBtn.disabled = true; |
|
|
loadMoreBtn.textContent = "All Images Loaded"; |
|
|
} else { |
|
|
loadMoreBtn.textContent = `Load more images (${ |
|
|
totalRows - currentOffset |
|
|
} remaining)`; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Error:", error); |
|
|
} finally { |
|
|
isLoading = false; |
|
|
loadingDiv.classList.add("hidden"); |
|
|
} |
|
|
} |
|
|
|
|
|
function displayRows(rows) { |
|
|
const gallery = document.getElementById("gallery"); |
|
|
|
|
|
rows.forEach((item) => { |
|
|
const row = item.row; |
|
|
|
|
|
|
|
|
const card = document.createElement("div"); |
|
|
card.className = "card"; |
|
|
|
|
|
|
|
|
if (row.images && row.images.length > 0) { |
|
|
const img = document.createElement("img"); |
|
|
img.src = row.images[0].src; |
|
|
img.loading = "lazy"; |
|
|
card.appendChild(img); |
|
|
} |
|
|
|
|
|
|
|
|
const content = document.createElement("div"); |
|
|
content.className = "card-content"; |
|
|
|
|
|
|
|
|
if (row["iconclass-prediction"]) { |
|
|
const toggleDiv = document.createElement("div"); |
|
|
toggleDiv.className = "raw-toggle"; |
|
|
toggleDiv.textContent = "+ Show raw prediction"; |
|
|
|
|
|
const rawDiv = document.createElement("div"); |
|
|
rawDiv.className = "raw-prediction"; |
|
|
rawDiv.textContent = row["iconclass-prediction"]; |
|
|
|
|
|
toggleDiv.addEventListener("click", () => { |
|
|
if (rawDiv.classList.contains("visible")) { |
|
|
rawDiv.classList.remove("visible"); |
|
|
toggleDiv.textContent = "+ Show raw prediction"; |
|
|
} else { |
|
|
rawDiv.classList.add("visible"); |
|
|
toggleDiv.textContent = "− Hide raw prediction"; |
|
|
} |
|
|
}); |
|
|
|
|
|
content.appendChild(toggleDiv); |
|
|
content.appendChild(rawDiv); |
|
|
} |
|
|
|
|
|
|
|
|
const predictions = row["iconclass-predictions-parsed"] || []; |
|
|
const groundTruth = row["iconclass-gt-parsed"] || []; |
|
|
|
|
|
|
|
|
const invalidPredictions = predictions.map((pred) => { |
|
|
|
|
|
return ( |
|
|
pred.toLowerCase().includes("not a valid") || |
|
|
pred.toLowerCase().includes("invalid") |
|
|
); |
|
|
}); |
|
|
|
|
|
|
|
|
const matches = predictions.filter((pred) => |
|
|
groundTruth.some( |
|
|
(gt) => |
|
|
gt.toLowerCase().includes(pred.toLowerCase()) || |
|
|
pred.toLowerCase().includes(gt.toLowerCase()) |
|
|
) |
|
|
); |
|
|
|
|
|
|
|
|
const comparison = document.createElement("div"); |
|
|
comparison.className = "comparison"; |
|
|
|
|
|
|
|
|
const predColumn = document.createElement("div"); |
|
|
predColumn.className = "column"; |
|
|
|
|
|
const predTitle = document.createElement("div"); |
|
|
predTitle.className = "column-title"; |
|
|
predTitle.textContent = "Predictions"; |
|
|
predColumn.appendChild(predTitle); |
|
|
|
|
|
predictions.forEach((pred, idx) => { |
|
|
const label = document.createElement("div"); |
|
|
const isInvalid = invalidPredictions[idx]; |
|
|
const isMatch = matches.includes(pred); |
|
|
|
|
|
label.className = `label prediction ${isInvalid ? "invalid" : ""} ${ |
|
|
isMatch && !isInvalid ? "match" : "" |
|
|
}`; |
|
|
label.textContent = pred; |
|
|
predColumn.appendChild(label); |
|
|
}); |
|
|
|
|
|
|
|
|
const gtColumn = document.createElement("div"); |
|
|
gtColumn.className = "column"; |
|
|
|
|
|
const gtTitle = document.createElement("div"); |
|
|
gtTitle.className = "column-title"; |
|
|
gtTitle.textContent = "Ground Truth"; |
|
|
gtColumn.appendChild(gtTitle); |
|
|
|
|
|
groundTruth.forEach((gt) => { |
|
|
const label = document.createElement("div"); |
|
|
const isMatched = matches.some( |
|
|
(pred) => |
|
|
gt.toLowerCase().includes(pred.toLowerCase()) || |
|
|
pred.toLowerCase().includes(gt.toLowerCase()) |
|
|
); |
|
|
label.className = `label ground-truth ${ |
|
|
isMatched ? "matched" : "" |
|
|
}`; |
|
|
label.textContent = gt; |
|
|
gtColumn.appendChild(label); |
|
|
}); |
|
|
|
|
|
comparison.appendChild(predColumn); |
|
|
comparison.appendChild(gtColumn); |
|
|
content.appendChild(comparison); |
|
|
|
|
|
|
|
|
const validPredictions = predictions.filter( |
|
|
(_, idx) => !invalidPredictions[idx] |
|
|
); |
|
|
const matchScore = |
|
|
validPredictions.length > 0 |
|
|
? Math.round((matches.length / validPredictions.length) * 100) |
|
|
: 0; |
|
|
|
|
|
const statsDiv = document.createElement("div"); |
|
|
statsDiv.className = "match-stats"; |
|
|
statsDiv.textContent = validPredictions.length > 0 |
|
|
? `${matches.length}/${validPredictions.length} matches` |
|
|
: `No valid predictions`; |
|
|
content.appendChild(statsDiv); |
|
|
|
|
|
card.appendChild(content); |
|
|
gallery.appendChild(card); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document |
|
|
.getElementById("loadMore") |
|
|
.addEventListener("click", loadDatasetPage); |
|
|
|
|
|
|
|
|
window.addEventListener("scroll", () => { |
|
|
if ( |
|
|
window.innerHeight + window.scrollY >= |
|
|
document.body.offsetHeight - 100 |
|
|
) { |
|
|
if (!isLoading && currentOffset < totalRows) { |
|
|
loadDatasetPage(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
loadDatasetPage(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|