804 lines
39 KiB
HTML
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 %} |