image-js
Version:
Image processing and manipulation in JavaScript
460 lines (437 loc) • 16.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _jsQuantities = _interopRequireDefault(require("js-quantities"));
var _deepValue = _interopRequireDefault(require("../../util/deepValue"));
var _Image = _interopRequireDefault(require("../Image"));
var _RoiLayer = _interopRequireDefault(require("./RoiLayer"));
var _RoiMap = _interopRequireDefault(require("./RoiMap"));
var _fromMask = _interopRequireDefault(require("./creator/fromMask"));
var _fromMaskConnectedComponentLabelingAlgorithm = _interopRequireDefault(require("./creator/fromMaskConnectedComponentLabelingAlgorithm"));
var _fromMaxima = _interopRequireDefault(require("./creator/fromMaxima"));
var _fromPoints = _interopRequireDefault(require("./creator/fromPoints"));
var _fromWaterShed = _interopRequireDefault(require("./creator/fromWaterShed"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
* 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]
*/
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 = Object.assign({}, this._options, options);
let roiMap = _fromMaxima.default.call(this._image, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
}
// docs is in the corresponding file
fromPoints(points, options = {}) {
let opt = Object.assign({}, this._options, options);
let roiMap = _fromPoints.default.call(this._image, points, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
return this;
}
/**
* @param {number[]} map
* @param {object} [options]
* @return {this}
*/
putMap(map, options = {}) {
let roiMap = new _RoiMap.default(this._image, map);
let opt = Object.assign({}, this._options, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
return this;
}
// docs is in the corresponding file
fromWaterShed(options = {}) {
let opt = Object.assign({}, this._options, options);
let roiMap = _fromWaterShed.default.call(this._image, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
}
// docs is in the corresponding file
fromMask(mask, options = {}) {
let opt = Object.assign({}, this._options, options);
let roiMap = _fromMask.default.call(this._image, mask, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
return this;
}
fromMaskConnectedComponentLabelingAlgorithm(mask, options = {}) {
let opt = Object.assign({}, this._options, options);
let roiMap = _fromMaskConnectedComponentLabelingAlgorithm.default.call(this._image, mask, options);
this._layers[opt.label] = new _RoiLayer.default(roiMap, opt);
return this;
}
/**
*
* @param {object} [options]
* @return {RoiMap}
*/
getMap(options = {}) {
let opt = Object.assign({}, 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(`this Roi layer (${label}) does not exist`);
}
const allRois = this._layers[label].roi;
const rois = [];
for (const roi of allRois) {
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.push(roi);
}
}
return rois;
}
/**
* Get an ROI by its id.
* @param {number} roiId
* @param {object} [options={}]
* @param {string} [options.label='default'] Label of the layer containing the ROI
* @return {Roi}
*/
getRoi(roiId, options = {}) {
const {
label = this._options.label
} = options;
if (!this._layers[label]) {
throw new Error(`this Roi layer (${label}) does not exist`);
}
const roi = this._layers[label].roi.find(roi => roi.id === roiId);
if (!roi) {
throw new Error(`found no Roi with id ${roiId}`);
}
return roi;
}
/**
* Returns an array of masks
* See {@link 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;
}
/**
* Returns an array of masks
* See {@link Roi.getAnalysisMasks} for the options
* @param {object} [options]
* @return {Image[]} Retuns an array of masks (1 bit Image)
*/
getAnalysisMasks(options = {}) {
const {
analysisProperty
} = options;
let maskProperty = `${analysisProperty}Mask`;
let rois = this.getRois(options);
if (rois.length === 0 || !rois[0][maskProperty]) return [];
return rois.map(roi => roi[maskProperty]);
}
/**
*
* @param {object} [options]
* @return {number[]}
*/
getData(options = {}) {
let opt = Object.assign({}, this._options, options);
this._assertLayerWithLabel(opt.label);
return this._layers[opt.label].roiMap.data;
}
/**
* Paint the ROI on a copy of the image and return this image.
* For painting options {@link Image.paintMasks}
* For ROI selection options, see {@link 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.
* @param {number} [options.pixelSize] Size of a pixel in SI
* @param {string} [options.unit="pixel"] Unit in which to display the values
* @return {Image} - The painted RGBA 8 bits image
*/
paint(options = {}) {
let {
labelProperty,
analysisProperty
} = options;
if (!this._painted) {
this._painted = this._image.rgba8();
}
let masks = this.getMasks(options);
if (labelProperty) {
const rois = this.getRois(options);
options.labels = rois.map(roi => (0, _deepValue.default)(roi, labelProperty));
const max = Math.max(...options.labels);
let isSurface = false;
let isDistance = false;
if (labelProperty.includes('surface')) {
isSurface = true;
} else if (/(?:perimeter|min|max|external|width|height|length)/.test(labelProperty)) {
isDistance = true;
}
if (isFinite(max)) {
let unitLabel = '';
if (options.unit !== 'pixel' && options.pixelSize && (isDistance || isSurface)) {
unitLabel = isSurface ? `${options.unit}^2` : options.unit;
let siLabel = isSurface ? 'm^2' : 'm';
let factor = isSurface ? options.pixelSize ** 2 : options.pixelSize;
const convert = _jsQuantities.default.swiftConverter(siLabel, unitLabel);
options.labels = options.labels.map(value => {
return convert(factor * value);
});
}
if (max > 50) {
options.labels = options.labels.map(number => Math.round(number) + unitLabel);
} else if (max > 10) {
options.labels = options.labels.map(number => number.toFixed(1) + unitLabel);
} else {
options.labels = options.labels.map(number => number.toFixed(2) + unitLabel);
}
}
options.labelsPosition = rois.map(roi => [roi.meanX, roi.meanY]);
}
this._painted.paintMasks(masks, options);
if (analysisProperty) {
let analysisMasks = this.getAnalysisMasks(options);
this._painted.paintMasks(analysisMasks, {
color: options.analysisColor,
alpha: options.analysisAlpha
});
}
return this._painted;
}
// return a mask corresponding to all the selected masks
getMask(options = {}) {
let mask = new _Image.default(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;
}
/**
* Merge multiple rois into one.
* All rois in the provided array will be merged into the first one.
* @param {Array<number>} roiIds - A list of Roi ids to merge
* @param {object} [options]
*/
mergeRois(roiIds, options = {}) {
if (!Array.isArray(roiIds) || roiIds.some(id => !Number.isInteger(id))) {
throw new Error('Roi ids must be an array of integers');
}
if (roiIds.length < 2) {
throw new Error('Roi ids must have at least two elements');
}
if (new Set(roiIds).size !== roiIds.length) {
throw new Error('Roi ids must be all different');
}
// Throws if one of the ids is wrong
roiIds.forEach(roiId => this.getRoi(roiId));
const roiMap = this.getMap(options);
roiMap.mergeRois(roiIds);
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
*/
exports.default = RoiManager;
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;
}