yolo数据集标记

This commit is contained in:
2025-12-25 17:41:18 +08:00
parent 1a135cdda7
commit a0555ac6b5
22 changed files with 10194 additions and 1 deletions
@@ -0,0 +1,359 @@
<!-- START OF FILE _augmentation_controls.html -->
<div class="accordion shadow-sm" id="aug-accordion">
<!-- 1. Geometric Transforms -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0" id="headingGeo">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-geometric">
<i class="bi bi-arrows-move mr-2 text-primary"></i> Geometric Transforms
</button>
</div>
<div id="collapse-geometric" class="collapse show" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row">
<!-- HorizontalFlip -->
<div class="col-md-6 mb-3 aug-option" data-controls="hflip-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-hflip-enabled" checked>
<label class="custom-control-label font-weight-bold text-dark" for="aug-hflip-enabled">Horizontal Flip</label>
</div>
<div id="hflip-controls" class="pl-4 mt-2">
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="aug-hflip-p" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
<!-- VerticalFlip -->
<div class="col-md-6 mb-3 aug-option" data-controls="vflip-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-vflip-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-vflip-enabled">Vertical Flip</label>
</div>
<div id="vflip-controls" class="pl-4 mt-2">
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="aug-vflip-p" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
<!-- Rotate90 -->
<div class="col-md-6 mb-3 aug-option" data-controls="rotate90-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-rotate90-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-rotate90-enabled">Random Rotate 90°</label>
</div>
<div id="rotate90-controls" class="pl-4 mt-2">
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="aug-rotate90-p" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
<!-- Rotate -->
<div class="col-md-6 mb-3 aug-option" data-controls="rotate-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-rotate-enabled" checked>
<label class="custom-control-label font-weight-bold text-dark" for="aug-rotate-enabled">Fine Rotation</label>
</div>
<div id="rotate-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-rotate-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Limit: ±<span class="val-display text-primary">30</span>°</label>
<input type="range" class="custom-range" id="aug-rotate-limit" min="0" max="90" step="5" value="30">
</div>
</div>
</div>
</div>
<!-- ShiftScaleRotate -->
<div class="col-12 mb-3 aug-option" data-controls="ssr-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-ssr-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-ssr-enabled">Shift / Scale / Rotate (SSR)</label>
</div>
<div id="ssr-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-ssr-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Rot: ±<span class="val-display text-primary">25</span>°</label>
<input type="range" class="custom-range" id="aug-ssr-rotate" min="0" max="90" step="5" value="25">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Shift: <span class="val-display text-primary">0.06</span></label>
<input type="range" class="custom-range" id="aug-ssr-shift" min="0" max="0.5" step="0.01" value="0.06">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Scale: <span class="val-display text-primary">0.1</span></label>
<input type="range" class="custom-range" id="aug-ssr-scale" min="0" max="0.9" step="0.05" value="0.1">
</div>
</div>
</div>
</div>
<!-- Affine -->
<div class="col-12 aug-option" data-controls="affine-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-affine-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-affine-enabled">Affine (Shear)</label>
</div>
<div id="affine-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-affine-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Shear: ±<span class="val-display text-primary">12</span>°</label>
<input type="range" class="custom-range" id="aug-affine-shear" min="0" max="45" step="1" value="12">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Crop & Pad -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 collapsed d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-crop">
<i class="bi bi-crop mr-2 text-info"></i> Crop & Pad
</button>
</div>
<div id="collapse-crop" class="collapse" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row aug-option" data-controls="crop-controls">
<div class="col-12">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-crop-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-crop-enabled">Random Sized BBox Safe Crop</label>
</div>
</div>
<div class="col-12 pl-4 mt-2" id="crop-controls">
<p class="text-muted small mb-2"><i class="bi bi-info-circle mr-1"></i> Crops a random part of the image ensuring objects are kept, then resizes to training size.</p>
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="aug-crop-p" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
</div>
</div>
</div>
<!-- 3. Color & Lighting -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 collapsed d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-color">
<i class="bi bi-palette mr-2 text-warning"></i> Color & Lighting
</button>
</div>
<div id="collapse-color" class="collapse" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row">
<!-- Grayscale -->
<div class="col-md-6 mb-3 aug-option" data-controls="grayscale-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-grayscale-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-grayscale-enabled">To Gray</label>
</div>
<div id="grayscale-controls" class="pl-4 mt-2">
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.1</span>
</label>
<input type="range" class="custom-range" id="aug-grayscale-p" min="0" max="1" step="0.1" value="0.1">
</div>
</div>
<!-- HSV -->
<div class="col-12 mb-3 aug-option" data-controls="hsv-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-hsv-enabled" checked>
<label class="custom-control-label font-weight-bold text-dark" for="aug-hsv-enabled">Hue Saturation Value</label>
</div>
<div id="hsv-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-hsv-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Hue: ±<span class="val-display text-primary">20</span></label>
<input type="range" class="custom-range" id="aug-hsv-h" min="0" max="100" step="5" value="20">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Sat: ±<span class="val-display text-primary">30</span></label>
<input type="range" class="custom-range" id="aug-hsv-s" min="0" max="100" step="5" value="30">
</div>
<div class="col-md-3 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Val: ±<span class="val-display text-primary">20</span></label>
<input type="range" class="custom-range" id="aug-hsv-v" min="0" max="100" step="5" value="20">
</div>
</div>
</div>
</div>
<!-- Brightness Contrast -->
<div class="col-12 aug-option" data-controls="bc-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-bc-enabled" checked>
<label class="custom-control-label font-weight-bold text-dark" for="aug-bc-enabled">Brightness & Contrast</label>
</div>
<div id="bc-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-md-4 col-12 mb-2">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-bc-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-md-4 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Bright: ±<span class="val-display text-primary">0.2</span></label>
<input type="range" class="custom-range" id="aug-bc-b" min="0" max="1" step="0.05" value="0.2">
</div>
<div class="col-md-4 col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Contrast: ±<span class="val-display text-primary">0.2</span></label>
<input type="range" class="custom-range" id="aug-bc-c" min="0" max="1" step="0.05" value="0.2">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 4. Blur & Noise -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 collapsed d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-blur">
<i class="bi bi-droplet-half mr-2 text-secondary"></i> Blur & Noise
</button>
</div>
<div id="collapse-blur" class="collapse" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row">
<!-- Blur -->
<div class="col-12 mb-3 aug-option" data-controls="blur-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-blur-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-blur-enabled">Gaussian Blur</label>
</div>
<div id="blur-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.3</span></label>
<input type="range" class="custom-range" id="aug-blur-p" min="0" max="1" step="0.1" value="0.3">
</div>
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Limit: <span class="val-display text-primary">7</span>px</label>
<input type="range" class="custom-range" id="aug-blur-limit" min="3" max="21" step="2" value="7">
</div>
</div>
</div>
</div>
<!-- Noise -->
<div class="col-12 aug-option" data-controls="noise-controls">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-noise-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-noise-enabled">Gaussian Noise</label>
</div>
<div id="noise-controls" class="pl-4 mt-2">
<div class="row">
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.3</span></label>
<input type="range" class="custom-range" id="aug-noise-p" min="0" max="1" step="0.1" value="0.3">
</div>
<div class="col-6">
<label class="small text-muted font-weight-bold mb-1">Var Limit: <span class="val-display text-primary">50</span></label>
<input type="range" class="custom-range" id="aug-noise-limit" min="10" max="250" step="10" value="50">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 5. Dropout & Cutout -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 collapsed d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-dropout">
<i class="bi bi-grid-3x3 mr-2 text-danger"></i> Dropout (Cutout)
</button>
</div>
<div id="collapse-dropout" class="collapse" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row aug-option" data-controls="cutout-controls">
<div class="col-12">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-cutout-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-cutout-enabled">CoarseDropout</label>
</div>
</div>
<div class="col-12 pl-4 mt-2" id="cutout-controls">
<div class="row">
<div class="col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Prob: <span class="val-display text-primary">0.5</span></label>
<input type="range" class="custom-range" id="aug-cutout-p" min="0" max="1" step="0.1" value="0.5">
</div>
<div class="col-6 mb-2">
<label class="small text-muted font-weight-bold mb-1">Holes: <span class="val-display text-primary">1</span></label>
<input type="range" class="custom-range" id="aug-cutout-holes" min="1" max="16" step="1" value="1">
</div>
<div class="col-12">
<label class="small text-muted font-weight-bold mb-1">Size: <span class="val-display text-primary">64</span>px</label>
<input type="range" class="custom-range" id="aug-cutout-size" min="8" max="128" step="8" value="64">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 6. Advanced -->
<div class="card border-0 mb-1">
<div class="card-header bg-white p-0">
<button class="btn btn-block text-left font-weight-bold text-dark py-3 px-4 collapsed d-flex align-items-center" type="button" data-toggle="collapse" data-target="#collapse-advanced">
<i class="bi bi-puzzle mr-2 text-success"></i> Advanced
</button>
</div>
<div id="collapse-advanced" class="collapse" data-parent="#aug-accordion">
<div class="card-body bg-light pt-3 pb-3 px-4">
<div class="row aug-option" data-controls="mosaic-controls">
<div class="col-12">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-mosaic-enabled">
<label class="custom-control-label font-weight-bold text-dark" for="aug-mosaic-enabled">Mosaic</label>
</div>
<div id="mosaic-controls" class="pl-4 mt-2">
<label class="d-flex justify-content-between small text-muted font-weight-bold mb-1">
<span>Probability</span>
<span class="val-display text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="aug-mosaic-p" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
{% macro render_modal(id, title) %}
<div class="modal fade" id="{{ id }}" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ caller() }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endmacro %}
@@ -0,0 +1,804 @@
<!-- 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 %}
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>FTC-ML Local</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modern.css') }}">
{% block head %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light bg-transparent pt-3 pb-3 px-4">
<a class="navbar-brand d-flex align-items-center" href="/">
<div class="d-flex align-items-center justify-content-center bg-primary text-white rounded-circle mr-2 shadow-sm" style="width: 32px; height: 32px;">
<i class="bi bi-box-seam" style="font-size: 0.9rem;"></i>
</div>
<span class="font-weight-bold" style="font-size: 1.1rem; letter-spacing: -0.01em;">Zero 2 YOLO Yard</span>
</a>
<button class="navbar-toggler border-0" type="button" data-toggle="collapse" data-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav mr-auto"></ul>
<div class="d-flex align-items-center">
<span class="small mr-3 font-weight-500 text-muted d-none d-md-block" style="opacity: 0.7;">
Produced By BlueDarkUP -- Based On FIRST ML Toolchain --
</span>
<button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle Theme">
<i class="bi bi-moon-fill" id="theme-toggle-icon"></i>
</button>
</div>
</div>
</nav>
<main role="main" class="flex-grow-1">
{% block content %}{% endblock %}
</main>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function() {
const themeToggleBtn = document.getElementById('theme-toggle-btn');
const themeToggleIcon = document.getElementById('theme-toggle-icon');
const body = document.body;
const applyTheme = (isDark) => {
if (isDark) {
body.classList.add('dark-mode');
themeToggleIcon.className = 'bi bi-sun-fill';
} else {
body.classList.remove('dark-mode');
themeToggleIcon.className = 'bi bi-moon-fill';
}
};
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) applyTheme(true);
else applyTheme(false);
themeToggleBtn.addEventListener('click', () => {
const isDark = body.classList.contains('dark-mode');
applyTheme(!isDark);
localStorage.setItem('theme', !isDark ? 'dark' : 'light');
});
})();
</script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+955
View File
@@ -0,0 +1,955 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_modal %}
{% block content %}
<div class="container-fluid px-4 px-lg-5 content-wrapper fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end page-header">
<div>
<h1 class="page-title mb-0">Dashboard</h1>
<p class="page-subtitle">Manage your computer vision pipeline.</p>
</div>
<ul class="nav nav-pills-fluent mt-3 mt-md-0" id="mainTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="videos-tab" data-toggle="tab" href="#videos" role="tab">
Videos
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="datasets-tab" data-toggle="tab" href="#datasets" role="tab">
Datasets
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="models-tab" data-toggle="tab" href="#models" role="tab">
Models
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab">
Settings
</a>
</li>
</ul>
</div>
<div class="tab-content" id="mainTabContent">
<div class="tab-pane fade show active" id="videos" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#uploadVideoModal">
<i class="bi bi-plus-lg mr-2"></i>New Video
</button>
</div>
<input type="file" id="frame-import-input" webkitdirectory directory multiple style="display: none;" />
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th style="width: 25%">Description</th>
<th>Filename</th>
<th>Status</th>
<th>Progress</th>
<th>Labels</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="video-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="datasets" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#createDatasetModal">
<i class="bi bi-plus-lg mr-2"></i>Create Dataset
</button>
</div>
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th style="width: 25%">Name</th>
<th>Status</th>
<th>Composition</th>
<th>Classes</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="dataset-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="models" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#importModelModal">
<i class="bi bi-upload mr-2"></i>Import Model
</button>
</div>
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th>Description</th>
<th>Type</th>
<th>Label Map</th>
<th>Date</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="model-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings" role="tabpanel">
<form id="settings-form">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-9">
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-cpu mr-2 text-primary"></i> AI Engine & Hardware
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Computation Device</label>
<select id="setting-gpu-device" class="form-control custom-select">
<option value="auto">Auto-detect</option>
<option value="cpu">CPU Only</option>
<option value="cuda:0">NVIDIA GPU #0</option>
<option value="cuda:1">NVIDIA GPU #1</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Assistance Model (SAM)</label>
<select id="setting-sam-model" class="form-control custom-select">
<option value="sam2.1_t.pt" data-name="SAM 2.1 Tiny">SAM 2.1 Tiny (Fast)</option>
<option value="sam2.1_s.pt" data-name="SAM 2.1 Small">SAM 2.1 Small (Balanced)</option>
<option value="sam2.1_b.pt" data-name="SAM 2.1 Base">SAM 2.1 Base</option>
<option value="sam2.1_l.pt" data-name="SAM 2.1 Large">SAM 2.1 Large (Precise)</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Feature Extractor</label>
<select id="setting-feature-extractor-model" class="form-control custom-select">
<option value="mobilenet_v3_large">MobileNetV3-Large</option>
<option value="mobilenet_v3_small">MobileNetV3-Small</option>
</select>
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-sliders mr-2 text-primary"></i> Algorithm Parameters
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
SAM Mask Conf. <span id="sam-mask-conf-val" class="text-primary">0.35</span>
</label>
<input type="range" class="custom-range" id="setting-sam-mask-confidence" min="0.1" max="0.9" step="0.05">
</div>
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
NMS IoU <span id="nms-iou-val" class="text-primary">0.70</span>
</label>
<input type="range" class="custom-range" id="setting-nms-iou-threshold" min="0.1" max="1.0" step="0.05">
</div>
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
Proto. Temp <span id="prototype-temp-val" class="text-primary">0.07</span>
</label>
<input type="range" class="custom-range" id="setting-prototype-temperature" min="0.01" max="0.5" step="0.01">
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-ui-checks mr-2 text-primary"></i> Workflow
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Default Tool</label>
<select id="setting-default-annotation-mode" class="form-control custom-select">
<option value="manual">Manual Draw</option>
<option value="sam">SAM (Point)</option>
<option value="lam">LAM (Click to Label)</option>
<option value="smart_select">Smart Select</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Auto-Save</label>
<div class="custom-control custom-switch mt-2">
<input type="checkbox" class="custom-control-input" id="setting-autosave-enabled">
<label class="custom-control-label" for="setting-autosave-enabled">Save on navigation</label>
</div>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">JPEG Quality</label>
<input type="number" id="setting-jpeg-quality" class="form-control" min="10" max="100" step="5">
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Legacy Tracker</label>
<select id="setting-opencv-tracker" class="form-control custom-select">
{% for tracker in tracker_fns %}<option value="{{ tracker }}">{{ tracker }}</option>{% endfor %}
</select>
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-palette mr-2 text-primary"></i> Class Colors
</div>
<div id="class-color-manager" class="d-flex flex-wrap custom-scrollbar" style="max-height: 200px; overflow-y: auto; gap: 10px;"></div>
</div>
<div class="fluent-card settings-section" style="border-left: 4px solid var(--color-warning);">
<div class="settings-section-title text-warning">
<i class="bi bi-tools mr-2"></i> Maintenance
</div>
<div class="d-flex align-items-center justify-content-between">
<div class="form-group mb-0 mr-3 flex-grow-1">
<label class="small text-muted font-weight-bold">Cache Save Interval (s)</label>
<input type="number" class="form-control" id="setting-cache-save-interval" min="10" max="300">
</div>
<button type="button" class="btn btn-outline-warning" id="clear-cache-btn">
<i class="bi bi-eraser-fill mr-1"></i> Clear Cache
</button>
</div>
</div>
<div class="text-right pb-5">
<button type="submit" class="btn btn-primary px-4 shadow">
Apply Settings
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% call render_modal('uploadVideoModal', 'Upload New Video') %}
<form id="upload-video-form">
<div class="form-group">
<label class="font-weight-600">Description</label>
<input type="text" class="form-control" id="video-description" name="description" placeholder="E.g., Field Test Match 1" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Video File (.mp4)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="video-file" name="video_file" accept="video/mp4" required>
<label class="custom-file-label" for="video-file">Choose file...</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Upload & Process</button>
</form>
{% endcall %}
{% call render_modal('createDatasetModal', 'Create Dataset') %}
<form id="create-dataset-form">
<div class="form-group">
<label class="font-weight-600">Name</label>
<input type="text" class="form-control" id="dataset-description" placeholder="E.g., Season 2024 V1" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Videos</label>
<select multiple class="form-control custom-select" id="dataset-videos" required size="5"></select>
<small class="text-muted">Ctrl/Cmd + Click to select multiple.</small>
</div>
<div class="form-row">
<div class="form-group col-6">
<label class="font-weight-600">Valid %</label>
<input type="number" class="form-control" id="eval-percent" value="20">
</div>
<div class="form-group col-6">
<label class="font-weight-600">Test %</label>
<input type="number" class="form-control" id="test-percent" value="10">
</div>
</div>
<div class="bg-light p-3 rounded mb-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 font-weight-bold">Augmentation</h6>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-enabled">
<label class="custom-control-label" for="aug-enabled"></label>
</div>
</div>
<div id="augmentation-options-panel" class="mt-3" style="display: none;">
<div class="form-group">
<label class="small text-muted font-weight-bold">Copies per Image</label>
<input type="number" class="form-control" id="aug-multiply-factor" value="3">
</div>
{% include '_augmentation_controls.html' %}
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Create Dataset</button>
</form>
{% endcall %}
{% call render_modal('importModelModal', 'Import Model') %}
<form id="import-model-form">
<div class="form-group">
<label class="font-weight-600">Description</label>
<input type="text" class="form-control" id="model-description" name="description" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Model (.tflite)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="model-file" name="model_file" accept=".tflite" required>
<label class="custom-file-label" for="model-file">Select file...</label>
</div>
</div>
<div class="form-group">
<label class="font-weight-600">Label Map (.txt)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="label-file" name="label_file" accept=".txt,.labels" required>
<label class="custom-file-label" for="label-file">Select file...</label>
</div>
</div>
<div class="form-group">
<label class="font-weight-600">Type</label>
<select class="form-control custom-select" id="model-type" name="model_type" required>
<option value="float32">Float32 (Standard)</option>
<option value="uint8">UINT8 (Edge TPU)</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-block">Import</button>
</form>
{% endcall %}
{% call render_modal('preAnnotateModal', 'Auto-Label') %}
<form id="pre-annotate-form">
<input type="hidden" id="pre-annotate-video-uuid" data-frame-count="0" data-labeled-count="0">
<div class="form-group">
<label class="font-weight-600">Model</label>
<select id="pre-annotate-model-select" class="form-control custom-select" required></select>
</div>
<div class="form-row">
<div class="form-group col-6">
<label class="font-weight-600">Start</label>
<input type="number" id="pre-annotate-start-frame" class="form-control">
</div>
<div class="form-group col-6">
<label class="font-weight-600">End</label>
<input type="number" id="pre-annotate-end-frame" class="form-control">
</div>
</div>
<div class="form-group">
<label class="d-flex justify-content-between font-weight-600">
Conf. <span id="confidence-value" class="text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="pre-annotate-confidence" min="0.1" max="0.95" step="0.05" value="0.5">
</div>
<div class="form-group">
<label class="font-weight-600 d-block mb-2">Strategy</label>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="strategy-overwrite" name="merge_strategy" value="overwrite" class="custom-control-input" checked>
<label class="custom-control-label" for="strategy-overwrite">Overwrite</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="strategy-skip" name="merge_strategy" value="skip_labeled" class="custom-control-input">
<label class="custom-control-label" for="strategy-skip">Fill Gaps</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Start</button>
</form>
{% endcall %}
<div class="modal fade" id="manageTasksModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content border-0 shadow-lg" style="border-radius: var(--radius-lg);">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title font-weight-bold" id="manageTasksModalTitle">Tasks</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body pt-3">
<div class="table-responsive rounded bg-light mb-4 border-0">
<table class="table table-sm table-hover mb-0 bg-transparent">
<tbody id="task-list-in-modal"></tbody>
</table>
</div>
<h6 class="font-weight-bold mb-3">New Task</h6>
<form id="create-task-form">
<input type="hidden" id="task-video-uuid">
<div class="form-row">
<div class="form-group col-md-4">
<input type="text" class="form-control form-control-sm" id="task-assigned-to" placeholder="User" required>
</div>
<div class="form-group col-md-3">
<input type="number" class="form-control form-control-sm" id="task-start-frame" placeholder="Start" required>
</div>
<div class="form-group col-md-3">
<input type="number" class="form-control form-control-sm" id="task-end-frame" placeholder="End" required>
</div>
<div class="form-group col-md-2">
<button type="submit" class="btn btn-sm btn-success btn-block">Add</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
let currentSettings = {};
$('.custom-file-input').on('change', function() {
let fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').addClass("selected").html(fileName);
});
function getStatusBadge(status, message) {
let badgeClass = 'secondary';
let icon = '';
if (status === 'READY') {
badgeClass = 'warning';
if(!message) badgeClass = 'success';
icon = '<i class="bi bi-check-circle-fill mr-1"></i>';
} else if (['PROCESSING', 'EXTRACTING', 'PENDING', 'PRE_ANNOTATING', 'APPLYING_PROTOTYPES'].includes(status)) {
badgeClass = 'info';
icon = '<span class="spinner-border spinner-border-sm mr-1"></span>';
} else if (status === 'CANCELLING') {
badgeClass = 'warning';
icon = '<i class="bi bi-x-circle mr-1"></i>';
} else if (status === 'FAILED') {
badgeClass = 'danger';
icon = '<i class="bi bi-exclamation-triangle-fill mr-1"></i>';
}
return `<span class="badge badge-${badgeClass} d-inline-flex align-items-center">${icon} ${status}</span>`;
}
function showSuccess(message) { alert(message); }
function showError(message) { alert('Error: ' + message); }
function refreshVideos() {
$.get('/listVideos', function(data) {
const videoList = $('#video-list');
videoList.empty();
data.all_videos.forEach(video => {
const isReady = video.status === 'READY';
let actions = '';
if (isReady) {
actions += `<button class="btn btn-sm btn-info btn-import-frames mr-1" data-uuid="${video.video_uuid}" title="Import frames"><i class="bi bi-images"></i></button>`;
actions += `<button class="btn btn-sm btn-warning btn-pre-annotate mr-1" data-uuid="${video.video_uuid}" data-frame-count="${video.frame_count}" data-labeled-count="${video.labeled_frame_count}" title="Auto-Label"><i class="bi bi-robot"></i></button>`;
actions += `<button class="btn btn-sm btn-success btn-manage-tasks mr-1" data-uuid="${video.video_uuid}" data-description="${video.description}" data-framecount="${video.frame_count}" title="Tasks"><i class="bi bi-card-checklist"></i></button>`;
} else {
actions += `<button class="btn btn-sm btn-secondary mr-1" disabled><i class="bi bi-images"></i></button>`;
}
if (['PRE_ANNOTATING', 'APPLYING_PROTOTYPES'].includes(video.status)) {
actions += `<button class="btn btn-sm btn-danger btn-cancel-task mr-1" data-uuid="${video.video_uuid}" title="Cancel Task"><i class="bi bi-stop-circle"></i></button>`;
}
actions += `<button class="btn btn-sm btn-outline-danger btn-delete-video" data-uuid="${video.video_uuid}" title="Delete"><i class="bi bi-trash"></i></button>`;
const progress = video.extracted_frame_count ? `${video.extracted_frame_count} / ${video.frame_count || '?'}` : '-';
const statusHtml = getStatusBadge(video.status, video.status_message);
const statusMsg = video.status_message ? `<div class="small text-muted mt-1">${video.status_message}</div>` : '';
const row = `
<tr>
<td class="align-middle"><strong>${video.description}</strong></td>
<td class="align-middle text-muted small">${video.video_filename || ''}</td>
<td class="align-middle">${statusHtml}${statusMsg}</td>
<td class="align-middle font-weight-bold text-muted small">${progress}</td>
<td class="align-middle"><span class="badge badge-neutral">${video.labeled_frame_count}</span></td>
<td class="align-middle text-right">${actions}</td>
</tr>`;
videoList.append(row);
});
const datasetVideoSelect = $('#dataset-videos');
datasetVideoSelect.empty();
data.ready_videos_for_dataset.forEach(video => {
datasetVideoSelect.append(`<option value="${video.video_uuid}">${video.description} (${video.labeled_frame_count} labels)</option>`);
});
});
}
function refreshDatasets() {
$.get('/listDatasets', function(data) {
const datasetList = $('#dataset-list');
datasetList.empty();
data.datasets.forEach(dataset => {
let actions = '';
if (dataset.status === 'READY') {
actions += `<a href="/downloadDataset/${dataset.dataset_uuid}" class="btn btn-sm btn-success mr-1" title="Download"><i class="bi bi-download"></i></a>`;
actions += `<a href="/datasetAnalysis/${dataset.dataset_uuid}" class="btn btn-sm btn-info mr-1" title="Analyze"><i class="bi bi-bar-chart-line"></i></a>`;
actions += `<button class="btn btn-sm btn-secondary btn-regenerate-dataset mr-1" data-uuid="${dataset.dataset_uuid}" title="Update"><i class="bi bi-arrow-repeat"></i></button>`;
} else if (dataset.status === 'FAILED') {
actions += `<button class="btn btn-sm btn-warning btn-regenerate-dataset mr-1" data-uuid="${dataset.dataset_uuid}" title="Retry"><i class="bi bi-arrow-repeat"></i></button>`;
}
actions += `<button class="btn btn-sm btn-outline-danger btn-delete-dataset" data-uuid="${dataset.dataset_uuid}" title="Delete"><i class="bi bi-trash"></i></button>`;
const videosCount = JSON.parse(dataset.video_uuids || '[]').length;
const classes = JSON.parse(dataset.sorted_label_list || '[]')
.map(c => `<span class="badge badge-neutral badge-pill mr-1 mb-1">${c}</span>`).join('');
const row = `
<tr>
<td class="align-middle"><strong>${dataset.description}</strong></td>
<td class="align-middle">${getStatusBadge(dataset.status, dataset.status_message)}</td>
<td class="align-middle">${videosCount} video(s)</td>
<td class="align-middle" style="line-height: 1.8;">${classes || '<span class="text-muted">-</span>'}</td>
<td class="align-middle text-right">${actions}</td>
</tr>`;
datasetList.append(row);
});
});
}
function refreshModels() {
$.get('/listModels', function(data) {
const modelList = $('#model-list');
modelList.empty();
data.models.forEach(model => {
const createdDate = new Date(model.create_time_ms).toLocaleDateString();
const typeBadge = model.model_type === 'uint8'
? `<span class="badge badge-info">INT8 (TPU)</span>`
: `<span class="badge badge-primary">FP32</span>`;
const row = `
<tr>
<td class="align-middle"><strong>${model.description}</strong></td>
<td class="align-middle">${typeBadge}</td>
<td class="align-middle text-muted small">${model.label_filename || 'N/A'}</td>
<td class="align-middle text-muted small">${createdDate}</td>
<td class="align-middle text-right">
<button class="btn btn-sm btn-outline-danger btn-delete-model" data-uuid="${model.model_uuid}"><i class="bi bi-trash"></i></button>
</td>
</tr>`;
modelList.append(row);
});
});
}
function loadSettings() {
$.get('/api/settings', function(data) {
if (data.success) {
currentSettings = data.settings;
const s = data.settings;
$('#setting-gpu-device').val(s.gpu_device);
$('#setting-sam-model').val(s.sam_model_checkpoint);
$('#setting-feature-extractor-model').val(s.feature_extractor_model_name);
$('#setting-sam-mask-confidence').val(s.sam_mask_confidence).trigger('input');
$('#setting-nms-iou-threshold').val(s.nms_iou_threshold).trigger('input');
$('#setting-prototype-temperature').val(s.prototype_temperature).trigger('input');
$('#setting-prototype-sample-limit').val(s.prototype_sample_limit);
$('#setting-batch-tracking-imgsz').val(s.batch_tracking_imgsz);
$('#setting-batch-tracking-conf').val(s.batch_tracking_conf).trigger('input');
$('#setting-batch-tracking-chunk-size').val(s.batch_tracking_chunk_size);
$('#setting-default-annotation-mode').val(s.default_annotation_mode);
$('#setting-autosave-enabled').prop('checked', s.autosave_enabled);
$('#setting-preannotation-conf').val(s.default_preannotation_conf).trigger('input');
$('#setting-jpeg-quality').val(s.frame_extraction_jpeg_quality);
$('#setting-opencv-tracker').val(s.default_opencv_tracker);
$('#setting-cache-save-interval').val(s.cache_save_interval_seconds);
loadClassColorManager();
}
});
}
function loadClassColorManager() {
$.get('/listClasses', function(data) {
if (data.success) {
const container = $('#class-color-manager');
container.empty();
if (data.labels.length === 0) {
container.html('<p class="text-muted small pl-2">No classes found. Add classes in the annotation view first.</p>');
return;
}
const classColors = currentSettings.class_colors || {};
data.labels.forEach(label => {
const color = classColors[label] || '#000000';
const item = `
<div class="d-flex align-items-center bg-white border rounded px-2 py-1 mr-2 mb-2 shadow-sm" style="min-width: 120px;">
<input type="color" class="border-0 p-0 mr-2 rounded-circle" value="${color}" data-class-name="${label}"
style="width: 24px; height: 24px; cursor: pointer; background: none;" title="Change color for ${label}">
<span class="small font-weight-bold text-truncate" style="max-width: 100px;">${label}</span>
</div>`;
container.append(item);
});
}
});
}
function setupAugmentationListeners(contextSelector) {
const context = $(contextSelector);
context.on('input', 'input[type="range"]', function() {
$(this).closest('div').find('.val-display').text($(this).val());
});
context.on('change', '.aug-option input[type="checkbox"]', function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
});
context.find('.aug-option input[type="checkbox"]').each(function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
});
}
$('#aug-enabled').on('change', function() {
$('#augmentation-options-panel').slideToggle($(this).is(':checked'));
});
setupAugmentationListeners('#createDatasetModal');
function getAugmentationOptions(contextSelector) {
const context = $(contextSelector);
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');
return {
enabled: isEnabled('#aug-enabled'),
multiply_factor: getVal('#aug-multiply-factor', 'int'),
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') }
};
}
$('#create-dataset-form').on('submit', function(e) {
e.preventDefault();
const augmentation_options = getAugmentationOptions('#createDatasetModal');
const datasetData = {
description: $('#dataset-description').val(),
video_uuids: $('#dataset-videos').val(),
eval_percent: $('#eval-percent').val(),
test_percent: $('#test-percent').val(),
augmentation_options: augmentation_options
};
$.ajax({
url: '/createDataset', type: 'POST', contentType: 'application/json',
data: JSON.stringify(datasetData),
success: function(response) {
if (response.success) {
showSuccess('Dataset creation started!');
$('#createDatasetModal').modal('hide');
refreshDatasets();
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#upload-video-form').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
$.ajax({
url: '/uploadVideo', type: 'POST', data: formData, processData: false, contentType: false,
success: function(response) {
if (response.success) {
showSuccess('Upload successful! Processing started.');
$('#uploadVideoModal').modal('hide');
$(this).trigger('reset');
$('.custom-file-label').html('Choose file...');
refreshVideos();
} else { showError(response.message); }
}.bind(this),
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#import-model-form').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
$.ajax({
url: '/importModel', type: 'POST', data: formData, processData: false, contentType: false,
success: function(response) {
if (response.success) {
showSuccess('Model imported!');
$('#importModelModal').modal('hide');
$(this).trigger('reset');
$('.custom-file-label').html('Choose file...');
refreshModels();
} else { showError(response.message); }
}.bind(this),
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#settings-form').on('submit', function(e) {
e.preventDefault();
const selectedSamOption = $('#setting-sam-model').find('option:selected');
const classColors = {};
$('#class-color-manager input[type="color"]').each(function() {
classColors[$(this).data('class-name')] = $(this).val();
});
const settingsData = {
gpu_device: $('#setting-gpu-device').val(),
sam_model_checkpoint: selectedSamOption.val(),
sam_model_name: selectedSamOption.data('name'),
feature_extractor_model_name: $('#setting-feature-extractor-model').val(),
sam_mask_confidence: parseFloat($('#setting-sam-mask-confidence').val()),
nms_iou_threshold: parseFloat($('#setting-nms-iou-threshold').val()),
prototype_temperature: parseFloat($('#setting-prototype-temperature').val()),
prototype_sample_limit: parseInt($('#setting-prototype-sample-limit').val()),
batch_tracking_imgsz: parseInt($('#setting-batch-tracking-imgsz').val()),
batch_tracking_conf: parseFloat($('#setting-batch-tracking-conf') ? $('#setting-batch-tracking-conf').val() : 0.3),
batch_tracking_chunk_size: parseInt($('#setting-batch-tracking-chunk-size') ? $('#setting-batch-tracking-chunk-size').val() : 10),
default_annotation_mode: $('#setting-default-annotation-mode').val(),
autosave_enabled: $('#setting-autosave-enabled').is(':checked'),
default_preannotation_conf: parseFloat($('#setting-preannotation-conf').val() || 0.5),
frame_extraction_jpeg_quality: parseInt($('#setting-jpeg-quality').val()),
default_opencv_tracker: $('#setting-opencv-tracker').val(),
cache_save_interval_seconds: parseInt($('#setting-cache-save-interval').val()),
class_colors: classColors
};
$.ajax({
url: '/api/settings', type: 'POST', contentType: 'application/json',
data: JSON.stringify(settingsData),
success: function(response) {
if (response.success) {
alert(response.restart_required ? "Settings saved! AI model/device changes will apply to new tasks." : "Settings saved successfully!");
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
['sam-mask-confidence', 'nms-iou-threshold', 'prototype-temperature', 'batch-tracking-conf', 'preannotation-conf'].forEach(id => {
$(`#setting-${id}`).on('input', function() {
$(this).closest('.form-group').find('.text-primary').text(parseFloat($(this).val()).toFixed(2));
});
});
$('#clear-cache-btn').on('click', function() {
if (confirm("Clear 'Smart Select' temporary cache?")) {
const $btn = $(this);
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Clearing...');
$.ajax({
url: '/api/clear_cache', type: 'POST',
success: function(res) { alert(res.message); },
complete: function() { $btn.prop('disabled', false).html('<i class="bi bi-eraser-fill"></i> Clear Feature Cache'); }
});
}
});
$(document).on("click", ".btn-manage-tasks", function() {
const videoUuid = $(this).data("uuid");
const videoDesc = $(this).data("description");
const frameCount = $(this).data("framecount");
$('#manageTasksModalTitle').text('Tasks: ' + videoDesc);
$('#task-video-uuid').val(videoUuid);
$('#task-start-frame').attr('max', frameCount - 1);
$('#task-end-frame').attr('max', frameCount - 1);
refreshTasksInModal(videoUuid);
$('#manageTasksModal').modal('show');
});
function refreshTasksInModal(videoUuid) {
const taskListBody = $('#task-list-in-modal');
taskListBody.html('<tr><td colspan="4" class="text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Loading...</td></tr>');
$.get(`/listTasks?video_uuid=${videoUuid}`, function(data) {
taskListBody.empty();
if (data.success && data.tasks.length > 0) {
data.tasks.forEach(task => {
const row = `
<tr>
<td class="align-middle font-weight-bold">${task.assigned_to}</td>
<td class="align-middle">${task.start_frame} - ${task.end_frame}</td>
<td class="align-middle text-muted small">${task.description || '-'}</td>
<td class="align-middle text-right">
<a href="/labelVideo?task_uuid=${task.task_uuid}" class="btn btn-sm btn-primary" title="Label"><i class="bi bi-pencil-square"></i></a>
<button class="btn btn-sm btn-outline-danger btn-delete-task" data-task-uuid="${task.task_uuid}" data-video-uuid="${videoUuid}"><i class="bi bi-trash"></i></button>
</td>
</tr>`;
taskListBody.append(row);
});
} else {
taskListBody.html('<tr><td colspan="4" class="text-center text-muted p-3">No tasks created yet.</td></tr>');
}
});
}
$('#create-task-form').on('submit', function(e) {
e.preventDefault();
const videoUuid = $('#task-video-uuid').val();
const taskData = {
video_uuid: videoUuid,
assigned_to: $('#task-assigned-to').val(),
description: $('#task-description').val(),
start_frame: $('#task-start-frame').val(),
end_frame: $('#task-end-frame').val(),
};
$.ajax({
url: '/createTask', type: 'POST', contentType: 'application/json', data: JSON.stringify(taskData),
success: function(response) {
if (response.success) {
$('#create-task-form')[0].reset();
refreshTasksInModal(videoUuid);
} else { showError(response.message); }
},
error: function() { showError('Error creating task.'); }
});
});
$(document).on("click", ".btn-pre-annotate", function() {
const videoUuid = $(this).data('uuid');
const frameCount = parseInt($(this).data('frame-count'));
const labeledCount = parseInt($(this).data('labeled-count'));
$('#pre-annotate-video-uuid').val(videoUuid).data('frame-count', frameCount).data('labeled-count', labeledCount);
const modelSelect = $('#pre-annotate-model-select');
modelSelect.html('<option>Loading models...</option>');
$('#pre-annotate-start-frame').val(0).attr('max', frameCount - 1);
$('#pre-annotate-end-frame').val(frameCount - 1).attr('max', frameCount - 1);
$.get('/listModels', function(data) {
modelSelect.empty();
if (data.models && data.models.length > 0) {
data.models.forEach(model => {
modelSelect.append(`<option value="${model.model_uuid}">${model.description} (${model.model_type})</option>`);
});
} else {
modelSelect.append('<option disabled>No models available. Import one first.</option>');
}
});
$('#preAnnotateModal').modal('show');
});
$('#pre-annotate-confidence').on('input', function() { $('#confidence-value').text($(this).val()); });
$('#pre-annotate-form').on('submit', function(e) {
e.preventDefault();
const videoUuid = $('#pre-annotate-video-uuid').val();
const modelUuid = $('#pre-annotate-model-select').val();
const labeledCount = parseInt($('#pre-annotate-video-uuid').data('labeled-count'));
if (!modelUuid) return showError("Please select a model.");
let msg = "Start auto-labeling?";
if (labeledCount > 0 && $('input[name="merge_strategy"]:checked').val() === 'overwrite') {
msg = `WARNING: This video has ${labeledCount} labels. This will OVERWRITE them in the selected range. Continue?`;
}
if (!confirm(msg)) return;
const options = {
start_frame: $('#pre-annotate-start-frame').val(),
end_frame: $('#pre-annotate-end-frame').val(),
confidence: $('#pre-annotate-confidence').val(),
merge_strategy: $('input[name="merge_strategy"]:checked').val()
};
$.ajax({
url: '/startPreAnnotation', type: 'POST', contentType: 'application/json',
data: JSON.stringify({ video_uuid: videoUuid, model_uuid: modelUuid, options: options }),
success: function(response) {
if (response.success) {
showSuccess('Auto-labeling started.');
$('#preAnnotateModal').modal('hide');
refreshVideos();
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
let videoUuidForImport = null;
$(document).on("click", ".btn-import-frames", function() {
videoUuidForImport = $(this).data('uuid');
$('#frame-import-input').click();
});
$('#frame-import-input').on('change', function(e) {
if (!videoUuidForImport || e.target.files.length === 0) return;
const formData = new FormData(this);
formData.append('video_uuid', videoUuidForImport);
for (let i = 0; i < e.target.files.length; i++) formData.append('frame_files', e.target.files[i]);
alert(`Importing ${e.target.files.length} frames...`);
$.ajax({
url: '/importFrames', type: 'POST', data: formData, processData: false, contentType: false,
success: function(res) {
if(res.success) { showSuccess(`Imported ${res.imported_count} frames.`); refreshVideos(); }
else showError(res.message);
},
error: function() { showError('Import failed.'); },
complete: function() { videoUuidForImport = null; $('#frame-import-input').val(''); }
});
});
$(document).on("click", ".btn-delete-video", function() {
if (confirm('Delete this video and ALL labels?')) {
$.ajax({ url: '/deleteVideo', type: 'POST', contentType: 'application/json', data: JSON.stringify({ video_uuid: $(this).data('uuid') }), success: refreshVideos });
}
});
$(document).on("click", ".btn-delete-dataset", function() {
if (confirm('Delete this dataset?')) {
$.ajax({ url: '/deleteDataset', type: 'POST', contentType: 'application/json', data: JSON.stringify({ dataset_uuid: $(this).data('uuid') }), success: refreshDatasets });
}
});
$(document).on("click", ".btn-delete-model", function() {
if (confirm('Delete this model?')) {
$.ajax({ url: '/deleteModel', type: 'POST', contentType: 'application/json', data: JSON.stringify({ model_uuid: $(this).data('uuid') }), success: refreshModels });
}
});
$(document).on("click", ".btn-delete-task", function() {
const videoUuid = $(this).data('video-uuid');
if (confirm('Delete this task?')) {
$.ajax({ url: '/deleteTask', type: 'POST', contentType: 'application/json', data: JSON.stringify({ task_uuid: $(this).data('task-uuid') }), success: function() { refreshTasksInModal(videoUuid); } });
}
});
$(document).on("click", ".btn-regenerate-dataset", function() {
if (confirm('Update dataset? Old files will be replaced.')) {
$.ajax({ url: '/regenerateDataset', type: 'POST', contentType: 'application/json', data: JSON.stringify({ dataset_uuid: $(this).data('uuid') }), success: function(res) { if(res.success){ alert('Update started.'); refreshDatasets(); } else alert(res.message); } });
}
});
$(document).on("click", ".btn-cancel-task", function() {
if (confirm('Cancel running task?')) {
$.ajax({ url: '/cancelTask', type: 'POST', contentType: 'application/json', data: JSON.stringify({ video_uuid: $(this).data('uuid') }), success: function(res) { if(res.success){ alert('Cancellation requested.'); refreshVideos(); } else alert(res.message); } });
}
});
loadSettings();
function runAllRefreshes() { refreshVideos(); refreshDatasets(); refreshModels(); }
runAllRefreshes();
setInterval(runAllRefreshes, 5000);
});
</script>
{% endblock %}