UNPKG

voluptasquisquam

Version:

Image processing and manipulation in JavaScript

376 lines (336 loc) 13.5 kB
import fromMask from './creator/fromMask'; import fromMaskConnectedComponentLabelingAlgorithm from './creator/fromMaskConnectedComponentLabelingAlgorithm'; import fromMaxima from './creator/fromMaxima'; import fromWaterShed from './creator/fromWaterShed'; import fromPoints from './creator/fromPoints'; import extendObject from 'extend'; import Image from '../Image'; import RoiMap from './RoiMap'; import RoiLayer from './RoiLayer'; /** * A manager of Regions of Interest. A RoiManager is related to a specific Image * and may contain multiple layers. Each layer is characterized by a label whose is * name by default 'default' * @class RoiManager * @param {Image} image * @param {object} [options] */ export default class RoiManager { constructor(image, options = {}) { this._image = image; this._options = options; if (!this._options.label) { this._options.label = 'default'; } this._layers = {}; this._painted = null; } // docs is in the corresponding file fromMaxima(options = {}) { let opt = extendObject({}, this._options, options); let roiMap = fromMaxima.call(this._image, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); } // docs is in the corresponding file fromPoints(points, options = {}) { let opt = extendObject({}, this._options, options); let roiMap = fromPoints.call(this._image, points, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); return this; } /** * @param {number[]} map * @param {object} [options] * @return {this} */ putMap(map, options = {}) { let roiMap = new RoiMap(this._image, map); let opt = extendObject({}, this._options, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); return this; } // docs is in the corresponding file fromWaterShed(options = {}) { let opt = extendObject({}, this._options, options); let roiMap = fromWaterShed.call(this._image, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); } // docs is in the corresponding file fromMask(mask, options = {}) { let opt = extendObject({}, this._options, options); let roiMap = fromMask.call(this._image, mask, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); return this; } fromMaskConnectedComponentLabelingAlgorithm(mask, options = {}) { let opt = extendObject({}, this._options, options); let roiMap = fromMaskConnectedComponentLabelingAlgorithm.call(this._image, mask, options); this._layers[opt.label] = new RoiLayer(roiMap, opt); return this; } /** * * @param {object} [options] * @return {RoiMap} */ getMap(options = {}) { let opt = extendObject({}, this._options, options); this._assertLayerWithLabel(opt.label); return this._layers[opt.label].roiMap; } /** * Return statistics about rows * @param {object} [options] * @return {object[]} */ rowsInfo(options = {}) { return this.getMap(options).rowsInfo(); } /** * Return statistics about columns * @param {object} [options] * @return {object[]} */ colsInfo(options = {}) { return this.getMap(options).rowsInfo(); } /** * Return the IDs of the Regions Of Interest (Roi) as an array of number * @param {object} [options] * @return {number[]} */ getRoiIds(options = {}) { let rois = this.getRois(options); if (rois) { let ids = new Array(rois.length); for (let i = 0; i < rois.length; i++) { ids[i] = rois[i].id; } return ids; } throw new Error('ROIs not found'); } /** * Allows to select ROI based on size, label and sign. * @param {object} [options={}] * @param {string} [options.label='default'] Label of the layer containing the ROI * @param {boolean} [options.positive=true] Select the positive region of interest * @param {boolean} [options.negative=true] Select he negative region of interest * @param {number} [options.minSurface=0] * @param {number} [options.maxSurface=Number.POSITIVE_INFINITY] * @param {number} [options.minWidth=0] * @param {number} [options.minHeight=Number.POSITIVE_INFINITY] * @param {number} [options.maxWidth=0] * @param {number} [options.maxHeight=Number.POSITIVE_INFINITY] * @param {number} [options.minRatio=0] Ratio width / height * @param {number} [options.maxRatio=Number.POSITIVE_INFINITY] * @return {Roi[]} */ getRois(options = {}) { let { label = this._options.label, positive = true, negative = true, minSurface = 0, maxSurface = Number.POSITIVE_INFINITY, minWidth = 0, maxWidth = Number.POSITIVE_INFINITY, minHeight = 0, maxHeight = Number.POSITIVE_INFINITY, minRatio = 0, maxRatio = Number.POSITIVE_INFINITY } = options; if (!this._layers[label]) { throw new Error('getRoi: This Roi layer (' + label + ') does not exists.'); } let allRois = this._layers[label].roi; // todo Is this old way to change the array size still faster ? let rois = new Array(allRois.length); let ptr = 0; for (let i = 0; i < allRois.length; i++) { let roi = allRois[i]; if (((roi.id < 0 && negative) || roi.id > 0 && positive) && roi.surface >= minSurface && roi.surface <= maxSurface && roi.width >= minWidth && roi.width <= maxWidth && roi.height >= minHeight && roi.height <= maxHeight && roi.ratio >= minRatio && roi.ratio <= maxRatio ) { rois[ptr++] = roi; } } rois.length = ptr; return rois; } /** * Returns an array of masks * See @links Roi.getMask for the options * @param {object} [options] * @return {Image[]} Retuns an array of masks (1 bit Image) */ getMasks(options = {}) { let rois = this.getRois(options); let masks = new Array(rois.length); for (let i = 0; i < rois.length; i++) { masks[i] = rois[i].getMask(options); } return masks; } /** * * @param {object} [options] * @return {number[]} */ getData(options = {}) { let opt = extendObject({}, this._options, options); this._assertLayerWithLabel(opt.label); return this._layers[opt.label].roiMap.data; } /** * Paint the ROI on a copy of the image adn return this image. * For painting options @links Image.paintMasks * For ROI selection options @links RoiManager.getMasks * @param {object} [options] - all the options to select ROIs * @param {string} [options.labelProperty] - Paint a mask property on the image. * May be any property of the ROI like * for example id, surface, width, height, meanX, meanY. * @return {Image} - The painted RGBA 8 bits image */ paint(options = {}) { let { labelProperty } = options; if (!this._painted) { this._painted = this._image.rgba8(); } let masks = this.getMasks(options); if (labelProperty) { let rois = this.getRois(options); options.labels = rois.map((roi) => roi[labelProperty]); options.labelsPosition = rois.map((roi) => [roi.meanX, roi.meanY]); } this._painted.paintMasks(masks, options); return this._painted; } // return a mask corresponding to all the selected masks getMask(options = {}) { let mask = new Image(this._image.width, this._image.height, {kind: 'BINARY'}); let masks = this.getMasks(options); for (let i = 0; i < masks.length; i++) { let roi = masks[i]; // we need to find the parent image to calculate the relative position for (let x = 0; x < roi.width; x++) { for (let y = 0; y < roi.height; y++) { if (roi.getBitXY(x, y)) { mask.setBitXY(x + roi.position[0], y + roi.position[1]); } } } } return mask; } /** * Reset the changes to the current painted iamge to the image that was * used during the creation of the RoiManager except if a new image is * specified as parameter; * @param {object} [options] * @param {Image} [options.image] A new iamge that you would like to sue for painting over */ resetPainted(options = {}) { const {image} = options; if (image) { this._painted = this.image.rgba8(); } else { this._painted = this._image.rgba8(); } } /** * In place modification of the roiMap that joins regions of interest * @param {object} [options] * @param {string|function(object,number,number)} [options.algorithm='commonBorderLength'] algorithm used to decide which ROIs are merged. * Current implemented algorithms are 'commonBorderLength' that use the parameters * 'minCommonBorderLength' and 'maxCommonBorderLength' as well as 'commonBorderRatio' that uses * the parameters 'minCommonBorderRatio' and 'maxCommonBorderRatio'. * @param {number} [options.minCommonBorderLength=5] minimal common number of pixels for merging * @param {number} [options.maxCommonBorderLength=100] maximal common number of pixels for merging * @param {number} [options.minCommonBorderRatio=0.3] minimal common border ratio for merging * @param {number} [options.maxCommonBorderRatio=1] maximal common border ratio for merging * @return {this} */ mergeRoi(options = {}) { const roiMap = this.getMap(options); roiMap.mergeRoi(options); this.putMap(roiMap.data, options); return this; } /** * Finds all corresponding ROIs for all ROIs in the manager * @param {number[]} roiMap * @param {object} [options] * @return {Array} array of objects returned in correspondingRoisInformation */ findCorrespondingRoi(roiMap, options = {}) { let allRois = this.getRois(options); let allRelated = []; for (let i = 0; i < allRois.length; i++) { let currentRoi = allRois[i]; let x = currentRoi.minX; let y = currentRoi.minY; let allPoints = currentRoi.points; let roiSign = Math.sign(currentRoi.id); let currentRelated = correspondingRoisInformation(x, y, allPoints, roiMap, roiSign); allRelated.push(currentRelated); } return allRelated; } _assertLayerWithLabel(label) { if (!this._layers[label]) { throw new Error(`no layer with label ${label}`); } } } /** * For a given ROI, find corresponding ROIs and properties in given ROIMap. * Returns an object containing the ID of ROIs, the surface shared by given and corresponding ROIs, * the percentage of given ROI surface covered by the corresponding ROI, the number of points with same and opposite signs, * the total number of points (same and opposite). * @param {number} x - minX value of ROI * @param {number} y - minY value of ROI * @param {Array<Array<number>>} points - points of ROI * @param {Array<number>} roiMap - roiMap from which we get the corresponding ROI * @param {number} roiSign - sign of ROI * @return {object} {{id: Array, surface: Array, roiSurfaceCovered: Array, same: number, opposite: number, total: number}} * @private */ function correspondingRoisInformation(x, y, points, roiMap, roiSign) { let correspondingRois = {id: [], surface: [], roiSurfaceCovered: [], same: 0, opposite: 0, total: 0}; for (let i = 0; i < points.length; i++) { let currentPoint = points[i]; let currentX = currentPoint[0]; let currentY = currentPoint[1]; let correspondingRoiMapIndex = (currentX + x) + (currentY + y) * roiMap.width; let value = roiMap.data[correspondingRoiMapIndex]; if (value > 0 || value < 0) { if (correspondingRois.id.includes(value)) { correspondingRois.surface[correspondingRois.id.indexOf(value)] += 1; } else { correspondingRois.id.push(value); correspondingRois.surface.push(1); } } } for (let i = 0; i < correspondingRois.id.length; i++) { let currentSign = Math.sign(correspondingRois.id[i]); if (currentSign === roiSign) { correspondingRois.same += correspondingRois.surface[i]; } else { correspondingRois.opposite += correspondingRois.surface[i]; } correspondingRois.roiSurfaceCovered[i] = correspondingRois.surface[i] / points.length; } correspondingRois.total = correspondingRois.opposite + correspondingRois.same; return correspondingRois; }