yolo数据集标记
This commit is contained in:
@@ -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>
|
||||
@@ -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">×</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
@@ -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>
|
||||
@@ -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>×</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 %}
|
||||
Reference in New Issue
Block a user