Add dataset QA and custom YOLO training flow

This commit is contained in:
2026-06-30 14:04:11 +08:00
parent 43ed767b4f
commit 93af8bcd3a
14 changed files with 529 additions and 18 deletions

View File

@@ -56,6 +56,22 @@ type UploadedDataset = {
samples: Record<string, Array<{ name: string; relative_path: string; size: number; previewable: boolean }>>;
};
type DatasetValidation = {
dataset: string;
counts: { images: number; labels: number; masks: number; annotations: number };
pairs: {
image_label: number;
image_mask: number;
images_without_labels: string[];
labels_without_images: string[];
images_without_masks: string[];
masks_without_images: string[];
};
classes: number[];
checks: Array<{ name: string; passed: boolean; count?: number; labels?: number; masks?: number; errors?: unknown[] }>;
ready: { yolo: boolean; mask: boolean; any: boolean };
};
type ResultItem = {
name: string;
path: string;
@@ -152,6 +168,7 @@ const defaultParams: Record<string, Record<string, unknown>> = {
"segmodel.train": { architecture: "Unet" },
"segmodel.predict": { architecture: "Unet", run_choice: 1 },
"yolo.train": { model: "YOLOv8n-seg" },
"yolo.train_custom": { model: "YOLO11n-seg", data: "var/uploads/datasets/example/dataset.yaml", epochs: 10, imgsz: 640, batch: 1, workers: 0, device: "cpu", exist_ok: true },
"yolo.predict": { model: "YOLOv8n-seg", pt_name: "best.pt", conf: 0.2, run_choice: 1 },
"yolo.heatmap": { model: "YOLOv8n-seg", cam_method: "All", pt_name: "best.pt", run_choice: 1 },
"mmseg.generate_alg": { dataset_choice: 1, gpu_count: 1, gpu_ids: [0], schedule_mode: 2, max_epochs: 300, algorithm_choice: 1 },
@@ -197,6 +214,7 @@ function useData() {
const [results, setResults] = useState<ResultItem[]>([]);
const [curves, setCurves] = useState<TrainingCurve[]>([]);
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({});
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
@@ -221,6 +239,18 @@ function useData() {
setResults(resultsNext.slice(0, 80));
setCurves(curvesNext.slice(0, 12));
setDatasets(datasetsNext);
const validationEntries: Array<[string, DatasetValidation]> = [];
await Promise.all(
datasetsNext.map(async (dataset) => {
try {
const validation = await api<DatasetValidation>(`/api/datasets/${encodeURIComponent(dataset.name)}/validate`);
validationEntries.push([dataset.name, validation]);
} catch {
// Dataset validation is advisory; upload and job controls should remain usable.
}
})
);
setDatasetValidations(Object.fromEntries(validationEntries));
setCoverage(coverageNext);
setAcceptance(acceptanceNext);
setDeepAcceptance(deepAcceptanceNext);
@@ -236,7 +266,7 @@ function useData() {
return () => window.clearInterval(timer);
}, []);
return { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh };
return { catalog, gpus, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
}
function StatusPill({ status }: { status: string }) {
@@ -244,7 +274,7 @@ function StatusPill({ status }: { status: string }) {
}
function App() {
const { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const { catalog, gpus, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
@@ -278,6 +308,7 @@ function App() {
() => datasets.find((dataset) => dataset.name === selectedDatasetName) ?? datasets.find((dataset) => dataset.name === datasetName),
[datasetName, datasets, selectedDatasetName]
);
const selectedValidation = selectedDataset ? datasetValidations[selectedDataset.name] : undefined;
const selectedCurve = curves.find((curve) => curve.relative_path === selectedCurvePath) ?? curves[0];
function pickTask(next: string) {
@@ -385,6 +416,23 @@ function App() {
}
}
async function generateSelectedYoloYaml() {
if (!selectedDataset) return;
setBusy(true);
try {
const classNames = selectedValidation?.classes.map((classId) => `class_${classId}`) ?? undefined;
const generated = await api<{ relative_path: string; path: string }>(`/api/datasets/${encodeURIComponent(selectedDataset.name)}/yolo-yaml`, {
method: "POST",
body: JSON.stringify({ class_names: classNames })
});
setTaskType("yolo.train_custom");
setParams(JSON.stringify({ model: "YOLO11n-seg", data: generated.path, epochs: 10, imgsz: 640, batch: 1, workers: 0, device: "cpu", exist_ok: true }, null, 2));
await refresh();
} finally {
setBusy(false);
}
}
async function inspectJob(job: Job) {
const detail = await api<Job>(`/api/jobs/${job.id}`);
setSelectedJob(detail);
@@ -565,13 +613,18 @@ function App() {
<p className="eyebrow">Files</p>
<h2></h2>
</div>
<FileImage size={22} />
<div className="buttonRow compactButtons">
<button className="iconButton" disabled={busy || !selectedValidation?.ready.yolo} onClick={generateSelectedYoloYaml} title="生成 YOLO dataset.yaml">
<FileSearch size={18} />
</button>
<FileImage size={22} />
</div>
</div>
<div className="datasetList">
{datasets.map((dataset) => (
<div key={dataset.name}>
<div
className={`datasetCard ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
key={dataset.name}
role="button"
tabIndex={0}
onClick={() => {
@@ -589,6 +642,10 @@ function App() {
<strong>{dataset.name}</strong>
<span>{dataset.counts.images} image · {dataset.counts.labels} label · {dataset.counts.masks} mask</span>
</div>
<div className="readinessLine">
<StatusPill status={datasetValidations[dataset.name]?.ready.yolo ? "success" : "queued"} />
<small>YOLO {datasetValidations[dataset.name]?.pairs.image_label ?? 0} pair · Mask {datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair</small>
</div>
<div className="sampleStrip">
{["images", "labels", "masks"].flatMap((kind) =>
(dataset.samples[kind] ?? []).slice(0, 4).map((sample) => (
@@ -600,8 +657,10 @@ function App() {
)}
</div>
</div>
</div>
))}
</div>
{selectedValidation && <DatasetQuality validation={selectedValidation} />}
</div>
</section>
@@ -819,6 +878,31 @@ function ResultPreview({ results }: { results: ResultItem[] }) {
);
}
function DatasetQuality({ validation }: { validation: DatasetValidation }) {
return (
<div className="qualityBox">
<div className="qualityHead">
<strong>{validation.dataset}</strong>
<span>{validation.ready.yolo ? "YOLO READY" : validation.ready.mask ? "MASK READY" : "CHECK"}</span>
</div>
<div className="qualityStats">
<div><span>Image/Label</span><strong>{validation.pairs.image_label}</strong></div>
<div><span>Image/Mask</span><strong>{validation.pairs.image_mask}</strong></div>
<div><span>Classes</span><strong>{validation.classes.length || 0}</strong></div>
<div><span>Annotations</span><strong>{validation.counts.annotations}</strong></div>
</div>
<div className="qualityChecks">
{validation.checks.map((check) => (
<div key={check.name} className={check.passed ? "ok" : "bad"}>
<span>{check.name}</span>
<small>{check.passed ? "ok" : `${check.errors?.length ?? 0} issue`}</small>
</div>
))}
</div>
</div>
);
}
function CurvePanel({
curves,
selected,