UNPKG

image-vectorizer

Version:

Potrace in Javascript, for NodeJS and Browser

418 lines (348 loc) 11.2 kB
// Histogram import Jimp from "jimp" import utils from "../utils.js" import Bitmap from "./Bitmap.js" var COLOR_DEPTH = 256 var COLOR_RANGE_END = COLOR_DEPTH - 1 /** * Calculates array index for pair of indexes. We multiple column (x) by 256 and then add row to it, * this way `(index(i, j) + 1) === index(i, j + i)` thus we can reuse `index(i, j)` we once calculated * * Note: this is different from how indexes calculated in {@link Bitmap} class, keep it in mind. * * @param x * @param y * @returns {*} * @private */ function index(x, y) { return COLOR_DEPTH * x + y } function normalizeMinMax(levelMin, levelMax) { /** * Shared parameter normalization for methods 'multilevelThresholding', 'autoThreshold', 'getDominantColor' and 'getStats' * * @param levelMin * @param levelMax * @returns {number[]} * @private */ levelMin = typeof levelMin === "number" ? utils.clamp(Math.round(levelMin), 0, COLOR_RANGE_END) : 0 levelMax = typeof levelMax === "number" ? utils.clamp(Math.round(levelMax), 0, COLOR_RANGE_END) : COLOR_RANGE_END if (levelMin > levelMax) { throw new Error('Invalid range "' + levelMin + "..." + levelMax + '"') } return [levelMin, levelMax] } /** * 1D Histogram * * @param {Number|Bitmap|Jimp|ImageData} imageSource - Image to collect pixel data from. Or integer to create empty histogram for image of specific size * @param [mode] Used only for Jimp images. {@link Bitmap} currently can only store 256 values per pixel, so it's assumed that it contains values we are looking for * @constructor * @protected */ function Histogram(imageSource, mode) { this.data = null this.pixels = 0 this._sortedIndexes = null this._cachedStats = {} this._lookupTableH = null if (typeof imageSource === "number") { this._createArray(imageSource) } else if (imageSource instanceof Bitmap) { this._collectValuesBitmap(imageSource) } else if (Jimp && imageSource instanceof Jimp) { this._collectValuesJimp(imageSource, mode) } else if (imageSource instanceof ImageData) { this._collectValuesImageData(imageSource) } else { throw new Error("Unsupported image source") } } Histogram.MODE_LUMINANCE = "luminance" Histogram.MODE_R = "r" Histogram.MODE_G = "g" Histogram.MODE_B = "b" Histogram.prototype = { /** * Initializes data array for an image of given pixel size * @param imageSize * @returns {Uint8Array|Uint16Array|Uint32Array} * @private */ _createArray: function (imageSize) { var ArrayType = imageSize <= Math.pow(2, 8) ? Uint8Array : imageSize <= Math.pow(2, 16) ? Uint16Array : Uint32Array this.pixels = imageSize return (this.data = new ArrayType(COLOR_DEPTH)) }, /** * Aggregates color data from {@link ImageData} instance * @param {ImageData} source * @param mode * @private */ _collectValuesImageData: function (source) { return source.data }, /** * Aggregates color data from {@link Bitmap} instance * @param {Bitmap} source * @private */ _collectValuesBitmap: function (source) { var data = this._createArray(source.size) var len = source.data.length var color for (var i = 0; i < len; i++) { color = source.data[i] data[color]++ } }, /** * Returns array of color indexes in ascending order * @param refresh * @returns {*} * @private */ _getSortedIndexes: function (refresh) { if (!refresh && this._sortedIndexes) { return this._sortedIndexes } var data = this.data var indexes = new Array(COLOR_DEPTH) var i = 0 for (i; i < COLOR_DEPTH; i++) { indexes[i] = i } indexes.sort(function (a, b) { return data[a] > data[b] ? 1 : data[a] < data[b] ? -1 : 0 }) this._sortedIndexes = indexes return indexes }, /** * Builds lookup table H from lookup tables P and S. * see {@link http://www.iis.sinica.edu.tw/page/jise/2001/200109_01.pdf|this paper} for more details * * @returns {Float64Array} * @private */ _thresholdingBuildLookupTable: function () { var P = new Float64Array(COLOR_DEPTH * COLOR_DEPTH) var S = new Float64Array(COLOR_DEPTH * COLOR_DEPTH) var H = new Float64Array(COLOR_DEPTH * COLOR_DEPTH) var pixelsTotal = this.pixels var i, j, idx, tmp // diagonal for (i = 1; i < COLOR_DEPTH; ++i) { idx = index(i, i) tmp = this.data[i] / pixelsTotal P[idx] = tmp S[idx] = i * tmp } // calculate first row (row 0 is all zero) for (i = 1; i < COLOR_DEPTH - 1; ++i) { tmp = this.data[i + 1] / pixelsTotal idx = index(1, i) P[idx + 1] = P[idx] + tmp S[idx + 1] = S[idx] + (i + 1) * tmp } // using row 1 to calculate others for (i = 2; i < COLOR_DEPTH; i++) { for (j = i + 1; j < COLOR_DEPTH; j++) { P[index(i, j)] = P[index(1, j)] - P[index(1, i - 1)] S[index(i, j)] = S[index(1, j)] - S[index(1, i - 1)] } } // now calculate H[i][j] for (i = 1; i < COLOR_DEPTH; ++i) { for (j = i + 1; j < COLOR_DEPTH; j++) { idx = index(i, j) H[idx] = P[idx] !== 0 ? (S[idx] * S[idx]) / P[idx] : 0 } } return (this._lookupTableH = H) }, /** * Implements Algorithm For Multilevel Thresholding * Receives desired number of color stops, returns array of said size. Could be limited to a range levelMin..levelMax * * Regardless of levelMin and levelMax values it still relies on between class variances for the entire histogram * * @param amount - how many thresholds should be calculated * @param [levelMin=0] - histogram segment start * @param [levelMax=255] - histogram segment end * @returns {number[]} */ multilevelThresholding: function (amount, levelMin, levelMax) { levelMin = normalizeMinMax(levelMin, levelMax) levelMax = levelMin[1] levelMin = levelMin[0] amount = Math.min(levelMax - levelMin - 2, ~~amount) if (amount < 1) { return [] } if (!this._lookupTableH) { this._thresholdingBuildLookupTable() } var H = this._lookupTableH var colorStops = null var maxSig = 0 if (amount > 4) { console.log("[Warning]: Threshold computation for more than 5 levels may take a long time") } function iterateRecursive(startingPoint, prevVariance, indexes, previousDepth) { startingPoint = (startingPoint || 0) + 1 prevVariance = prevVariance || 0 indexes = indexes || new Array(amount) previousDepth = previousDepth || 0 var depth = previousDepth + 1 // t var variance for (var i = startingPoint; i < levelMax - amount + previousDepth; i++) { variance = prevVariance + H[index(startingPoint, i)] indexes[depth - 1] = i if (depth + 1 < amount + 1) { // we need to go deeper iterateRecursive(i, variance, indexes, depth) } else { // enough, we can compare values now variance += H[index(i + 1, levelMax)] if (maxSig < variance) { maxSig = variance colorStops = indexes.slice() } } } } iterateRecursive(levelMin || 0) return colorStops ? colorStops : [] }, /** * Automatically finds threshold value using Algorithm For Multilevel Thresholding * * @param {number} [levelMin] * @param {number} [levelMax] * @returns {null|number} */ autoThreshold: function (levelMin, levelMax) { var value = this.multilevelThresholding(1, levelMin, levelMax) return value.length ? value[0] : null }, /** * Returns dominant color in given range. Returns -1 if not a single color from the range present on the image * * @param [levelMin=0] * @param [levelMax=255] * @param [tolerance=1] * @returns {number} */ getDominantColor: function (levelMin, levelMax, tolerance) { levelMin = normalizeMinMax(levelMin, levelMax) levelMax = levelMin[1] levelMin = levelMin[0] tolerance = tolerance || 1 var colors = this.data, dominantIndex = -1, dominantValue = -1, i, j, tmp if (levelMin === levelMax) { return colors[levelMin] ? levelMin : -1 } for (i = levelMin; i <= levelMax; i++) { tmp = 0 for (j = ~~(tolerance / -2); j < tolerance; j++) { tmp += utils.between(i + j, 0, COLOR_RANGE_END) ? colors[i + j] : 0 } var summIsBigger = tmp > dominantValue var summEqualButMainColorIsBigger = dominantValue === tmp && (dominantIndex < 0 || colors[i] > colors[dominantIndex]) if (summIsBigger || summEqualButMainColorIsBigger) { dominantIndex = i dominantValue = tmp } } return dominantValue <= 0 ? -1 : dominantIndex }, /** * Returns stats for histogram or its segment. * * Returned object contains median, mean and standard deviation for pixel values; * peak, mean and median number of pixels per level and few other values * * If no pixels colors from specified range present on the image - most values will be NaN * * @param {Number} [levelMin=0] - histogram segment start * @param {Number} [levelMax=255] - histogram segment end * @param {Boolean} [refresh=false] - if cached result can be returned * @returns {{levels: {mean: (number|*), median: *, stdDev: number, unique: number}, pixelsPerLevel: {mean: (number|*), median: (number|*), peak: number}, pixels: number}} */ getStats: function (levelMin, levelMax, refresh) { levelMin = normalizeMinMax(levelMin, levelMax) levelMax = levelMin[1] levelMin = levelMin[0] if (!refresh && this._cachedStats[levelMin + "-" + levelMax]) { return this._cachedStats[levelMin + "-" + levelMax] } var data = this.data var sortedIndexes = this._getSortedIndexes() var pixelsTotal = 0 var medianValue = null var meanValue var medianPixelIndex var pixelsPerLevelMean var pixelsPerLevelMedian var tmpSumOfDeviations = 0 var tmpPixelsIterated = 0 var allPixelValuesCombined = 0 var i, tmpPixels, tmpPixelValue var uniqueValues = 0 // counter for levels that's represented by at least one pixel var mostPixelsPerLevel = 0 // Finding number of pixels and mean for (i = levelMin; i <= levelMax; i++) { pixelsTotal += data[i] allPixelValuesCombined += data[i] * i uniqueValues += data[i] === 0 ? 0 : 1 if (mostPixelsPerLevel < data[i]) { mostPixelsPerLevel = data[i] } } meanValue = allPixelValuesCombined / pixelsTotal pixelsPerLevelMean = pixelsTotal / (levelMax - levelMin) pixelsPerLevelMedian = pixelsTotal / uniqueValues medianPixelIndex = Math.floor(pixelsTotal / 2) // Finding median and standard deviation for (i = 0; i < COLOR_DEPTH; i++) { tmpPixelValue = sortedIndexes[i] tmpPixels = data[tmpPixelValue] if (tmpPixelValue < levelMin || tmpPixelValue > levelMax) { continue } tmpPixelsIterated += tmpPixels tmpSumOfDeviations += Math.pow(tmpPixelValue - meanValue, 2) * tmpPixels if (medianValue === null && tmpPixelsIterated >= medianPixelIndex) { medianValue = tmpPixelValue } } return (this._cachedStats[levelMin + "-" + levelMax] = { // various pixel counts for levels (0..255) levels: { mean: meanValue, median: medianValue, stdDev: Math.sqrt(tmpSumOfDeviations / pixelsTotal), unique: uniqueValues }, // what's visually represented as bars pixelsPerLevel: { mean: pixelsPerLevelMean, median: pixelsPerLevelMedian, peak: mostPixelsPerLevel }, pixels: pixelsTotal }) } } export default Histogram