UNPKG

visual-regression-engine

Version:

A modular visual comparison engine for Processing and p5.js regression testing

140 lines (110 loc) 3.81 kB
class ClusterAnalyzer { constructor(options = {}) { this.options = options; } analyzeClusters(diffBuffer, width, height, options) { const visited = new Set(); const clusters = []; // Find all clusters using BFS for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pos = (y * width + x) * 4; // Check if this is a diff pixel (red from pixelmatch) and not visited if (this._isDiffPixel(diffBuffer, pos) && !visited.has(pos)) { const cluster = this._findCluster(diffBuffer, x, y, width, height, visited); clusters.push(cluster); } } } // Analyze cluster significance const significantClusters = clusters.filter( cluster => !cluster.isLineShift && cluster.size >= options.minClusterSize ); const significantPixels = significantClusters.reduce((sum, cluster) => sum + cluster.size, 0); return { clusters, significantClusters: significantClusters.length, significantPixels, totalClusters: clusters.length }; } _isDiffPixel(buffer, pos) { return buffer[pos] === 255 && buffer[pos + 1] === 0 && buffer[pos + 2] === 0; } _findCluster(buffer, startX, startY, width, height, visited) { const queue = [{ x: startX, y: startY }]; const clusterPixels = []; let size = 0; while (queue.length > 0) { const { x, y } = queue.shift(); const pos = (y * width + x) * 4; if (visited.has(pos) || !this._isDiffPixel(buffer, pos)) { continue; } visited.add(pos); size++; clusterPixels.push({ x, y }); // Add 8-connected neighbors for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const npos = (ny * width + nx) * 4; if (!visited.has(npos)) { queue.push({ x: nx, y: ny }); } } } } } const isLineShift = this._detectLineShift(clusterPixels, buffer, width, height); return { size, pixels: clusterPixels, isLineShift, bounds: this._calculateBounds(clusterPixels) }; } _detectLineShift(clusterPixels, buffer, width, height) { if (clusterPixels.length === 0) return false; let linelikePixels = 0; for (const { x, y } of clusterPixels) { let neighbors = 0; // Count diff pixel neighbors in 8-connected neighborhood for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const npos = (ny * width + nx) * 4; if (this._isDiffPixel(buffer, npos)) { neighbors++; } } } } // Line-like pixels typically have ≤2 neighbors if (neighbors <= 2) { linelikePixels++; } } // If >80% of pixels are line-like, consider it a line shift return linelikePixels / clusterPixels.length > this.options.lineShiftThreshold; } _calculateBounds(pixels) { if (pixels.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; let minX = pixels[0].x, maxX = pixels[0].x; let minY = pixels[0].y, maxY = pixels[0].y; for (const { x, y } of pixels) { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); } return { minX, minY, maxX, maxY, width: maxX - minX + 1, height: maxY - minY + 1 }; } } module.exports = ClusterAnalyzer;