Bachelorarbeit/packages/render-fidelity-tools/dist/image-comparison-worker.js

241 lines
12 KiB
JavaScript

(function () {
'use strict';
/* @license ISC
* @see LICENSE
*/
// NOTE(cdata): This is an adapted subset of the original pixelmatch library.
// The top-level API of the original library has been omitted, as we only make
// use of the lower-level features that aren't actually exported by the upstream
// module.
// calculate color difference according to the paper "Measuring perceived color
// difference using YIQ NTSC transmission color space in mobile applications" by
// Y. Kotsarenko and F. Ramos
function colorDelta(img1, img2, k, m, yOnly = false) {
var a1 = img1[k + 3] / 255, a2 = img2[m + 3] / 255, r1 = blend(img1[k + 0], a1), g1 = blend(img1[k + 1], a1), b1 = blend(img1[k + 2], a1), r2 = blend(img2[m + 0], a2), g2 = blend(img2[m + 1], a2), b2 = blend(img2[m + 2], a2), y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
if (yOnly)
return y; // brightness difference only
var i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2), q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
}
function rgb2y(r, g, b) {
return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
}
function rgb2i(r, g, b) {
return r * 0.59597799 - g * 0.27417610 - b * 0.32180189;
}
function rgb2q(r, g, b) {
return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
}
// blend semi-transparent color with white
function blend(c, a) {
return 255 + (c - 255) * a;
}
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const COMPONENTS_PER_PIXEL = 4;
// 35215 is the maximum possible value for the YIQ difference metric
// @see https://github.com/mapbox/pixelmatch/blob/master/index.js#L14
// @see http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
const MAX_COLOR_DISTANCE = 35215;
class ImageComparator {
constructor(candidateImage, goldenImage, dimensions) {
this.candidateImage = candidateImage;
this.goldenImage = goldenImage;
this.dimensions = dimensions;
const { width, height } = dimensions;
this.imagePixels = width * height;
}
drawPixel(image, position, r, g, b, a = 255) {
image[position + 0] = r;
image[position + 1] = g;
image[position + 2] = b;
image[position + 3] = a;
}
generateVisuals(threshold) {
const { candidateImage, goldenImage } = this;
const { width, height } = this.dimensions;
const blackWhiteImage = new Uint8ClampedArray(this.imagePixels * COMPONENTS_PER_PIXEL);
const deltaImage = new Uint8ClampedArray(this.imagePixels * COMPONENTS_PER_PIXEL);
const thresholdSquared = threshold * threshold;
let maximumDeltaIntensity = 0;
if (candidateImage.length != goldenImage.length) {
throw new Error(`Image sizes do not match (candidate: ${candidateImage.length}, golden: ${goldenImage.length})`);
}
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const index = y * width + x;
const position = index * COMPONENTS_PER_PIXEL;
const delta = colorDelta(candidateImage, goldenImage, position, position);
const exactlyMatched = (delta <= thresholdSquared ? 1 : 0) * 255;
const thresholdDelta = Math.max(0, delta - thresholdSquared);
const deltaIntensity = Math.round(255 * thresholdDelta / MAX_COLOR_DISTANCE);
maximumDeltaIntensity = Math.max(deltaIntensity, maximumDeltaIntensity);
this.drawPixel(blackWhiteImage, position, exactlyMatched, exactlyMatched, exactlyMatched);
this.drawPixel(deltaImage, position, 255, 255 - deltaIntensity, 255 - deltaIntensity);
}
}
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const index = y * width + x;
const position = index * COMPONENTS_PER_PIXEL;
const absoluteDeltaIntensity = 255 - deltaImage[position + 1];
const relativeDeltaIntensity = Math.round(255 - 255 * (absoluteDeltaIntensity / maximumDeltaIntensity));
this.drawPixel(deltaImage, position, 255, relativeDeltaIntensity, relativeDeltaIntensity);
}
}
return {
imageBuffers: { delta: deltaImage.buffer, blackWhite: blackWhiteImage.buffer }
};
}
analyze() {
const { candidateImage, goldenImage } = this;
const { width, height } = this.dimensions;
let squareSum = 0;
if (candidateImage.length != goldenImage.length) {
throw new Error(`Image sizes do not match (candidate: ${candidateImage.length}, golden: ${goldenImage.length})`);
}
let modelPixelCount = 0;
let colorlessPixelCount = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const index = y * width + x;
// image's pixel data is stored in an 1-D array, 1st row sequentialy,
// then 2nd row, .. for each pixel, its data is stored by order of r, g,
// b, a. here position is the index for current pixel's r , position+3
// is index for its alpha
const position = index * COMPONENTS_PER_PIXEL;
// alpha is in range 0~255 here, map it to 0~1
const alpha = candidateImage[position + 3] / 255;
let isWhitePixel = true;
let isBlackPixel = true;
for (let i = 0; i < 3; i++) {
const colorComponent = candidateImage[position + i] * alpha;
if (colorComponent != 255) {
isWhitePixel = false;
}
if (colorComponent != 0) {
isBlackPixel = false;
}
}
if (isBlackPixel || isWhitePixel) {
colorlessPixelCount++;
}
if (alpha === 0) {
continue;
}
const delta = colorDelta(candidateImage, goldenImage, position, position);
squareSum += delta * delta;
modelPixelCount++;
}
}
const imagePixelCount = width * height;
if (colorlessPixelCount === imagePixelCount) {
throw new Error('Candidate image is colorless!');
}
const rmsDistanceRatio = Math.sqrt(squareSum / modelPixelCount) / MAX_COLOR_DISTANCE;
return { analysis: { rmsDistanceRatio } };
}
}
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ImageComparisonWorker {
constructor() {
this.analyzer = null;
this.candidateCanvas = null;
this.candidateContext = null;
this.goldenCanvas = null;
this.goldenContext = null;
this.blackWhiteCanvas = null;
this.blackWhiteContext = null;
this.deltaCanvas = null;
this.deltaContext = null;
self.onmessage = (event) => this.onGlobalMessage(event);
}
onMessage(event, port) {
const data = event.data;
switch (data.type) {
case 'canvases-ready': {
const { candidateCanvas, goldenCanvas, blackWhiteCanvas, deltaCanvas } = data;
this.candidateCanvas = candidateCanvas;
this.candidateContext = candidateCanvas.getContext('2d');
this.goldenCanvas = goldenCanvas;
this.goldenContext = goldenCanvas.getContext('2d');
this.blackWhiteCanvas = blackWhiteCanvas;
this.blackWhiteContext = blackWhiteCanvas.getContext('2d');
this.deltaCanvas = deltaCanvas;
this.deltaContext = deltaCanvas.getContext('2d');
break;
}
case 'images-assigned': {
const { candidateImageBuffer, goldenImageBuffer, dimensions } = data;
if (this.candidateCanvas == null || this.goldenCanvas == null ||
this.blackWhiteCanvas == null || this.deltaCanvas == null) {
console.warn('Images assigned before canvases are available!');
}
this.candidateCanvas.width = this.goldenCanvas.width =
this.blackWhiteCanvas.width = this.deltaCanvas.width =
dimensions.width;
this.candidateCanvas.height = this.goldenCanvas.height =
this.blackWhiteCanvas.height = this.deltaCanvas.height =
dimensions.height;
const candidateArray = new Uint8ClampedArray(candidateImageBuffer);
const goldenArray = new Uint8ClampedArray(goldenImageBuffer);
const { width, height } = dimensions;
this.analyzer =
new ImageComparator(candidateArray, goldenArray, dimensions);
this.candidateContext.putImageData(new ImageData(candidateArray, width, height), 0, 0);
this.goldenContext.putImageData(new ImageData(goldenArray, width, height), 0, 0);
break;
}
case 'threshold-changed': {
const { threshold } = data;
const { analyzer } = this;
if (analyzer == null) {
console.warn(`Analyzer not created!`);
return;
}
const { width, height } = this.analyzer.dimensions;
const result = analyzer.generateVisuals(threshold);
this.blackWhiteContext.putImageData(new ImageData(new Uint8ClampedArray(result.imageBuffers.blackWhite), width, height), 0, 0);
this.deltaContext.putImageData(new ImageData(new Uint8ClampedArray(result.imageBuffers.delta), width, height), 0, 0);
port.postMessage({ type: 'analysis-completed', result });
break;
}
}
}
onGlobalMessage(event) {
event.ports.forEach(port => port.onmessage = (event) => this.onMessage(event, port));
}
}
self.imageComparisonWorker = new ImageComparisonWorker();
})();
//# sourceMappingURL=image-comparison-worker.js.map