UNPKG

@wdio/image-comparison-core

Version:

Image comparison core module for @wdio/visual-service - WebdriverIO visual testing framework

243 lines (242 loc) 9.97 kB
/** * NOTE: This code/logic is based on logical research and support from the following sources: * - Copilot AI * - ChatGPT * * This is still a draft and may not be accurate, more research is needed and will tell you if this is correct. * It produces the following log based on `150585` diff pixels: * * [0-0] Processing diff pixels started * [0-0] Bounding boxes: [ * [0-0] { left: 912, top: 743, right: 1144, bottom: 762 }, * [0-0] { left: 650, top: 749, right: 790, bottom: 760 }, * [0-0] { left: 537, top: 749, right: 644, bottom: 760 }, * [0-0] { left: 377, top: 749, right: 415, bottom: 760 }, * [0-0] { left: 362, top: 749, right: 371, bottom: 759 }, * [0-0] { left: 290, top: 750, right: 356, bottom: 762 }, * [0-0] { left: 159, top: 746, right: 284, bottom: 760 }, * [0-0] { left: 536, top: 711, right: 754, bottom: 730 }, * [0-0] { left: 913, top: 711, right: 1186, bottom: 730 }, * [0-0] { left: 368, top: 717, right: 413, bottom: 730 }, * [0-0] { left: 159, top: 711, right: 362, bottom: 728 }, * [0-0] { left: 912, top: 652, right: 1144, bottom: 703 }, * [0-0] { left: 536, top: 652, right: 790, bottom: 701 }, * [0-0] { left: 377, top: 690, right: 415, bottom: 701 }, * [0-0] { left: 362, top: 690, right: 371, bottom: 700 }, * [0-0] { left: 159, top: 652, right: 356, bottom: 703 }, * [0-0] { left: 129, top: 529, right: 1236, bottom: 633 }, * [0-0] { left: 475, top: 457, right: 1046, bottom: 513 }, * [0-0] { left: 319, top: 470, right: 454, bottom: 513 }, * [0-0] { left: 387, top: 399, right: 514, bottom: 428 }, * [0-0] { left: 818, top: 398, right: 978, bottom: 422 }, * [0-0] { left: 527, top: 398, right: 807, bottom: 428 }, * [0-0] { left: 600, top: 143, right: 766, bottom: 338 }, * [0-0] { left: 25, top: 27, right: 56, bottom: 58 } * [0-0] ] * [0-0] Processing 150585 diff pixels * [0-0] Union operations started * [0-0] Union time: 155ms * [0-0] Grouping pixels into bounding boxes * [0-0] Grouping time: 19ms * [0-0] Total analysis time: 209ms * [0-0] Post-processing bounding boxes * [0-0] Post-processing time: 3ms * [0-0] Number merged: 24 */ import logger from '@wdio/logger'; import { saveBase64Image, addBlockOuts } from './images.js'; const log = logger('@wdio/visual-service:@wdio/image-comparison-core:pixelDiffProcessing'); class DisjointSet { parent; rank; constructor() { this.parent = new Map(); this.rank = new Map(); } find(x) { if (this.parent.get(x) !== x) { this.parent.set(x, this.find(this.parent.get(x))); // Path compression } return this.parent.get(x); } union(x, y) { const rootX = this.find(x); const rootY = this.find(y); if (rootX !== rootY) { const rankX = this.rank.get(rootX) || 0; const rankY = this.rank.get(rootY) || 0; if (rankX > rankY) { this.parent.set(rootY, rootX); } else if (rankX < rankY) { this.parent.set(rootX, rootY); } else { this.parent.set(rootY, rootX); this.rank.set(rootX, rankX + 1); } } } add(x) { if (!this.parent.has(x)) { this.parent.set(x, x); this.rank.set(x, 0); } } } function mergeBoundingBoxes(boxes, proximity) { log.info(`Merging bounding boxes started with a proximity of ${proximity} pixels`); const merged = []; while (boxes.length) { const box = boxes.pop(); let mergedWithAnotherBox = false; for (let i = 0; i < boxes.length; i++) { const otherBox = boxes[i]; if (box.left <= otherBox.right + proximity && box.right >= otherBox.left - proximity && box.top <= otherBox.bottom + proximity && box.bottom >= otherBox.top - proximity) { boxes.splice(i, 1); boxes.push({ left: Math.min(box.left, otherBox.left), top: Math.min(box.top, otherBox.top), right: Math.max(box.right, otherBox.right), bottom: Math.max(box.bottom, otherBox.bottom), }); mergedWithAnotherBox = true; break; } } if (!mergedWithAnotherBox) { merged.push(box); } } return merged; } function processDiffPixels(diffPixels, proximity) { log.info('Processing diff pixels started'); log.info(`Processing ${diffPixels.length} diff pixels`); // Calculate total pixels and diff percentage let maxX = 0; let maxY = 0; for (const pixel of diffPixels) { maxX = Math.max(maxX, pixel.x); maxY = Math.max(maxY, pixel.y); } const totalPixels = diffPixels.length > 0 ? (maxX + 1) * (maxY + 1) : 0; const diffPercentage = totalPixels > 0 ? (diffPixels.length / totalPixels) * 100 : 0; log.info(`Total pixels in image: ${totalPixels.toLocaleString()}`); log.info(`Number of diff pixels: ${diffPixels.length.toLocaleString()}`); log.info(`Diff percentage: ${diffPercentage.toFixed(2)}%`); // Fail fast if there are too many differences const MAX_DIFF_PERCENTAGE = 20; // 20% threshold const MAX_DIFF_PIXELS = 5000000; // 5M pixels threshold if (diffPercentage > MAX_DIFF_PERCENTAGE || diffPixels.length > MAX_DIFF_PIXELS) { log.error(`Too many differences detected! Diff percentage: ${diffPercentage.toFixed(2)}%, Diff pixels: ${diffPixels.length.toLocaleString()}`); log.error('This likely indicates a major visual difference or an issue with the comparison.'); log.error('Consider checking if the baseline image is correct or if there are major UI changes.'); // Return a single bounding box covering the entire image return [{ left: 0, top: 0, right: maxX, bottom: maxY }]; } const totalStartTime = Date.now(); const ds = new DisjointSet(); const pixelMap = new Map(); const directions = [ { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 1, dy: 1 }, { dx: -1, dy: 1 }, ]; // Initialize disjoint set and pixel map for (const pixel of diffPixels) { const key = `${pixel.x},${pixel.y}`; ds.add(key); pixelMap.set(key, pixel); } log.info('Union operations started'); const unionStartTime = Date.now(); // Union pixels within the proximity range for (const pixel of diffPixels) { const key = `${pixel.x},${pixel.y}`; for (const { dx, dy } of directions) { const neighborKey = `${pixel.x + dx},${pixel.y + dy}`; if (pixelMap.has(neighborKey)) { ds.union(key, neighborKey); } } } log.info(`Union time: ${Date.now() - unionStartTime}ms`); log.info('Grouping pixels into bounding boxes'); const groupingStartTime = Date.now(); // Group pixels by their root const groups = new Map(); for (const key of pixelMap.keys()) { const root = ds.find(key); if (!groups.has(root)) { groups.set(root, []); } groups.get(root)?.push(pixelMap.get(key)); } // Calculate bounding boxes const boundingBoxes = []; for (const pixels of groups.values()) { let left = Infinity; let top = Infinity; let right = -Infinity; let bottom = -Infinity; for (const pixel of pixels) { if (pixel.x < left) { left = pixel.x; } if (pixel.y < top) { top = pixel.y; } if (pixel.x > right) { right = pixel.x; } if (pixel.y > bottom) { bottom = pixel.y; } } boundingBoxes.push({ left, top, right, bottom }); } log.info(`Grouping time: ${Date.now() - groupingStartTime}ms`); const totalAnalysisTime = Date.now() - totalStartTime; log.info(`Total analysis time: ${totalAnalysisTime}ms`); // Post-process to merge nearby bounding boxes log.info('Post-processing bounding boxes'); const postProcessStartTime = Date.now(); const mergedBoxes = mergeBoundingBoxes(boundingBoxes, proximity); log.info(`Post-processing time: ${Date.now() - postProcessStartTime}ms`); log.info(`Number merged: ${mergedBoxes.length}`); return mergedBoxes; } /** * Generate and save diff image with bounding boxes */ export async function generateAndSaveDiff(data, imageCompareOptions, ignoredBoxes, diffFilePath, rawMisMatchPercentage) { const diffBoundingBoxes = []; const saveAboveTolerance = imageCompareOptions.saveAboveTolerance ?? 0; const storeDiffs = rawMisMatchPercentage > saveAboveTolerance || process.argv.includes('--store-diffs'); if (storeDiffs) { const isDifference = rawMisMatchPercentage > saveAboveTolerance; const isDifferenceMessage = 'WARNING:\n There was a difference. Saved the difference to'; const debugMessage = 'INFO:\n Debug mode is enabled. Saved the debug file to:'; if (imageCompareOptions.createJsonReportFiles) { diffBoundingBoxes.push(...processDiffPixels(data.diffPixels, imageCompareOptions.diffPixelBoundingBoxProximity)); } await saveBase64Image(await addBlockOuts(Buffer.from(await data.getBuffer()).toString('base64'), ignoredBoxes), diffFilePath); log.warn('\x1b[33m%s\x1b[0m', ` ##################################################################################### ${isDifference ? isDifferenceMessage : debugMessage} ${diffFilePath} #####################################################################################`); } return { diffBoundingBoxes, storeDiffs }; } export { processDiffPixels };