Files
2025-12-25 17:41:18 +08:00

804 lines
39 KiB
HTML

<!-- START OF FILE dataset_analysis.html -->
{% extends "layout.html" %}
{% block head %}
<style>
/* 图表容器固定高度,防止抖动 */
.chart-container {
position: relative;
height: 350px;
width: 100%;
}
.chart-card-body {
padding: 1.25rem;
background-color: var(--bg-surface);
}
/* 图片画廊 Grid */
#gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
max-height: 80vh;
overflow-y: auto;
padding: 1rem;
background-color: var(--bg-body);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
}
#gallery .gallery-item {
position: relative;
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
#gallery .gallery-item:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-soft); /* 柔和边框 */
}
#gallery img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
cursor: pointer;
}
#gallery .gallery-item.hidden { display: none; }
#gallery .caption {
background: var(--bg-surface);
padding: 0.5rem;
font-size: 0.75rem;
text-align: center;
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.caption-text {
font-weight: 600;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 加载状态 */
#loading-indicator {
height: 60vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
/* 增强预览器样式 */
#aug-preview-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
#aug-preview-gallery img {
width: 100%;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.aug-preview-source-img {
width: 80px;
height: 60px;
object-fit: cover;
border: 2px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0.7;
transition: all 0.2s;
}
.aug-preview-source-img.active, .aug-preview-source-img:hover {
opacity: 1;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-soft);
}
#preview-aug-controls {
max-height: 500px;
overflow-y: auto;
padding-right: 10px;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid content-wrapper fade-in">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
<div>
<h3 class="mb-1 font-weight-bold" style="color: var(--text-main);">Dataset Analysis</h3>
<!-- 移除 text-primary,改用 text-secondary 避免太亮 -->
<p class="lead mb-0" style="font-size: 1.1rem; color: var(--text-secondary);">{{ dataset.description }}</p>
</div>
<a href="/" class="btn btn-secondary shadow-sm"><i class="bi bi-arrow-left"></i> Back to Home</a>
</div>
<!-- Loading State -->
<div id="loading-indicator">
<!-- 移除 text-primary,使用内联样式引用柔和变量 -->
<div class="spinner-border mb-3" role="status" style="width: 3rem; height: 3rem; color: var(--color-primary);"></div>
<h4 class="font-weight-normal" style="color: var(--text-main);">Analyzing Dataset...</h4>
<p class="text-muted">Crunching numbers and generating previews.</p>
</div>
<!-- Main Content -->
<div id="analysis-content" style="display: none;">
<!-- Summary Card -->
<div class="card mb-4 shadow-sm border-0">
<!-- 移除 bg-light,使用柔和背景色 -->
<div class="card-header border-0" style="background-color: var(--bg-surface-secondary);">
<strong style="color: var(--text-main);"><i class="bi bi-clipboard-data mr-2"></i>Summary & Health Check</strong>
</div>
<div class="card-body">
<div id="summary-container"></div>
<div id="consistency-check-container" class="mt-4 pt-3 border-top">
<h6 class="font-weight-bold mb-2" style="color: var(--text-main);"><i class="bi bi-robot mr-1"></i> AI Consistency Review</h6>
<p class="text-muted small mb-3">Detect potential labeling errors (e.g., mismatched classes) using embeddings.</p>
<div class="d-flex align-items-center flex-wrap">
<div class="custom-control custom-switch mr-4 mb-2">
<input type="checkbox" class="custom-control-input" id="enable-color-check" checked>
<label class="custom-control-label font-weight-bold small" for="enable-color-check" style="color: var(--text-secondary);">Strict Color Check</label>
</div>
<button class="btn btn-sm btn-primary shadow-sm mb-2" id="run-consistency-check-btn">
<i class="bi bi-search mr-1"></i> Run AI Review
</button>
</div>
</div>
</div>
</div>
<!-- Charts Row 1 -->
<div class="row">
<div class="col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<!-- 移除 bg-white -->
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Instance Counts</div>
<div class="card-body chart-card-body">
<div class="chart-container"><canvas id="class-counts-chart"></canvas></div>
</div>
</div>
</div>
<div class="col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Objects per Image</div>
<div class="card-body chart-card-body">
<div class="chart-container"><canvas id="objects-per-image-chart"></canvas></div>
</div>
</div>
</div>
<div class="col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Brightness Distribution</div>
<div class="card-body chart-card-body">
<div class="chart-container"><canvas id="brightness-chart"></canvas></div>
</div>
</div>
</div>
</div>
<!-- Charts Row 2 -->
<div class="row">
<div class="col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Aspect Ratio</div>
<div class="card-body chart-card-body">
<div class="chart-container"><canvas id="aspect-ratio-chart"></canvas></div>
</div>
</div>
</div>
<div class="col-lg-8 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Bounding Box Heatmap</div>
<div class="card-body chart-card-body">
<!-- 移除 bg-light,改为柔和背景变量 -->
<div id="bbox-heatmap-container" class="chart-container rounded border" style="background-color: var(--bg-surface-secondary);">
<canvas id="bbox-center-scatter-plot"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Augmentation Previewer -->
<div class="row">
<div class="col-12 mb-4">
<div class="card shadow-sm border-0">
<!-- 移除 bg-primary text-white,改用柔和的背景和深色文字 -->
<div class="card-header d-flex align-items-center" style="background-color: var(--color-primary-soft); color: var(--text-main); border-bottom: 1px solid var(--border-color);">
<i class="bi bi-magic mr-2" style="color: var(--color-primary);"></i> <strong>Augmentation Previewer</strong>
</div>
<div class="card-body p-0">
<div class="row no-gutters">
<!-- Left: Controls -->
<!-- 移除 bg-light -->
<div class="col-lg-4 border-right p-3" style="background-color: var(--bg-surface-secondary);">
<h6 class="font-weight-bold mb-3 border-bottom pb-2">1. Configure Parameters</h6>
<div id="preview-aug-controls" class="custom-scrollbar">
{% include '_augmentation_controls.html' %}
</div>
</div>
<!-- Right: Preview -->
<div class="col-lg-8 p-3" style="background-color: var(--bg-surface);">
<h6 class="font-weight-bold mb-3 border-bottom pb-2">2. Live Preview</h6>
<div class="mb-3">
<small class="text-muted d-block mb-2 font-weight-bold text-uppercase">Select Source Image:</small>
<div id="aug-preview-source-selector" class="d-flex flex-wrap" style="gap: 10px;"></div>
</div>
<div id="aug-preview-gallery-container" class="border rounded p-3" style="min-height: 300px; background-color: var(--bg-surface-secondary);">
<div id="aug-preview-gallery"></div>
<div id="aug-preview-loader" class="text-center mt-5" style="display: none;">
<div class="spinner-border" role="status" style="color: var(--color-primary);"></div>
<p class="mt-2 text-muted small">Generating augmented samples...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Annotator Stats -->
<div class="row">
<div class="col-lg-12 mb-4">
<div class="card shadow-sm">
<div class="card-header font-weight-bold small text-uppercase text-muted" style="background-color: var(--bg-surface);">Annotator Statistics</div>
<div class="card-body chart-card-body">
<div class="chart-container"><canvas id="annotator-chart"></canvas></div>
</div>
</div>
</div>
</div>
<!-- Image Gallery -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header border-bottom py-3 d-flex justify-content-between align-items-center flex-wrap" style="background-color: var(--bg-surface);">
<div>
<strong style="color: var(--text-main);"><i class="bi bi-images mr-2"></i>Dataset Gallery</strong>
<span class="badge badge-secondary ml-2"><span id="gallery-count">0</span> images</span>
</div>
<div class="d-flex align-items-center mt-2 mt-md-0">
<label class="mr-2 mb-0 small font-weight-bold text-muted">Filter:</label>
<select id="outlier-filter" class="form-control form-control-sm mr-2 shadow-none border-secondary" style="width: auto; max-width: 250px;">
<option value="none">Show All</option>
<option value="area_smallest">Smallest Objects (Top 10)</option>
<option value="area_largest">Largest Objects (Top 10)</option>
<option value="ar_smallest">Squarish AR (Top 10)</option>
<option value="ar_largest">Extreme AR (Top 10)</option>
<option value="iou_high">High Overlap (IoU > 0.95)</option>
<!-- 修改硬编码的粉色为 danger 变量 -->
<option value="consistency_outliers" style="display: none; color: var(--color-danger); font-weight: bold;">AI Review Outliers</option>
</select>
<button id="reset-filter-btn" class="btn btn-sm btn-outline-secondary"><i class="bi bi-x-lg"></i> Reset</button>
</div>
</div>
<div class="card-body p-0">
<div id="gallery" class="custom-scrollbar border-0 rounded-0"></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script>
$(document).ready(function() {
const datasetUuid = "{{ dataset.dataset_uuid }}";
let originalAnalysisData = null;
let fullGalleryData = [];
let samplePoolData = [];
let allBboxesData = [];
let suspiciousPairsData = [];
let imageClassMap = {};
let consistencyOutlierIndices = [];
let charts = {
classCounts: null,
objectsPerImage: null,
brightness: null,
aspectRatio: null,
centerScatter: null,
annotator: null
};
// --- 核心配色逻辑 ---
// 根据模式返回对应的柔和颜色
function getColor(type) {
const isDark = document.body.classList.contains('dark-mode');
switch (type) {
case 'primary': // 实例计数
return isDark ? 'rgba(59, 82, 107, 0.6)' : 'rgba(165, 187, 241, 0.7)';
case 'success': // 每张图片对象数
return isDark ? 'rgba(47, 111, 78, 0.6)' : 'rgba(105, 219, 177, 0.7)';
case 'warning': // 亮度分布
return isDark ? 'rgba(138, 106, 38, 0.6)' : 'rgba(255, 224, 102, 0.7)';
case 'info': // 长宽比
return isDark ? 'rgba(80, 70, 120, 0.6)' : 'rgba(200, 190, 255, 0.7)';
case 'danger': // 标注员
return isDark ? 'rgba(140, 59, 59, 0.6)' : 'rgba(255, 201, 201, 0.7)';
case 'scatter': // 散点图
return isDark ? 'rgba(129, 161, 193, 0.6)' : 'rgba(92, 124, 250, 0.6)';
default:
return isDark ? '#888' : '#ddd';
}
}
// 主题配置函数 (适配 dark/light)
function getChartThemeOptions() {
const isDarkMode = document.body.classList.contains('dark-mode');
// 使用 CSS 变量对应的值
const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)';
const textColor = isDarkMode ? '#909296' : '#868e96';
return {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: { labels: { color: textColor } },
tooltip: {
// Tooltip 背景适配:深色模式用深灰,浅色模式用白
backgroundColor: isDarkMode ? 'rgba(43, 45, 51, 0.95)' : 'rgba(255, 255, 255, 0.95)',
titleColor: isDarkMode ? '#d8dee9' : '#343a40',
bodyColor: isDarkMode ? '#d8dee9' : '#343a40',
borderColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
borderWidth: 1,
padding: 10,
cornerRadius: 8
}
},
scales: {
x: {
grid: { color: gridColor },
ticks: { color: textColor },
title: { display: true, color: textColor }
},
y: {
beginAtZero: true,
grid: { color: gridColor },
ticks: { color: textColor },
title: { display: true, color: textColor }
}
}
};
}
// 监听主题切换,实时更新图表
const themeObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const newOptions = getChartThemeOptions();
// 更新所有图表
if(charts.classCounts) {
charts.classCounts.data.datasets[0].backgroundColor = getColor('primary');
updateChartOptions(charts.classCounts, newOptions);
}
if(charts.objectsPerImage) {
charts.objectsPerImage.data.datasets[0].backgroundColor = getColor('success');
updateChartOptions(charts.objectsPerImage, newOptions);
}
if(charts.brightness) {
charts.brightness.data.datasets[0].backgroundColor = getColor('warning');
updateChartOptions(charts.brightness, newOptions);
}
if(charts.aspectRatio) {
charts.aspectRatio.data.datasets[0].backgroundColor = getColor('info');
updateChartOptions(charts.aspectRatio, newOptions);
}
if(charts.annotator) {
charts.annotator.data.datasets[0].backgroundColor = getColor('danger');
updateChartOptions(charts.annotator, newOptions);
}
if(charts.centerScatter) {
charts.centerScatter.data.datasets[0].backgroundColor = getColor('scatter');
updateChartOptions(charts.centerScatter, newOptions);
}
}
}
});
themeObserver.observe(document.body, { attributes: true });
function updateChartOptions(chart, newOptions) {
chart.options.plugins.legend.labels.color = newOptions.plugins.legend.labels.color;
chart.options.plugins.tooltip = newOptions.plugins.tooltip; // 更新 tooltip 样式
chart.options.scales.x.grid.color = newOptions.scales.x.grid.color;
chart.options.scales.x.ticks.color = newOptions.scales.x.ticks.color;
chart.options.scales.y.grid.color = newOptions.scales.y.grid.color;
chart.options.scales.y.ticks.color = newOptions.scales.y.ticks.color;
chart.update();
}
// --- Augmentation Controls Logic ---
function setupAugmentationListeners(contextSelector) {
const context = $(contextSelector);
context.on('input', 'input[type="range"]', function() {
$(this).closest('div').find('.val-display').text($(this).val());
if (contextSelector === '#preview-aug-controls') {
triggerAugmentationPreview(); // Live update
}
});
context.on('change', '.aug-option input[type="checkbox"]', function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
if (contextSelector === '#preview-aug-controls') {
triggerAugmentationPreview(); // Live update
}
});
context.find('.aug-option input[type="checkbox"]').each(function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
});
}
function getAugmentationOptions(isForPreview) {
const context = $('#preview-aug-controls');
const getVal = (selector, type = 'float') => type === 'int' ? parseInt(context.find(selector).val()) : parseFloat(context.find(selector).val());
const isEnabled = (selector) => context.find(selector).is(':checked');
let augOptions = {
enabled: true,
hflip: { enabled: isEnabled('#aug-hflip-enabled'), p: getVal('#aug-hflip-p') },
vflip: { enabled: isEnabled('#aug-vflip-enabled'), p: getVal('#aug-vflip-p') },
rotate90: { enabled: isEnabled('#aug-rotate90-enabled'), p: getVal('#aug-rotate90-p') },
rotate: { enabled: isEnabled('#aug-rotate-enabled'), p: getVal('#aug-rotate-p'), limit: getVal('#aug-rotate-limit', 'int') },
ssr: { enabled: isEnabled('#aug-ssr-enabled'), p: getVal('#aug-ssr-p'), rotate: getVal('#aug-ssr-rotate', 'int'), shift: getVal('#aug-ssr-shift'), scale: getVal('#aug-ssr-scale') },
affine: { enabled: isEnabled('#aug-affine-enabled'), p: getVal('#aug-affine-p'), shear: getVal('#aug-affine-shear', 'int') },
crop: { enabled: isEnabled('#aug-crop-enabled'), p: getVal('#aug-crop-p') },
grayscale: { enabled: isEnabled('#aug-grayscale-enabled'), p: getVal('#aug-grayscale-p') },
hsv: { enabled: isEnabled('#aug-hsv-enabled'), p: getVal('#aug-hsv-p'), h: getVal('#aug-hsv-h', 'int'), s: getVal('#aug-hsv-s', 'int'), v: getVal('#aug-hsv-v', 'int') },
bc: { enabled: isEnabled('#aug-bc-enabled'), p: getVal('#aug-bc-p'), b: getVal('#aug-bc-b'), c: getVal('#aug-bc-c') },
blur: { enabled: isEnabled('#aug-blur-enabled'), p: getVal('#aug-blur-p'), limit: getVal('#aug-blur-limit', 'int') },
noise: { enabled: isEnabled('#aug-noise-enabled'), p: getVal('#aug-noise-p'), limit: getVal('#aug-noise-limit') },
cutout: { enabled: isEnabled('#aug-cutout-enabled'), p: getVal('#aug-cutout-p'), holes: getVal('#aug-cutout-holes', 'int'), size: getVal('#aug-cutout-size', 'int') },
mosaic: { enabled: isEnabled('#aug-mosaic-enabled'), p: getVal('#aug-mosaic-p') }
};
if(!isForPreview){
augOptions.multiply_factor = 3;
}
return augOptions;
}
let previewTimeout;
function triggerAugmentationPreview() {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(() => {
const activeSource = $('#aug-preview-source-selector .active');
if (activeSource.length > 0) {
const videoUuid = activeSource.data('video-uuid');
const frameNumber = activeSource.data('frame-number');
fetchAugmentationPreviews(videoUuid, frameNumber);
}
}, 500); // 防抖
}
function fetchAugmentationPreviews(videoUuid, frameNumber) {
const gallery = $('#aug-preview-gallery');
const loader = $('#aug-preview-loader');
gallery.empty();
loader.show();
const augOptions = getAugmentationOptions(true);
const payload = {
video_uuid: videoUuid, frame_number: frameNumber,
augmentation_options: augOptions, sample_pool: samplePoolData
};
$.ajax({
url: '/api/previewAugmentations', type: 'POST', contentType: 'application/json',
data: JSON.stringify(payload),
success: function(response) {
if (response.success) {
response.previews.forEach(imgData => {
gallery.append(`<div><img src="${imgData}" alt="Augmented Preview" class="shadow-sm"></div>`);
});
} else {
gallery.html(`<div class="alert alert-danger w-100">${response.message}</div>`);
}
},
error: function() { gallery.html('<div class="alert alert-danger w-100">Server error fetching previews.</div>'); },
complete: function() { loader.hide(); }
});
}
// --- Chart Rendering Functions ---
function createHistogramData(data, bins = 10, min, max) {
if (data.length === 0) return { labels: [], counts: [] };
min = min ?? Math.min(...data);
max = max ?? Math.max(...data);
if (min === max) { min -= 0.5; max += 0.5; }
const binSize = (max - min) / bins;
const counts = Array(bins).fill(0);
const labels = [];
for (let i = 0; i < bins; i++) {
const binStart = min + i * binSize;
const binEnd = binStart + binSize;
labels.push(`${binStart.toFixed(2)}-${binEnd.toFixed(2)}`);
}
for (const value of data) {
let binIndex = Math.floor((value - min) / binSize);
binIndex = Math.max(0, Math.min(bins - 1, binIndex));
if (value === max) binIndex = bins - 1;
counts[binIndex]++;
}
return { labels, counts };
}
function renderSummaryAndWarnings(summary, warnings) {
const container = $('#summary-container');
container.empty();
if (summary) {
// 使用柔和的 Info 样式
container.append(`<div class="alert shadow-sm border-0" style="background-color: var(--color-primary-soft); color: var(--text-main);">${summary}</div>`);
}
if (warnings && warnings.length > 0) {
const warningsList = $('<div class="list-group"></div>');
warnings.forEach(warning => {
warningsList.append(`<div class="list-group-item small border-0 mb-1" style="background-color: rgba(251, 191, 36, 0.15); color: var(--text-main);"><i class="bi bi-exclamation-triangle-fill mr-2" style="color: var(--color-warning)"></i>${warning}</div>`);
});
container.append(warningsList);
}
}
function renderCenterScatterPlot(points) {
if(charts.centerScatter) charts.centerScatter.destroy();
const options = getChartThemeOptions();
$.extend(true, options.scales, {
x: { type: 'linear', position: 'bottom', min: 0, max: 1, title: { display: true, text: 'Normalized X Position' }},
y: { min: 0, max: 1, reverse: true, title: { display: true, text: 'Normalized Y Position' }}
});
options.plugins.legend.display = false;
charts.centerScatter = new Chart(document.getElementById('bbox-center-scatter-plot').getContext('2d'), {
type: 'scatter',
data: { datasets: [{ label: 'BBox Center', data: points, backgroundColor: getColor('scatter'), pointRadius: 3, pointHoverRadius: 5 }] },
options: options
});
}
function renderGallery(images) {
const gallery = $('#gallery');
gallery.empty();
$('#gallery-count').text(images.length);
images.forEach((image, index) => {
const annotatedUrl = `/media/annotated_frame/${image.video_uuid}/${image.frame}.jpg`;
const goToLabelUrl = image.task_uuid ? `/labelVideo?task_uuid=${image.task_uuid}&frame=${image.frame}` : '#';
const buttonDisabled = image.task_uuid ? '' : 'disabled';
// 使用 btn-secondary 而不是 btn-outline-primary 以减少视觉噪点
const item = $(`
<div class="gallery-item" data-index="${index}">
<img src="${annotatedUrl}" alt="Frame ${image.frame}" loading="lazy">
<div class="caption">
<span class="caption-text text-truncate w-100" title="${image.video}">${image.video}</span>
<span class="text-muted small">Frame ${image.frame}</span>
<a href="${goToLabelUrl}" class="btn btn-sm btn-secondary btn-block mt-2 ${buttonDisabled}">Go to Label</a>
</div>
</div>`);
item.find('img').on('click', () => window.open(image.original_url, '_blank'));
gallery.append(item);
});
}
function filterGalleryByClass(className) {
const classImageIndices = [];
for (const [imageIndex, classList] of Object.entries(imageClassMap)) {
if (classList.includes(className)) { classImageIndices.push(parseInt(imageIndex)); }
}
$('#outlier-filter').val('none');
$('#gallery .gallery-item').each(function() {
const itemIndex = parseInt($(this).data('index'));
$(this).toggleClass('hidden', !classImageIndices.includes(itemIndex));
});
}
function resetGalleryFilter() {
$('#outlier-filter').val('none');
$('#gallery .gallery-item').removeClass('hidden');
}
$('#outlier-filter').on('change', function() {
const filterType = $(this).val();
if (filterType === 'none') { $('#gallery .gallery-item').removeClass('hidden'); return; }
let sortedBboxes, targetImageIndices;
switch(filterType) {
case 'area_smallest': sortedBboxes = [...allBboxesData].sort((a, b) => a.area - b.area); break;
case 'area_largest': sortedBboxes = [...allBboxesData].sort((a, b) => b.area - a.area); break;
case 'ar_smallest': sortedBboxes = [...allBboxesData].sort((a, b) => Math.abs(a.aspect_ratio - 1) - Math.abs(b.aspect_ratio - 1)); break;
case 'ar_largest': sortedBboxes = [...allBboxesData].sort((a, b) => Math.abs(b.aspect_ratio - 1) - Math.abs(a.aspect_ratio - 1)); break;
case 'iou_high': targetImageIndices = [...new Set(suspiciousPairsData.map(p => p.image_index))]; break;
case 'consistency_outliers': targetImageIndices = consistencyOutlierIndices; break;
}
if (sortedBboxes) { targetImageIndices = [...new Set(sortedBboxes.slice(0, 10).map(b => b.image_index))]; }
$('#gallery .gallery-item').each(function() {
const itemIndex = parseInt($(this).data('index'));
$(this).toggleClass('hidden', !targetImageIndices.includes(itemIndex));
});
});
$('#reset-filter-btn').on('click', resetGalleryFilter);
function renderClassCounts(classCounts) {
// 使用 getColor('primary')
const chartData = { labels: Object.keys(classCounts), datasets: [{ label: '# of Instances', data: Object.values(classCounts), backgroundColor: getColor('primary') }]};
if (charts.classCounts) { charts.classCounts.destroy(); }
const options = getChartThemeOptions();
options.onHover = (event, chartElement) => { event.native.target.style.cursor = chartElement[0] ? 'pointer' : 'default'; };
options.onClick = (event, elements) => {
if (elements.length > 0) {
const clickedIndex = elements[0].index;
const className = charts.classCounts.data.labels[clickedIndex];
filterGalleryByClass(className);
} else { resetGalleryFilter(); }
};
const ctx = document.getElementById('class-counts-chart');
charts.classCounts = new Chart(ctx, { type: 'bar', data: chartData, options: options });
}
function renderObjectsPerImage(data) {
const counts = data.reduce((acc, val) => { acc[val] = (acc[val] || 0) + 1; return acc; }, {});
// 使用 getColor('success')
const chartData = { labels: Object.keys(counts), datasets: [{ label: '# of Images', data: Object.values(counts), backgroundColor: getColor('success') }] };
if(charts.objectsPerImage) charts.objectsPerImage.destroy();
const options = getChartThemeOptions();
options.scales.x.title.text = 'Number of Objects';
charts.objectsPerImage = new Chart($('#objects-per-image-chart'), { type: 'bar', data: chartData, options: options });
}
function renderBrightness(data) {
const histData = createHistogramData(data, 20, 0, 255);
// 使用 getColor('warning')
const chartData = { labels: histData.labels, datasets: [{ label: 'Image Count', data: histData.counts, backgroundColor: getColor('warning') }] };
if(charts.brightness) charts.brightness.destroy();
charts.brightness = new Chart($('#brightness-chart'), { type: 'bar', data: chartData, options: getChartThemeOptions() });
}
function renderAspectRatio(data) {
const logData = data.map(d => d > 0 ? Math.log10(d) : 0);
const minLog = logData.length > 0 ? Math.min(...logData) : 0;
const maxLog = logData.length > 0 ? Math.max(...logData) : 1;
const histData = createHistogramData(logData, 15, minLog, maxLog);
histData.labels = histData.labels.map(l => {
const range = l.split('-').map(parseFloat);
return `${Math.pow(10, range[0]).toFixed(2)}-${Math.pow(10, range[1]).toFixed(2)}`;
});
// 使用 getColor('info')
const chartData = { labels: histData.labels, datasets: [{ label: 'BBox Count', data: histData.counts, backgroundColor: getColor('info') }] };
if(charts.aspectRatio) charts.aspectRatio.destroy();
const options = getChartThemeOptions();
options.scales.x.title.text = 'Width / Height';
charts.aspectRatio = new Chart($('#aspect-ratio-chart'), { type: 'bar', data: chartData, options: options });
}
function renderAnnotatorStats(stats) {
const users = Object.keys(stats);
if (users.length === 0) { $('#annotator-chart').parent().html('<p class="text-muted text-center pt-5">No annotator data found.</p>'); return; }
const imageCounts = users.map(u => stats[u].image_count);
// 使用 getColor('danger')
const chartData = { labels: users, datasets: [{ label: '# Labeled Images', data: imageCounts, backgroundColor: getColor('danger') }] };
if(charts.annotator) charts.annotator.destroy();
charts.annotator = new Chart($('#annotator-chart'), { type: 'bar', data: chartData, options: getChartThemeOptions() });
}
// --- Main Fetch Logic ---
$.ajax({
url: `/api/datasetAnalysis/${datasetUuid}`, type: 'GET',
success: function(response) {
if (response.success) {
$('#loading-indicator').hide();
$('#analysis-content').fadeIn();
originalAnalysisData = response;
fullGalleryData = response.gallery_images;
allBboxesData = response.all_bboxes;
suspiciousPairsData = response.suspicious_pairs;
imageClassMap = response.image_class_map;
renderSummaryAndWarnings(response.summary_text, response.warnings);
renderClassCounts(response.class_counts);
renderObjectsPerImage(response.objects_per_image);
renderAspectRatio(response.aspect_ratios);
renderCenterScatterPlot(response.center_points);
if (response.brightness_levels) renderBrightness(response.brightness_levels);
if (response.annotator_stats) renderAnnotatorStats(response.annotator_stats);
renderGallery(fullGalleryData);
setupAugmentationListeners('#preview-aug-controls');
const sourceSelector = $('#aug-preview-source-selector');
const sampleImages = fullGalleryData.slice(0, 5);
samplePoolData = sampleImages.map(img => ({ video_uuid: img.video_uuid, frame_number: img.frame }));
sampleImages.forEach((img, index) => {
const thumb = $(`<img src="${img.original_url}" class="aug-preview-source-img shadow-sm" data-video-uuid="${img.video_uuid}" data-frame-number="${img.frame}">`);
if (index === 0) thumb.addClass('active');
sourceSelector.append(thumb);
});
if (sampleImages.length > 0) {
triggerAugmentationPreview();
} else {
$('#aug-preview-gallery-container').html('<p class="text-muted text-center pt-5">No images available in this dataset to generate previews.</p>');
}
sourceSelector.on('click', 'img', function() {
sourceSelector.find('img').removeClass('active');
$(this).addClass('active');
triggerAugmentationPreview();
});
} else { $('#loading-indicator').html(`<div class="alert alert-danger shadow-sm">Error loading analysis: ${response.message}</div>`); }
},
error: function() { $('#loading-indicator').html('<div class="alert alert-danger shadow-sm">Failed to fetch analysis data from the server.</div>'); }
});
// AI Consistency Check Button
$('#run-consistency-check-btn').on('click', function() {
const $btn = $(this);
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm" role="status"></span> Running...');
const isColorCheckEnabled = $('#enable-color-check').is(':checked');
$.ajax({
url: `/api/datasetAnalysis/${datasetUuid}/consistency_check`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ enable_color_check: isColorCheckEnabled }),
success: function(response) {
if (response.success) {
consistencyOutlierIndices = response.outlier_image_indices;
// 使用柔和的 success 样式
const resultMessage = $(`<div class="alert mt-3 shadow-sm" style="background-color: rgba(52, 211, 153, 0.15); color: var(--text-main); border: 1px solid var(--color-success);">${response.message}</div>`);
$('#consistency-check-container').append(resultMessage);
if (consistencyOutlierIndices.length > 0) {
$('#outlier-filter option[value="consistency_outliers"]').show();
$('#outlier-filter').val('consistency_outliers').trigger('change');
resultMessage.append(' <br><strong>Filter applied to show outliers below.</strong>');
}
$btn.hide();
} else {
alert('AI Review Failed: ' + response.message);
$btn.prop('disabled', false).html('<i class="bi bi-search mr-1"></i> Run AI Review');
}
},
error: function(xhr) {
const errorMsg = xhr.responseJSON ? xhr.responseJSON.message : "Server error.";
alert(errorMsg);
$btn.prop('disabled', false).html('<i class="bi bi-search mr-1"></i> Run AI Review');
}
});
});
});
</script>
{% endblock %}