Add dataset QA and custom YOLO training flow
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user