/**
|
* 数据拆分工具类
|
* 提供随机分配、种子生成等功能
|
*/
|
|
import { ElMessage } from "element-plus";
|
import { generateRandomSeed } from "./randomSeedUtils";
|
|
/**
|
* 使用Fisher-Yates洗牌算法进行随机分配
|
* @param {Array} instances - 图片数组
|
* @param {number} trainCount - 训练集数量
|
* @param {number} valCount - 验证集数量
|
* @param {number} testCount - 测试集数量
|
* @param {number} seed - 随机种子
|
* @returns {Object} { assignments: Map, seed: number }
|
*/
|
export const assignInstancesRandomly = (
|
instances,
|
trainCount,
|
valCount,
|
testCount,
|
seed = null
|
) => {
|
// 设置随机种子
|
const randomSeed = seed || generateRandomSeed();
|
const assignments = new Map();
|
|
// 伪随机数生成器(确保可重现)
|
let rng = randomSeed;
|
const random = () => {
|
rng = (rng * 9301 + 49297) % 233280;
|
return rng / 233280;
|
};
|
|
// 创建分配数组
|
const assignments_array = [
|
...Array(trainCount).fill("train"),
|
...Array(valCount).fill("val"),
|
...Array(testCount).fill("test"),
|
];
|
|
// 不自动补充剩余图片,允许部分图片保持未分配状态
|
const total = instances.length;
|
const assigned = trainCount + valCount + testCount;
|
console.log(`[splitUtils] 分配信息: 总图片${total}张,预期分配${assigned}张,未分配${total - assigned}张`)
|
|
// Fisher-Yates 洗牌算法
|
for (let i = assignments_array.length - 1; i > 0; i--) {
|
const j = Math.floor(random() * (i + 1));
|
[assignments_array[i], assignments_array[j]] = [
|
assignments_array[j],
|
assignments_array[i],
|
];
|
}
|
|
// 随机选择要分配的图片
|
const shuffledInstances = [...instances];
|
for (let i = shuffledInstances.length - 1; i > 0; i--) {
|
const j = Math.floor(random() * (i + 1));
|
[shuffledInstances[i], shuffledInstances[j]] = [shuffledInstances[j], shuffledInstances[i]];
|
}
|
|
// 只分配指定数量的图片,剩余的保持未分配
|
for (let i = 0; i < assigned && i < shuffledInstances.length; i++) {
|
assignments.set(shuffledInstances[i].id, assignments_array[i]);
|
}
|
|
return { assignments, seed: randomSeed };
|
};
|
|
/**
|
* 生成拆分ID
|
* @returns {string} 格式: split_YYYYMMDDHHMMSSSS
|
*/
|
export const generateSplitId = () => {
|
const now = new Date();
|
|
// 格式化日期部分 YYYYMMDD
|
const year = now.getFullYear();
|
const month = (now.getMonth() + 1).toString().padStart(2, "0");
|
const day = now.getDate().toString().padStart(2, "0");
|
const dateStr = `${year}${month}${day}`;
|
|
// 格式化时间部分 HHMMSSSS
|
const hours = now.getHours().toString().padStart(2, "0");
|
const minutes = now.getMinutes().toString().padStart(2, "0");
|
const seconds = now.getSeconds().toString().padStart(2, "0");
|
const milliseconds = now.getMilliseconds().toString().padStart(3, "0");
|
const timeStr = `${hours}${minutes}${seconds}${milliseconds}`;
|
|
return `split_${dateStr}${timeStr}`;
|
};
|
|
/**
|
* 生成拆分名称
|
* @returns {string} 格式: split_YYMMDDHHmmss (年月日时分秒)
|
*/
|
export const generateSplitName = () => {
|
const now = new Date();
|
|
// 使用年月日时分秒格式,简洁且唯一
|
const year = now.getFullYear().toString().slice(-2); // 取后两位年份
|
const month = (now.getMonth() + 1).toString().padStart(2, "0");
|
const day = now.getDate().toString().padStart(2, "0");
|
const hours = now.getHours().toString().padStart(2, "0");
|
const minutes = now.getMinutes().toString().padStart(2, "0");
|
const seconds = now.getSeconds().toString().padStart(2, "0");
|
|
// 格式: split_231215143025 (年月日时分秒)
|
const timeStr = `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
return `split_${timeStr}`;
|
};
|
|
/**
|
* 验证拆分数据的有效性
|
* @param {Object} splitData - 拆分数据
|
* @param {number} totalInstances - 总实例数
|
* @returns {Object} { isValid: boolean, errors: string[] }
|
*/
|
export const validateSplitData = (splitData, totalInstances) => {
|
const errors = [];
|
|
if (!splitData.name || splitData.name.trim() === "") {
|
errors.push("拆分名称不能为空");
|
}
|
|
const totalCount =
|
splitData.trainCount + splitData.valCount + splitData.testCount;
|
if (totalCount > totalInstances) {
|
errors.push(`总数量 ${totalCount} 超过了实例总数 ${totalInstances}`);
|
}
|
|
if (totalCount === 0) {
|
errors.push("至少需要分配一个实例");
|
ElMessage({
|
message: "至少需要分配一个实例",
|
type: "warning",
|
});
|
}
|
|
return {
|
isValid: errors.length === 0,
|
errors,
|
};
|
};
|
|
/**
|
* 计算拆分比例
|
* @param {number} trainCount - 训练集数量
|
* @param {number} valCount - 验证集数量
|
* @param {number} testCount - 测试集数量
|
* @param {number} totalInstances - 总实例数
|
* @returns {Object} { trainRatio, valRatio, testRatio }
|
*/
|
export const calculateSplitRatios = (
|
trainCount,
|
valCount,
|
testCount,
|
totalInstances
|
) => {
|
if (totalInstances === 0)
|
return { trainRatio: 0, valRatio: 0, testRatio: 0 };
|
|
return {
|
trainRatio: Math.round((trainCount / totalInstances) * 100),
|
valRatio: Math.round((valCount / totalInstances) * 100),
|
testRatio: Math.round((testCount / totalInstances) * 100),
|
};
|
};
|
|
/**
|
* 根据比例计算数量
|
* @param {number} ratio - 比例 (0-100)
|
* @param {number} totalInstances - 总实例数
|
* @returns {number} 计算得出的数量
|
*/
|
export const calculateCountFromRatio = (ratio, totalInstances) => {
|
return Math.floor((ratio / 100) * totalInstances);
|
};
|
|
/**
|
* 计算数据集分配统计
|
* @param {Map} assignments - 分配映射
|
* @returns {Object} { trainCount, valCount, testCount, total }
|
*/
|
export const calculateDatasetCounts = (assignments) => {
|
const assignmentValues = Array.from(assignments.values());
|
const trainCount = assignmentValues.filter(v => v === 'train').length;
|
const valCount = assignmentValues.filter(v => v === 'val').length;
|
const testCount = assignmentValues.filter(v => v === 'test').length;
|
|
return {
|
trainCount,
|
valCount,
|
testCount,
|
total: trainCount + valCount + testCount
|
};
|
};
|
|
/**
|
* 计算图像分布统计
|
* @param {Array} images - 图片数组
|
* @param {Map} assignments - 分配映射
|
* @returns {Object} { train: Set, val: Set, test: Set }
|
*/
|
export const calculateImageDistribution = (images, assignments) => {
|
const imagesByDataset = { train: new Set(), val: new Set(), test: new Set() };
|
|
images.forEach(image => {
|
const assignment = assignments.get(image.id);
|
if (assignment && imagesByDataset[assignment]) {
|
imagesByDataset[assignment].add(image.id);
|
}
|
});
|
|
return imagesByDataset;
|
};
|
|
/**
|
* 计算百分比
|
* @param {number} value - 数值
|
* @param {number} total - 总数
|
* @returns {number} 百分比 (0-100)
|
*/
|
export const calculatePercentage = (value, total) => {
|
return total > 0 ? Math.round(value / total * 100) : 0;
|
};
|
|
/**
|
* 计算分配图片的实例统计
|
* @param {Array} allImages - 所有图片数据
|
* @param {Map} assignments - 分配映射(图片ID -> 数据集类型)
|
* @returns {Object} { trainInstances, valInstances, testInstances, totalInstances }
|
*/
|
export const calculateInstanceCounts = (allImages, assignments) => {
|
const counts = { train: 0, val: 0, test: 0 };
|
|
// 遍历分配的图片,统计各数据集的实例数量
|
assignments.forEach((datasetType, imageId) => {
|
const image = allImages.find(img => img.id === imageId);
|
if (image && counts[datasetType] !== undefined) {
|
counts[datasetType] += image.annotationCount || 0;
|
}
|
});
|
|
return {
|
trainInstances: counts.train,
|
valInstances: counts.val,
|
testInstances: counts.test,
|
totalInstances: counts.train + counts.val + counts.test
|
};
|
};
|