/** * 数据拆分工具类 * 提供随机分配、种子生成等功能 */ 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 }; };