//ImageLibJS是一个基于ES6面向WEB的以图像处理为核心的数组运算库。
//文档生成命令:npx jsdoc ./Imagelib.js -d docs
/**
* @class ImgArray更重要的是数组,提供数组的计算,但是数组的运算支持上偏向于图像处理,
* 后期可以仿照Numpy转为强大的数组计算库,支持常见的多种元素数据类型初始化,但是仅对
* float32类型进行了广泛地验证和测试,其他类型未验证,使用时需要注意自行检查。
*/
class ImgArray {
/**
* dtypes 是一个静态对象,对于存储数组数据的类型与JS TypedArray的映射关系。
* @type {Object}
* @example
* //dtypes的值如下:
* dtypes = {
* float32: Float32Array,
* uint8: Uint8Array,
* uint16: Uint16Array,
* int32: Int32Array,
* uint32: Uint32Array,
* cuint8: Uint8ClampedArray,
* int16: Int16Array,
* int8: Int8Array,
* float64: Float64Array,
* };
*/
static dtypes = {
float32: Float32Array,
uint8: Uint8Array,
uint16: Uint16Array,
int32: Int32Array,
uint32: Uint32Array,
cuint8: Uint8ClampedArray,
int16: Int16Array,
int8: Int8Array,
float64: Float64Array,
};
/**
* dtypenames 获取{@link ImgArray#dtypes}对象中所有数据类型的名称。返回一个包含所有数据类型名称的数组,数组中包含{@link ImgArray#dtypes}对象的所有键。
* @static
* @type {string[]}
*/
static get dtypenames() {
return Object.keys(this.dtypes);
}
/**
* constructor,数组初始化函数,采用对象的方式进行初始化,提供了默认参数。
* 只实现了三维数组,各维度类比于图像的高,宽和通道,即height,width,channel。
* @constructor
* @param {Object} options - 配置对象,用于初始化图像数据。
* @param {number} [options.height=256] - 表示数组(图像)的高度,默认值是256
* @param {number} [options.width=256] - 表示数组(图像)的宽度,默认值是256
* @param {number} [options.channel=3] - 表示数组(图像)的通道数,默认值是3
* @param {boolean} [options.lazy=false] - 默认值为false,表示进行数组元素的初始化,否则不进行元素的数组元素的初始化
* @param {string} [options.dtype='float32'] - 默认为'float32',可选的值为dtypes中的值
*/
constructor({ height = 256, width = 256, channel = 3, lazy = false, dtype = 'float32' } = {}) {
this.height = height;
this.width = width;
this.channel = channel;
this.numel = height * width * channel; //元素数量
this.dtype = dtype
this.data = null;
if (lazy == false) {
this.initarray()
}
}
/**
* initarray() 根据数组的基本信息,为数组分配存储空间。
* 当数组初始化时lazy=false时调用,分配空间,分配后数组元素的值为0。
* @returns {ImgArray} 返回当前实例,以支持链式调用。
*/
initarray() {
if (this.data == null) {
this.data = new ImgArray.dtypes[this.dtype](this.numel);
}
this.lazy = false;
return this;
}
/**
* 表示数组形状
* @typedef {Object} Shape
* @property {number} height - 数组的高度
* @property {number} width - 数组的宽度
* @property {number} channel -数组的通道数
*/
/**
* 返回图像的形状对象。用属性的方式获取数组的形状,包括高,宽,通道。
* @type {Shape}
*/
get shape() {
return { height: this.height, width: this.width, channel: this.channel };
}
/**
* size,获取数组元素的个数。
* @type {number}
*/
get size() {
return this.numel;
}
/**
* elbytes,数组中一个元素占用的字节数。
* @type {number}
*/
get elbytes() {
return ImgArray.dtypes[this.dtype].BYTES_PER_ELEMENT;
}
/**
* bytesize,用属性的方式获取数组占用的总字节数。
* @type {number}
*/
get bytesize() {
return this.numel * this.elbytes;
}
/**
* elidxtoidx,将元素的索引转为数组的三个索引值[h,w,c]
* 该函数是一个内部函数,一般由其他方法调用,不对外使用
* @param {number} [elidx=0] - 线性索引,默认值为0,表示获取第一个元素,即[0,0,0]
* @returns {number[]|undefined} 返回一个包含三维索引的数组,或者如果 `elidx` 超出范围,返回 `undefined`。
* - hidx: 高度索引,表示数组的第0维索引。
* - widx: 宽度索引,表示数组的第1维索引。
* - cidx: 通道索引,表示数组的第2维索引。
*/
elidxtoidx(elidx = 0) {
if (elidx < 0 || elidx > this.numel - 1) return;
let hwidth = this.width * this.channel;
let hidx = parseInt(elidx / hwidth);
elidx = elidx % hwidth;
let widx = parseInt(elidx / this.channel);
let cidx = elidx % this.channel;
return [hidx, widx, cidx];
}
/**
* idxtoelidx,将数组的三个索引值[h,w,c],转为元素索引elidx
* 该函数是一个内部函数,一般由其他方法调用,不对外使用
* @param {number} hidx - 数组元素的高索引,也就是第0维度的索引
* @param {number} widx - 数组元素的宽索引,也就是第1维度的索引
* @param {number} cidx - 数组元素的通道索引,也就是第2维度的索引
* @return {number} - 元素在数组内的真实索引值
*/
idxtoelidx(hidx, widx, cidx) {
return (hidx * this.width + widx) * this.channel + cidx;
}
/**
* checkisvalid,检查索引值是否合法,即是否在数组范围内
* 目前只支持正索引
* @param {number} hidx - 数组元素的高索引,也就是第0维度的索引
* @param {number} widx - 数组元素的宽索引,也就是第1维度的索引
* @param {number} cidx - 数组元素的通道索引,也就是第2维度的索引
* @return {boolean} - true表示索引合法,false表示索引不合法
*/
checkisvalid(hidx, widx, cidx) {
if (hidx + 1 > this.height || hidx < 0) return false;
if (widx + 1 > this.width || widx < 0) return false;
if (cidx + 1 > this.channel || cidx < 0) return false;
return true;
}
/**
* getel,根据数组索引获取元素值
* @param {number} hidx - 数组元素的高索引,也就是第0维度的索引
* @param {number} widx - 数组元素的宽索引,也就是第1维度的索引
* @param {number} cidx - 数组元素的通道索引,也就是第2维度的索引
* @returns {number|null} 元素值,若数组元素索引越界,则返回null
*/
getel(hidx, widx, cidx) {
//由数组索引获取元素值
// TODO: 当数组为
if (this.data === null) {
console.error('数组没有初始化');
return null;
}
if (this.checkisvalid(hidx, widx, cidx)) {
return this.data.at(this.idxtoelidx(hidx, widx, cidx));
}
console.error('数组元素索引越界');
return null;
}
/**
* setel,根据索引设置指定元素的值
* @param {number} hidx - 数组元素的高索引,也就是第0维度的索引
* @param {number} widx - 数组元素的宽索引,也就是第1维度的索引
* @param {number} cidx - 数组元素的通道索引,也就是第2维度的索引
* @param {number} value - 要设置的元素值
* @returns {ImgArray|null} 该数组对象本身,若数组元素索引越界,则返回null
*/
setel(hidx, widx, cidx, value) {
this.initarray();
if (this.checkisvalid(hidx, widx, cidx)) {
this.data[this.idxtoelidx(hidx, widx, cidx)] = value;
return this; //以供链式调用
}
console.error('数组元素索引越界')
return null;
}
/**
* fill,向数组填充常数数值。
* @param {number|number[]} [value=3] - 要填充的值,可以是一个数值,也可以是一个数组,数组长度与通道数相同,表示对每个通道进行填充
* @param {number|number[]} [cidx=null] - 要填充的通道索引,可以是一个数值,也可以是一个数组,表示对多个通道进行填充
* @returns {ImgArray} 该数组对象本身
* @example
* new ImgArray().fill(255); //填充所有元素值为255;
* @example
* new ImgArray().fill(255,1); //只对1通道填充为255;
* @example
* new ImgArray().fill(255,[0,1]); //对0,1通道填充均填充为255
* @example
* new ImgArray().fill([255,120],[0,2]); //对0通道填充255,对2道填充120
*/
fill(value = 3, cidx = null) {
this.initarray();
if (cidx == null) {
this.data.fill(value);
return this;
}
if (cidx instanceof Array) {
if (value instanceof Array) {
if (value.length == cidx.length) {
let map = {};
cidx.forEach((x, idx) => { map[x] = value[idx]; });
this.data.forEach((x, idx, arr) => {
let ci = idx % this.channel;
if (map[ci]) arr[idx] = map[ci];
}, this);
}
else {
console.error("value数组长度与cidx数组长度不一致");
}
}
else {
this.data.forEach((x, idx, arr) => { if (cidx.includes(idx % this.channel)) arr[idx] = value; }, this);
}
}
else {
this.data.forEach((x, idx, arr) => { if (idx % this.channel == cidx) arr[idx] = value; }, this);
}
return this;
}
/**
* issameshape,两数组形状是否相同
* @param {ImgArray} bimgarr - 另一个数组
* @returns {Boolean} 两数组形状是否相同
*/
issameshape(bimgarr) {
//判断两数组形状是否相同
return this.height === bimgarr.height &&
this.width === bimgarr.width &&
this.channel === bimgarr.channel;
}
/**
* issamehw,两数组宽高是否相同
* @param {ImgArray} bimgarr - 另一个数组
* @returns {Boolean} 两数组宽高是否相同
*/
issamehw(bimgarr) {
return this.height === bimgarr.height && this.width === bimgarr.width;
}
/**
* issamech,判断两数组的通道是否相同
* @param {ImgArray} bimgarr - 另一个数组
* @returns {Boolean} 两数组的通道是否相同
*/
issamech(bimgarr) {
return this.channel === bimgarr.channel
}
/**
* dstack,将多个数组沿通道堆叠,数组的宽高必须要相同
* @param {ImgArray|...ImgArray} - 一个或多个数组
* @returns {ImgArray|undefined} 堆叠形成的新数组,若堆叠数组的宽高尺寸不一致,返回undefined
*/
dstack(...imgars) {
//imgars里为ImgArray实例,应当要具备相同的尺寸
//将该数组与另外多个高宽一致的数组堆叠为一个新数组
let tmpimgarrs = [this, ...imgars];
let channelnum = 0;
let channelmapping = [];
for (let imgidx = 0; imgidx < tmpimgarrs.length; imgidx++) {
if (!this.issamehw(tmpimgarrs[imgidx])) {
console.error(`错误:第${imgidx}个堆叠数组的宽高尺寸不一致`);
return;
}
let tmpchannel = tmpimgarrs[imgidx].channel;
channelnum += tmpchannel;
for (let cidx = 0; cidx < tmpchannel; cidx++) {
channelmapping.push([imgidx, cidx]);
}
}
let newimgarr = new ImgArray({ height: this.height, width: this.width, channel: channelnum })
for (let hidx = 0; hidx < newimgarr.height; hidx++) {
for (let widx = 0; widx < newimgarr.width; widx++) {
for (let cidx = 0; cidx < newimgarr.channel; cidx++) {
let [imgidx, ocidx] = channelmapping[cidx];
let tmpv = tmpimgarrs[imgidx].getel(hidx, widx, ocidx);
newimgarr.setel(hidx, widx, cidx, tmpv)
}
}
}
return newimgarr;
}
/**
* dsplit,提取数组的某些通道
* @param {...number} channels - 要提取的通道索引
* @returns {ImgArray} 提取的数组
*/
dsplit(...channels) {
//将数组沿通道进行分割
//可获取指定通道的数据
//dsplit(0,2),获取第0和2通道,返回是一个ImgArray的数组
let arr = [];
if (channels.length == 0) channels = [...new Array(this.channel).keys()]
for (let i of channels) {
let tmp = new ImgArray({ height: this.height, width: this.width, channel: 1 });
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
let value = this.getel(hidx, widx, i);
tmp.setel(hidx, widx, 0, value);
}
}
arr.push(tmp);
}
return arr;
}
/**
* hstack,水平堆叠数组,沿宽度方向叠加
* @param {...ImgArray} imgars - 水平堆叠的数组
* @returns {ImgArray} 水平堆叠后的结果
*/
hstack(...imgars) {
//需要数组的高和通道数一置,但是该函数不进行检测,由用户来保证
let tmpimgarrs = [this, ...imgars];
let newwidth = 0;
let widths = [];
for (let imgidx = 0; imgidx < tmpimgarrs.length; imgidx++) {
let tmpwidth = tmpimgarrs[imgidx].width;
newwidth += tmpwidth;
widths.push(newwidth);
}
let newimgarr = new ImgArray({ height: this.height, width: newwidth, channel: this.channel })
for (let hidx = 0; hidx < newimgarr.height; hidx++) {
for (let widx = 0; widx < newimgarr.width; widx++) {
let imgidx = widths.map(x => widx < x).indexOf(true);
let owidx = imgidx > 0 ? widx - widths[imgidx - 1] : widx;
for (let cidx = 0; cidx < newimgarr.channel; cidx++) {
newimgarr.setel(hidx, widx, cidx, tmpimgarrs[imgidx].getel(hidx, owidx, cidx));
}
}
}
return newimgarr;
}
/**
* vstack,垂直堆叠数组,沿高度方向叠加
* @param {...ImgArray} imgarrs - 垂直堆叠的数组
* @returns {ImgArray} 垂直堆叠后的结果
*/
vstack(...imgarrs) {
//需要数组的宽和通道数一置,但是该函数不进行检测,由用户来保证
let tmpimgarrs = [this, ...imgarrs];
let newheight = 0;
for (let imgidx = 0; imgidx < tmpimgarrs.length; imgidx++) {
newheight += tmpimgarrs[imgidx].height;
}
let newarray = new ImgArray({ height: newheight, width: this.width, channel: this.channel });
let offsetidx = 0;
for (let imgidx = 0; imgidx < tmpimgarrs.length; imgidx++) {
newarray.data.set(tmpimgarrs[imgidx].data, offsetidx);
offsetidx += tmpimgarrs[imgidx].numel;
}
return newarray;
}
/**
* 最近邻放大
* scale,数组,使用最近邻方法进行对数组在宽度和高度上进行整数倍的缩放。
* @param {number} scale -尺寸
*/
scale(k = 8) {
let outarr = new ImgArray({ height: this.height * k, width: this.width * k, channel: this.channel, dtype: this.dtype })
for (let hidx = 0; hidx < outarr.height; hidx++) {
for (let widx = 0; widx < outarr.width; widx++) {
for (let cidx = 0; cidx < outarr.channel; cidx++) {
let v = this.getel(parseInt(hidx / k), parseInt(widx / k), cidx);
outarr.setel(hidx, widx, cidx, v);
}
}
}
return outarr;
}
/**
* grid,将多个尺寸相同的数组堆叠成网格
* @param {number} row - 行数
* @param {number} col - 列数
* @param {...ImgArray} imgarrs - 需要堆叠的数组
* @returns {ImgArray|undefined} 网格堆叠后的结果,若尺寸不一致,返回undefined
*/
grid(row, col, ...imgarrs) {
//参数为ImgArray实例,且必须为相同尺寸
let tmpimgarrs = [this, ...imgarrs];
if (row * col < 2 && tmpimgarrs.length != row * col) {
console.error(`错误:参数错误,row=${row},col=${col},imgarrs.length=${imgarrs.length}`);
return;
}
let outarr = new ImgArray({ height: row * this.height, width: col * this.width, channel: this.channel });
for (let hidx = 0; hidx < outarr.height; hidx++) {
for (let widx = 0; widx < outarr.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
const imghidx = Math.floor(hidx / this.height);
const imgwidx = Math.floor(widx / this.width);
const imgidx = imghidx * col + imgwidx;
let tmpv = tmpimgarrs[imgidx].getel(hidx % this.height, widx % this.width, cidx);
outarr.setel(hidx, widx, cidx, tmpv);
}
}
}
return outarr;
}
/**
* repeat,在水平和竖直方向重复数组
* @param {number} row - 在高度方向上重复的次数,也就是在行方向上重复的次数
* @param {number} col - 在宽度方向上重复的次数,也就是在列方向上重复的次数
* @returns {ImgArray|undefined} 重复后的结果,若row * col < 2,则返回undefined
*/
repeat(row, col) {
if (row * col < 2) {
console.error(`错误:repeat参数错误,row=${row},col=${col}`);
return;
}
let outarr = new ImgArray({ height: row * this.height, width: col * this.width, channel: this.channel });
for (let hidx = 0; hidx < outarr.height; hidx++) {
for (let widx = 0; widx < outarr.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx % this.height, widx % this.width, cidx);
outarr.setel(hidx, widx, cidx, tmpv);
}
}
}
return outarr;
}
/**
* filiplr,在宽度轴上,进行左右翻转
* @returns {ImgArray} 水平翻转后的结果
*/
fliplr() {
let outarr = this.empty(false);
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx, widx, cidx);
outarr.setel(hidx, this.width - widx - 1, cidx, tmpv);
}
}
}
return outarr
}
/**
* flipud, 在高度轴上,进行上下翻转
* @returns {ImgArray} 上下翻转后的结果
*/
flipud() {
let outarr = this.empty(false);
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx, widx, cidx);
outarr.setel(this.height - hidx - 1, widx, cidx, tmpv)
}
}
}
return outarr
}
/**
* rollud,上下滚动
* @param {number} [dy=100] - 滚动距离,默认为100
* @returns {ImgArray} 滚动后的结果
*/
rollud(dy = 100) {
dy = -Math.sign(dy) * (Math.abs(dy) % this.height);
let newarr = this.empty(false);
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx, widx, cidx);
newarr.setel((hidx + dy + this.height) % this.height, widx, cidx, tmpv)
}
}
}
return newarr;
}
/**
* rolllr,左右滚动
* @param {number} [dx=100] - 滚动距离,默认为100
* @returns {ImgArray} 滚动后的结果
*/
rolllr(dx = 100) {
dx = Math.sign(dx) * (Math.abs(dx) % this.width);
let newarr = this.empty(false);
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx, widx, cidx);
newarr.setel(hidx, (widx + dx + this.width) % this.width, cidx, tmpv)
}
}
}
return newarr;
}
/**
* rotate,旋转
* @param {number} [degree=90] - 旋转角度,默认为90
* @returns {ImgArray} 旋转后的结果
*/
rotate(degree = 90) {
degree = degree % 360;
let rad = degree * Math.PI / 180;
let newarr = this.empty(false);
let cx = (this.width - 1) / 2;
let cy = (this.height - 1) / 2;
let cos = Math.cos(rad);
let sin = Math.sin(rad);
let dx = cx - cx * cos + cy * sin;
let dy = cy - cx * sin - cy * cos;
for (let hidx = 0; hidx < this.height; hidx++) {
for (let widx = 0; widx < this.width; widx++) {
let oldwidx = cos * widx - sin * hidx + dx;
let oldhidx = sin * widx + cos * hidx + dy;
if (!this.checkisvalid(oldhidx, oldwidx, 0)) continue;
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(oldhidx, oldwidx, cidx);
newarr.setel(hidx, widx, cidx, tmpv);
}
}
}
return newarr
}
//数组点运算
/**
* vectorize,逐元素运算,function的形式参考array.map接受的回调函数的形式
* @param {function} func - 函数,形式为(x)=>y
* @returns {ImgArray} 运算结果
*/
vectorize(func) {
let newarr = this.empty(true);
newarr.data = this.data.map(func);
return newarr;
}
//数组间的运算
/**
* operateArrayOperation,数组间运算,运算方式由参数func决定
* @param {ImgArray} otherArray - 与当前数组尺寸相同
* @param {function} func - 是一个具有两个参数的函数,返回一个值
* @returns {ImgArray|null} 运算结果,若两数组的尺寸不匹配或类型不匹配,则返回null
*/
operateArrayOperation(otherArray, func) {
if (this.issameshape(otherArray)) {
let newarr = this.empty(true);
newarr.data = this.data.map((val, idx) => func(val, otherArray.data[idx]));
return newarr;
} else {
console.error('两数组的尺寸不匹配或类型不匹配。');
return null;
}
}
/**
* add,数组加法,支持常数加法
* @param {ImgArray|number} otherArrayOrValue - 与当前数组尺寸相同,或者为一个常数
* @returns {ImgArray|null} 加法后的结果,若输入的是无效的操作数,则返回null
*/
add(otherArrayOrValue) {
const addFunc = (x, y) => x + y;
if (typeof otherArrayOrValue === 'number') {
return this.vectorize(x => addFunc(x, otherArrayOrValue));
} else if (otherArrayOrValue instanceof ImgArray) {
return this.operateArrayOperation(otherArrayOrValue, addFunc);
} else {
console.error('无效的操作数。');
return null;
}
}
/**
* sub,数组减法,支持常数或数组
* @param {ImgArray|number} otherArrayOrValue - 与当前数组尺寸相同,或者为一个常数
* @returns {ImgArray|null} 减法后的结果,若输入的是无效的操作数,则返回null
*/
sub(otherArrayOrValue) {
const subFunc = (x, y) => x - y;
if (typeof otherArrayOrValue === 'number') {
return this.vectorize(x => subFunc(x, otherArrayOrValue));
} else if (otherArrayOrValue instanceof ImgArray) {
return this.operateArrayOperation(otherArrayOrValue, subFunc);
} else {
console.error('无效的操作数。');
return null;
}
}
/**
* mul,数组乘法,支持常数或数组
* @param {ImgArray|number} otherArrayOrValue - 与当前数组尺寸相同,或者为一个常数
* @returns {ImgArray|null} 乘法后的结果,若输入的是无效的操作数,则返回null
*/
mul(otherArrayOrValue) {
const mulFunc = (x, y) => x * y;
if (typeof otherArrayOrValue === 'number') {
return this.vectorize(x => mulFunc(x, otherArrayOrValue));
} else if (otherArrayOrValue instanceof ImgArray) {
return this.operateArrayOperation(otherArrayOrValue, mulFunc);
} else {
console.error('无效的操作数。');
return null;
}
}
/**
* div,数组除法,支持常数或数组
* @param {ImgArray|number} otherArrayOrValue - 与当前数组尺寸相同,或者为一个常数
* @returns {ImgArray|null} 除法后的结果,若输入的除数数组中包含0元素或除数不合法,则返回null
*/
div(otherArrayOrValue) {
const divFunc = (x, y) => x / y;
if (typeof otherArrayOrValue === 'number' && otherArrayOrValue !== 0) {
return this.vectorize(x => divFunc(x, otherArrayOrValue));
} else if (otherArrayOrValue instanceof ImgArray) {
if (otherArrayOrValue.data.some(value => value === 0)) {
console.error('除数数组中包含0元素。');
return null;
}
return this.operateArrayOperation(otherArrayOrValue, divFunc);
} else {
console.error('除数不合法。');
return null;
}
}
/**
* clamp,截断,将小于vmin的元素变成vmin,大于vmax的元素变成vmax
* @param {number} [vmin=0] - 最小值,默认为0
* @param {number} [vmax=255] - 最大值,默认为255
* @returns {ImgArray} 截断后的结果
*/
clamp(vmin = 0, vmax = 255) {
return this.vectorize(x => x < vmin ? vmin : (x > vmax ? vmax : x));
}
/**
* abs,对数组中的每个元素计算绝对值。返回一个与原数组尺寸相同的新数组,每个元素为原数组元素的绝对值。
* @returns {ImgArray}
*/
abs() {
return this.vectorize(x => Math.abs(x));
}
/**
* square,计算数组中每个元素的平方。
* @returns {ImgArray} 平方结果
*/
square() {
return this.vectorize(x => x * x);
}
/**
* pow,幂运算,计算数组中每个元素的幂。
* @param {number} value - 幂数
* @returns {ImgArray} 幂运算结果
*/
pow(value) {
return this.vectorize(x => Math.pow(x, value));
}
/**
* ReLU函数,将小于value的元素变成value,大于value的不变
* @param {number} [value=0] - 阈值,默认为0
* @returns {ImgArray} ReLU函数结果
*/
relu(value = 0) {
return this.vectorize(x => x < value ? value : x);
}
/**
* opposite,取反,即每个元素取负
* @returns {ImgArray} 取反结果
*/
opposite() {
return this.vectorize(x => -x);
}
/**
* vectorizemath,对Math中的函数进行运算
* @returns {ImgArray} 函数运算结果
*/
vectorizemath(func = Math.sin, ...params) {
//可以看成是一个母函数,只要是调用了Math的函数,都可以用这个函数,
//例如:前面的pow()方法,写成vectorizemath(Math.pow,y)
//例如:前面的abs()方法,写成vectorizemath(Math.abs)
return this.vectorize(x => func(x, ...params))
}
/**
* todo:分成5个函数,分别对应不同的阈值处理方式
* @param {number} [threshold=100] - 阈值,默认为100
* @param {string} [method='binary'] - 阈值处理方式,默认为'binary',可选值有'binary'、'truncate'、'binary_inv'、'tozero'、'tozero_inv'
* @param {number} [maxval=255] - 最大值,默认为255
* @returns {ImgArray} 阈值处理结果
*/
threshold(threshold = 100, method = 'binary', maxval = 255) {
if (method == 'binary') {
//二值化
return this.vectorize((x) => { return x > threshold ? maxval : 0 })
}
else if (method == 'truncate') {
//截断化
return this.vectorize((x) => { return x > threshold ? threshold : x })
}
else if (method == 'binary_inv') {
//二值化(反转)
return this.vectorize((x) => { return x > threshold ? 0 : maxval })
}
else if (method == 'tozero') {
//归零化
return this.vectorize((x) => { return x > threshold ? x : 0 })
}
else if (method == 'tozero_inv') {
//归零化(反转)
return this.vectorize((x) => { return x > threshold ? 0 : x })
}
}
/**
* span 区间变换
* @param {number} lower - 下限
* @param {number} upper - 上限
* @param {number} [vmin=0] - 最小值,默认为0
* @param {number} [vmax=255] - 最大值,默认为255
* @returns {ImgArray} 区间变换结果
*/
span(lower, upper, vmin = 0, vmax = 255) {
//区间变换
let func = (x) => {
if (x >= upper) return vmax;
if (x <= lower) return vmin;
return (x - lower) / (upper - lower) * (vmax - vmin) + vmin;
}
return this.vectorize(func);
}
/**
* globalminmax,获取整个数组的最小值和最大值
* @returns {Object} 包含最小值和最大值的对象
*/
globalminmax() {
//获取整个数组的最小值和最大值
let minv = this.data[0];
let maxv = this.data[0];
this.data.forEach((x) => {
if (x < minv) {
minv = x;
return
}
if (x > maxv) {
maxv = x;
return
}
});
return { minv, maxv };
}
/**
* stretch,拉伸到指定的数值范围内
* @param {number} [vmin=0] - 最小值,默认为0
* @param {number} [vmax=255] - 最大值,默认为255
* @returns {ImgArray} 拉伸后的结果
*/
stretch(vmin = 0, vmax = 255) {
//拉伸到指定的数值范围内
let { minv, maxv } = this.globalminmax();
if (minv == maxv) {
return this.copy().fill((vmin + vmax) / 2);
}
else {
return this.vectorize((x) => { return (x - minv) / (maxv - minv) * (vmax - vmin) + vmin });
}
}
/**
* pad,对高和宽进行常数填充的扩边操作,顺序是左上右下
* @param {Object} options - 填充选项
* @param {number} [options.l=1] - 左边填充的个数,默认为1
* @param {number} [options.r=2] - 右边填充的个数,默认为2
* @param {number} [options.t=3] - 上边填充的个数,默认为3
* @param {number} [options.b=4] - 下边填充的个数,默认为4
* @param {number} [options.fillvalue=0] - 填充的值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含填充后的数据。
*/
pad({ l = 1, r = 2, t = 3, b = 4, fillvalue = 0 } = {}) {
let newheight = this.height + t + b;
let newwidth = this.width + l + r;
let newarr = new ImgArray({ height: newheight, width: newwidth, channel: this.channel }).fill(fillvalue);
for (let hidx = t; hidx < this.height + t; hidx++) {
for (let widx = l; widx < this.width + l; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let ohidx = hidx - t;
let owidx = widx - l;
let tmpv = this.getel(ohidx, owidx, cidx);
newarr.setel(hidx, widx, cidx, tmpv);
}
}
}
return newarr;
}
/**
* slice,对高和宽进行切片操作
* @param {number} sthidx - 高的起始位置(不包含结束位置)
* @param {number} edhidx - 高的结束位置(不包含结束位置)
* @param {number} stwidx - 宽的起始位置(不包含结束位置)
* @param {number} edwidx - 宽的结束位置(不包含结束位置)
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含切片后的数据。
*/
slice(sthidx, edhidx, stwidx, edwidx) {
//参数含义分别别是高的起始和结束位置(不包含结束位置),宽的起始和结束位置(不包含结束位置)
//返回一个新的ImgArray对象
if (this.checkisvalid(sthidx, stwidx, 0) && this.checkisvalid(edhidx - 1, edwidx - 1, 0)) {
let newheight = edhidx - sthidx;
let newwidth = edwidx - stwidx;
let newimgarr = new ImgArray({ height: newheight, width: newwidth, channel: this.channel });
for (let hidx = 0; hidx < newheight; hidx++) {
for (let widx = 0; widx < newwidth; widx++) {
for (let cidx = 0; cidx < this.channel; cidx++) {
let tmpv = this.getel(hidx + sthidx, widx + stwidx, cidx);
newimgarr.setel(hidx, widx, cidx, tmpv);
}
}
}
return newimgarr;
}
console.error('数组索引越界。')
}
/**
* structure,按照结构元素进行邻域展开
* @param {Array} pattern - 结构元素,是一个二维数组,表示邻域的形状
* @param {number} [fillvalue=0] - 填充的值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含邻域展开后的数据。
*/
structure(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
let kernelheight = pattern.length;
let kernelwidth = pattern[0].length;
let pady = Math.floor(kernelheight / 2);
let padx = Math.floor(kernelwidth / 2);
let padimgar = this.pad({ l: padx, t: pady, r: padx, b: pady, fillvalue });
let imgarrs = [];
for (let idxh = 0; idxh < kernelheight; idxh++) {
for (let idxw = 0; idxw < kernelwidth; idxw++) {
if (pattern[idxh][idxw] == 0) continue;
imgarrs.push(padimgar.slice(idxh, idxh + this.height, idxw, idxw + this.width));
}
}
let [baseimg, ...resimgs] = imgarrs;
return baseimg.dstack(...resimgs);
}
/**
* neighbor,按尺寸进行邻域展开,sizes是2个元素组成的数组,表示邻域的高和宽,里边应当是奇数
* 该方法实际上不操作,只生成pattern,调用strcuture完成邻域的生成
* @param {number[]} [sizes = [3, 3]] - 邻域的尺寸,是一个2个元素的数组,表示邻域的高和宽
* @param {number} [fillvalue=0] - 填充的值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含邻域展开后的数据。
*/
neighbor(sizes = [3, 3], fillvalue = 0) {
let pt = [];
for (let idxh = 0; idxh < sizes[0]; idxh++) {
pt.push(Array(sizes[1]).fill(1));
}
return this.structure(pt, fillvalue);
}
/**
* apply_along_channel,沿通道的运算
* @param {function} func - 一回调函数接收一个一维数组,返回一个数值
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含运算后的数据。
*/
apply_along_channel(func) {
let outimgarr = new ImgArray({ height: this.height, width: this.width, channel: 1 });
for (let idxh = 0; idxh < this.height; idxh++) {
for (let idxw = 0; idxw < this.width; idxw++) {
let stidx = this.idxtoelidx(idxh, idxw, 0);
let data = this.data.slice(stidx, stidx + this.channel);
let value = func(data);
outimgarr.setel(idxh, idxw, 0, value);
}
}
return outimgarr;
}
/**
* mean,计算均值
* @param {boolean} [ischannel=false] - 当ischannel为True时,沿通道计算均值,否则计算全局均值
* @returns {number|ImgArray} 返回一个数值或一个新的 `ImgArray` 实例,包含均值或均值数组。
*/
mean(ischannel = false) {
if (ischannel) {
let func = (x) => { return x.reduce((prev, curr) => { return prev + curr }) / x.length };
return this.apply_along_channel(func);
}
return this.data.reduce((prev, curr) => { return prev + curr }) / this.data.length;
}
/**
* max,计算最大值,膨胀,最大值池化
* @param {boolean} [ischannel=false] - 当ischannel为True时,沿通道计算最大值,否则计算全局最大值
* @returns {number|ImgArray} 返回一个数值或一个新的 `ImgArray` 实例,包含最大值或最大值数组。
*/
max(ischannel = false) {
if (ischannel) {
let func = (x) => { return Math.max(...x) }
return this.apply_along_channel(func);
}
let maxv = this.data[0];
this.data.forEach((x) => { if (x > maxv) maxv = x });
return maxv;
}
/**
* min,计算最小值,腐蚀,最小值池化
* @param {boolean} [ischannel=false] - 当ischannel为True时,沿通道计算最小值,否则计算全局最小值
* @returns {number|ImgArray} 返回一个数值或一个新的 `ImgArray` 实例,包含最小值或最小值数组。
*/
min(ischannel = false) {
if (ischannel) {
let func = (x) => { return Math.min(...x) }
return this.apply_along_channel(func)
}
let minv = this.data[0];
this.data.forEach((x) => { if (x < minv) minv = x });
return minv;
}
/**
* linearc,线性加权
* @param {number[]} [weights = [1, 2, 3]] - 线性加权系数,是一个一维数组,表示每个通道的权重
* @param {number} [bais=0] - 偏差值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含线性加权后的数据。
*/
linearc(weights = [1, 2, 3], bais = 0) {
//判断weights的长度与通道数相同
let func = (x) => {
let res = 0
for (let i = 0; i < weights.length; i++) {
res += x[i] * weights[i]
}
return res + bais
}
return this.apply_along_channel(func)
}
/**
* conv2d,卷积运算,weights卷积
* @param {number[][]} [weights = [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]] - 卷积核,是一个二维数组,表示卷积核的权重
* @param {number} [bias=0] - 偏差值,默认为0
* @param {number} [fillvalue=0] - 填充的值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含卷积运算后的数据。
*/
conv2d(weights = [[1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9]], bias = 0, fillvalue = 0) {
//卷积运算,只对通道数为1的数组有效
let size = [weights.length, weights[0].length]
let nb = this.neighbor(size, fillvalue)
return nb.linearc(weights.flat(), bias)
}
/**
* median,中值滤波,对通道数为1的数组结果正确
* @param {number[][]} [sizes = [3, 3]] - 邻域大小,是一个二维数组,表示邻域的大小
* @param {number} [fillvalue=0] - 填充的值,默认为0
* @returns {ImgArray} 返回一个新的 `ImgArray` 实例,包含中值滤波后的数据。
*/
median(sizes = [3, 3], fillvalue = 0) {
let tmparr = this.neighbor(sizes, fillvalue)
/**
* 计算数组的中值。
* 对传入的数组进行排序并计算中值。如果数组长度是偶数,则返回中间两个元素的平均值。
* @param {number[]} arr - 需要计算中值的数组。
* @returns {number} 返回中值。
*/
function calcmedian(arr) {
arr.sort((a, b) => a - b);
const mid = Math.floor(arr.length / 2);
if (arr.length % 2 === 0) {
return (arr[mid - 1] + arr[mid]) / 2;
} else {
return arr[mid];
}
}
return tmparr.apply_along_channel(calcmedian);
}
/**
* gaussianBlur,应用高斯模糊。
* 该方法生成一个高斯核,并将其应用于图像数据,实现高斯模糊效果。
* @param {number} [sigma=2] - 高斯核的标准差,控制模糊的程度。默认为 `2`。
* @param {number[]} [kernel_size=[3, 3]] - 高斯核的尺寸,默认为 `[3, 3]`。尺寸应该为奇数。
* @param {number} [fillvalue=0] - 邻域填充的默认值,默认为 `0`。
* @returns {ImgArray} 返回一个新的图像数据对象,应用了高斯模糊。
*/
gaussianBlur(sigma = 2, kernel_size = [3, 3], fillvalue = 0) {
let tmparr = this.neighbor(kernel_size, fillvalue)
/**
* 创建高斯核。
* 根据给定的标准差和核大小生成一个高斯核,并进行归一化处理。
* @param {number} sigma - 高斯核的标准差。
* @param {number[]} kernel_size - 高斯核的尺寸。
* @returns {number[][]} 返回一个归一化的高斯核。
*/
function creategausskernel(sigma, kernel_size) {
let kernel = [];
let center = Math.floor(kernel_size[0] / 2);
for (let i = 0; i < kernel_size[0]; i++) {
let row = [];
for (let j = 0; j < kernel_size[1]; j++) {
let x = i - center;
let y = j - center;
let value = (1 / (2 * Math.PI * sigma * sigma)) * Math.exp(-(x * x + y * y) / (2 * sigma * sigma));
row.push(value);
}
kernel.push(row);
}
// 归一化
let sum = kernel.reduce((a, b) => a.concat(b)).reduce((a, b) => a + b);
for (let i = 0; i < kernel_size[0]; i++) {
for (let j = 0; j < kernel_size[1]; j++) {
kernel[i][j] /= sum;
}
}
return kernel;
}
return tmparr.conv2d(creategausskernel(sigma, kernel_size));
}
/**
* sobelx,对图像应用 Sobel 算子,计算图像在 X 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
sobelx() {
let kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
return this.conv2d(kernel);
}
/**
* sobely,对图像应用 Sobel 算子,计算图像在 Y 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
sobely() {
let kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
return this.conv2d(kernel)
}
/**
* sobelxy,计算图像的梯度幅值,结合 Sobel X 和 Sobel Y 方向的梯度。
* @returns {ImgArray} 返回图像的梯度幅值。
*/
sobelxy() {
return this.sobelx().abs().add(this.sobely().abs());
}
/**
* laplacian,应用拉普拉斯算子对图像进行边缘检测。
* @param {number} [size=4] - 拉普拉斯核的大小,默认为 `4`。
* @returns {ImgArray} 返回一个新的图像数据对象,应用了拉普拉斯算子。
*/
laplacian(size = 4) {
let kernel4 = [[0, -1, 0], [-1, 4, -1], [0, -1, 0]];
let kernel8 = [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]];
let kernel = size == 4 ? kernel4 : kernel8;
return this.conv2d(kernel)
}
/**
* sharrx,对图像应用 Sobel 算子,计算图像在 X 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
sharrx() {
let kernel = [[-3, 0, 3], [-10, 0, 10], [-3, 0, 3]];
return this.conv2d(kernel);
}
/**
* sharry,对图像应用 Sobel 算子,计算图像在 Y 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
sharry() {
let kernel = [[-3, -10, -3], [0, 0, 0], [3, 10, 3]];
return this.conv2d(kernel);
}
/**
* sharrxy,计算图像的梯度幅值,通过结合 Sobel X 和 Sobel Y 方向的梯度计算结果。
* @returns {ImgArray} 返回图像的梯度幅值。
*/
sharrxy() {
return this.sharrx().abs().add(this.sharry().abs());
}
/**
* prewittx,对图像应用 Prewitt 算子,计算图像在 X 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
prewittx() {
let kernel = [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]];
return this.conv2d(kernel);
}
/**
* prewitty,对图像应用 Prewitt 算子,计算图像在 Y 方向上的梯度。
* @returns {ImgArray} 返回计算后的图像数据对象。
*/
prewitty() {
let kernel = [[-1, -1, -1], [0, 0, 0], [1, 1, 1]];
return this.conv2d(kernel)
}
/**
* prewittxy,计算图像的梯度幅值,结合 Prewitt X 和 Prewitt Y 方向的梯度。
* @returns {ImgArray} 返回图像的梯度幅值。
*/
prewittxy() {
return this.prewittx().abs().add(this.prewitty().abs());
}
//canny算子,有问题
/**
* canny,应用 Canny 算子。对图像进行边缘检测
* @returns {ImgArray} 返回一个新的图像数据对象,应用了 Canny 算子。
*/
canny() {
let tmparr = this.structure([[1, 1, 1], [1, -7, 1], [1, 1, 1]], 0)
return tmparr.conv2d(creategausskernel(1, 3))
}
/**
* maxpooling,应用最大值池化对图像进行降采样。
* @param {number[]} [size=[3, 3]] - 池化核的大小,默认为 `[3, 3]`。
* @param {number} [fillvalue=0] - 填充值,默认为 `0`。
* @returns {ImgArray} 返回一个新的图像数据对象,应用了最大值池化。
*/
maxpooling(size = [3, 3], fillvalue = 0) {
//最大值池化
let tmparr = this.neighbor(size, fillvalue);
return tmparr.max(true);
}
/**
* minpooling,应用最小值池化对图像进行降采样。
* @param {number[]} [size=[3, 3]] - 池化核的大小,默认为 `[3, 3]`。
* @param {number} [fillvalue=0] - 填充值,默认为 `0`。
* @returns {ImgArray} 返回一个新的图像数据对象,应用了最小值池化。
*/
minpooling(size = [3, 3], fillvalue = 0) {
//最小值池化
let tmparr = this.neighbor(size, fillvalue);
return tmparr.min(true);
}
/**
* avgpooling,应用平均值池化对图像进行降采样。
* @param {number[]} [size=[3, 3]] - 池化核的大小,默认为 `[3, 3]`。
* @param {number} [fillvalue=0] - 填充值,默认为 `0`。
* @returns {ImgArray} 返回一个新的图像数据对象,应用了平均值池化。
*/
avgpooling(size = [3, 3], fillvalue = 0) {
//平均池化(均值滤波)
let tmparr = this.neighbor(size, fillvalue);
return tmparr.mean(true);
}
/**
* dilate,膨胀操作,使用给定的结构元素对图像进行膨胀。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 膨胀操作的结构元素,通常是一个矩阵,表示膨胀的形状和大小。默认为 3x3 的邻域。
* @param {number} [filfillvalue = 0] - 膨胀操作中填充区域的值,默认为 0。
* @returns {ImgArray} 返回膨胀后的图像数据对象。
*/
dilate(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
let tmparr = this.structure(pattern, fillvalue);
return tmparr.max(true);
}
/**
* erode,腐蚀操作,使用给定的结构元素对图像进行腐蚀。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 腐蚀操作的结构元素,通常是一个矩阵,表示腐蚀的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 腐蚀操作中填充区域的值,默认为 0。
* @returns {ImgArray} 返回腐蚀后的图像数据对象。
*/
erode(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
let tmparr = this.structure(pattern, fillvalue);
return tmparr.min(true);
}
/**
* mgradient,形态学梯度操作,使用给定的结构元素对图像进行形态学梯度操作。
* @param {number[][]} [sizes = [3, 3]] - 形态学梯度操作的结构元素,通常是一个矩阵,表示形态学梯度的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 形态学梯度操作中填充区域的值,默认为 0。
* @returns {ImgArray} 返回形态学梯度后的图像数据对象。
*/
mgradient(sizes = [3, 3], fillvalue = 0) {
let tmparr = this.neighbor(sizes, fillvalue);
return tmparr.max(true).sub(tmparr.min(true));
}
/**
* open,开运算操作,使用给定的结构元素对图像进行开运算。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 开运算的结构元素,通常是一个矩阵,表示开运算的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 开运算中填充区域的值,默认为 0。
* @returns {ImgArray} 返回开运算后的图像数据对象。
*/
open(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
return this.erode(pattern, fillvalue).dilate(pattern, fillvalue);
}
/**
* close,闭运算操作,使用给定的结构元素对图像进行闭运算。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 闭运算的结构元素,通常是一个矩阵,表示闭运算的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 闭运算中填充区域的值,默认为 0。
* @returns {ImgArray} 返回闭运算后的图像数据对象。
*/
close(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
return this.dilate(pattern, fillvalue).erode(pattern, fillvalue);
}
/**
* tophat,顶帽运算,使用给定的结构元素对图像进行顶帽运算。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 顶帽运算的结构元素,通常是一个矩阵,表示顶帽运算的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 顶帽运算中填充区域的值,默认为 0。
* @returns {ImgArray} 返回顶帽运算后的图像数据对象。
*/
tophat(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
return this.sub(this.open(pattern, fillvalue));
}
/**
* blackhat,黑帽运算,使用给定的结构元素对图像进行黑帽运算。
* @param {number[][]} [pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]]] - 黑帽运算的结构元素,通常是一个矩阵,表示黑帽运算的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 黑帽运算中填充区域的值,默认为
* @returns {ImgArray} 返回黑帽运算后的图像数据对象。
*/
blackhat(pattern = [[1, 1, 1], [0, 1, 0], [1, 1, 1]], fillvalue = 0) {
return this.close(pattern, fillvalue).sub(this);
}
/**
* adaptiveThreshold,自适应阈值分割,使用给定的结构元素对图像进行自适应阈值分割。
* @param {number[][]} [sizes = [3, 3]] - 自适应阈值分割的结构元素,通常是一个矩阵,表示自适应阈值分割的形状和大小。默认为 3x3 的邻域。
* @param {number} [fillvalue = 0] - 自适应阈值分割中填充区域的值,默认为 0。
* @param {number} [constant = 0] - 自适应阈值分割的常数,默认为 0。
* @returns {ImgArray} 返回自适应阈值分割后的图像数据对象。
*/
adaptiveThreshold(sizes = [3, 3], fillvalue = 0, constant = 0) {
//自适应阈值分割,后续讨论,有问题
let tmparr = this.neighbor(sizes, fillvalue);
let res = new Tensor(this.width, this.height);
for (let i = 0; i < this.width; i++) {
for (let j = 0; j < this.height; j++) {
let sum = 0;
for (let m = 0; m < sizes[0]; m++) {
for (let n = 0; n < sizes[1]; n++) {
sum += tmparr.get(m, n);
}
}
let mean = sum / (sizes[0] * sizes[1]);
res.set(i, j, this.get(i, j) > mean - constant ? 1 : 0);
}
}
return res;
}
/**
* lbp,计算经典的局部二值模式(LBP)特征。
* LBP 用于描述图像的局部纹理特征,常用于图像分类和面部识别等任务。
* @returns {ImgArray} 返回包含 LBP 特征的图像数据对象。
*/
lbp() {
let tmparr = this.neighbor([3, 3], 0);
/**
* 计算 LBP 特征的函数
* @param {number[]} x - 包含 9 个元素的数组,表示 3x3 邻域的像素值。
* @returns {number} 返回计算后的 LBP 特征值(十进制)。
*/
function calclbp(x) {
let tmpnew = 0;
let weights = [128, 64, 32, 1, 0, 16, 2, 4, 8];
for (let i = 0; i < 9; i++) {
if (x[i] > x[4]) {
tmpnew += weights[i];
}
}
return tmpnew;
}
return tmparr.apply_along_channel(calclbp);
}
/**
* lmi,计算 LMI 特征。
* LMI 用于描述图像的局部纹理特征,常用于图像分类和面部识别等任务。
* @param {number[][]} [sizes = [3, 3]] - LMI 特征的邻域大小,默认为 3x3 的邻域。
* @returns {ImgArray} 返回包含 LMI 特征的图像数据对象。
*/
lmi(sizes = [3, 3]) {
let tmparr = this.neighbor(sizes, 0);
let centerpos = Math.floor(tmparr.shape.channel / 2);
/**
* 计算 LMI 特征的函数
* @param {number[]} x - 表示邻域的像素值。
* @returns {number} 返回计算后的 LMI 特征值(十进制)。
*/
function calclmi(x) {
let tmpnew = 0;
let centvalue = x[centerpos];
for (let i = 0; i < x.length; i++) {
if (x[i] < centvalue) {
tmpnew += 1;
}
}
return tmpnew;
}
return tmparr.apply_along_channel(calclmi);
}
/**
* cellsim,计算元胞自动机(Cellular Automaton)特征。
* 元胞自动机是一种描述自然界中复杂规则的数学模型,常用于图像处理和计算机视觉等应用。
* @param {number[][]} [sizes = [3, 3]] - 元胞自动机特征的邻域大小,默认为 3x3 的邻域。
* @returns {ImgArray} 返回包含元胞自动机特征的图像数据对象。
*/
cellsim(sizes = [3, 3]) {
//元胞自动机,生命游戏
// 生命游戏规则:生命游戏是一种著名的二维元胞自动机,其规则非常简单,每个元胞根据其周围八个相邻元胞的状态来改变自己的状态,规则定义如下:
// 如果一个元胞的状态为死亡(0),且周围有三个元胞的状态为存活(1),则该元胞下一个状态为存活(1);
// 如果一个元胞的状态为存活(1),且周围有两个或三个元胞的状态也为存活(1),则该元胞下一个状态仍为存活(1);
// 其他情况下,该元胞下一个状态为死亡(0)。
let tmparr = this.neighbor(sizes, 0);
function calcell(x) {
let tmpnew = 0;
let centerpos = Math.floor(x.length / 2);
for (let i = 0; i < x.length; i++) {
if (x[i] >= 0.5) {
tmpnew += 1;
}
}
if (x[centerpos] >= 0.5) {
if (tmpnew == 4 || tmpnew == 3)
return 1;
else return 0;
} else {
if (tmpnew == 3)
return 1;
else return 0;
}
}
return tmparr.apply_along_channel(calcell);
}
/**
* copy,复制当前图像数据对象,创建一个新副本。
* @returns {ImgArray} 当前图像数据的副本。
*/
copy() {
let newarr = this.empty(true);
newarr.data = this.data.slice();
return newarr;
}
/**
* empty,创建一个与当前图像数据对象相同尺寸的空数组,考虑了ImgArray和Img实例。
* @param {boolean} [lazy = false] - 是否启用懒加载,默认为 false。
* @returns {ImgArray} 创建的空数组。
*/
empty(lazy = false) {
if (this.constructor.name == "ImgArray") {
return new ImgArray({
height: this.height,
width: this.width,
channel: this.channel,
lazy,
dtype: this.dtype
})
}
else {
return new Img({
height: this.height,
width: this.width,
lazy
})
}
}
/**
* meshgrid,创建一个规则格网,用于在图像处理中生成网格特征。
* @static
* @param {number[]} [hrange = [0, 256]] - 格网在垂直方向的范围,默认为 [0, 256]。
* @param {number[]} [wrange = [0, 256]] - 格网在水平方向的范围,默认为 [0, 256]。
* @returns {ImgArray[]} 返回一个包含两个 ImgArray 对象的数组,其中第一个数组表示垂直方向的格网,第二个数组表示水平方向的格网。
*/
static meshgrid(hrange = [0, 256], wrange = [0, 256]) {
//产生规则格网
let width = wrange[1] - wrange[0]
let height = hrange[1] - hrange[0]
let ximgarr = new ImgArray({ height, width, channel: 1 })
let yimgarr = new ImgArray({ height, width, channel: 1 })
for (let hidx = 0; hidx < height; hidx++) {
for (let widx = 0; widx < width; widx++) {
ximgarr.setel(hidx, widx, 0, widx + wrange[0]);
yimgarr.setel(hidx, widx, 0, hidx + hrange[0]);
}
}
return [yimgarr, ximgarr];
}
/**
* random,创建一个指定尺寸的,指定范围的均值分布的数组。
* @static
* @param {Object} options - 创建数组的选项。
* @param {number} [options.height=256] - 数组的高度,默认为 256。
* @param {number} [options.width=256] - 数组的宽度,默认为 256。
* @param {number} [options.channel=3] - 数组的通道数,默认为 3。
* @param {number} [options.vmin=0] - 数组元素的最小值,默认为 0。
* @param {number} [options.vmax=255] - 数组元素的最大值,默认为 255。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示创建的随机数组。
*/
static random({ height = 256, width = 256, channel = 3, vmin = 0, vmax = 255 } = {}) {
let newimgarr = new ImgArray({ height, width, channel, lazy: false });
newimgarr.data = newimgarr.data.map(x => Math.random() * (vmax - vmin) + vmin);
return newimgarr;
}
/**
* zeros,创建一个指定尺寸的零数组。
* @static
* @param {Object} options - 创建数组的选项。
* @param {number} [options.height=256] - 数组的高度,默认为 256。
* @param {number} [options.width=256] - 数组的宽度,默认为 256。
* @param {number} [options.channel=3] - 数组的通道数,默认为 3。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示创建的零数组。
*/
static zeros({ height = 256, width = 256, channel = 3 } = {}) {
return new ImgArray({ height, width, channel, lazy: false });
}
/**
* ones,创建一个指定尺寸的 ones 数组。
* @static
* @param {Object} options - 创建数组的选项。
* @param {number} [options.height=256] - 数组的高度,默认为 256。
* @param {number} [options.width=256] - 数组的宽度,默认为 256。
* @param {number} [options.channel=3] - 数组的通道数,默认为 3。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示创建的 ones 数组。
*/
static ones({ height = 256, width = 256, channel = 3 } = {}) {
return new ImgArray({ height, width, channel, lazy: false }).fill(1);
}
/**
* full,创建一个指定尺寸的指定值数组。
* @static
* @param {Object} options - 创建数组的选项。
* @param {number} [options.height=256] - 数组的高度,默认为 256。
* @param {number} [options.width=256] - 数组的宽度,默认为 256。
* @param {number} [options.channel=3] - 数组的通道数,默认为 3。
* @param {number} [options.value=0] - 数组元素的值,默认为 0。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示创建的指定值数组。
*/
static full({ height = 256, width = 256, channel = 3 } = {}, value = 0) {
return new ImgArray({ height, width, channel, lazy: false }).fill(value);
}
/**
* fromBuffer,从一个 ArrayBuffer 创建一个 ImgArray。
* @static
* @param {ArrayBuffer} arraybuffer - 用于创建 ImgArray 的 ArrayBuffer。
* @param {ImgArray} shape - 创建 ImgArray 的形状。
* @param {string} [dtype='float32'] - 创建 ImgArray 的数据类型,默认为 'float32'。
* @returns {ImgArray} 返回一个ImgArray 对象,表示从 ArrayBuffer 创建的 ImgArray
* @throws {Error} 如果 dtype 不是 ImgArray 支持的数据类型,则抛出异常。
* @throws {Error} 如果 ArrayBuffer 的长度与 shape 不匹配,则抛出异常。
*/
static fromBuffer(arraybuffer, shape, dtype = 'float32') {
// arraybuffer创建ImgArray,假设buffer的排列方向是hwc,将buffer根据dtype转换为typedarray
// 参照random函数,检查转换后的和shape个数一致
let { height, width, channel } = shape;
let type = ImgArray.dtypes[dtype];
if (type == undefined) {
throw new Error(`dtype ${dtype} is not supported, supported dtypes are ${ImgArray.dtypenames}`);
}
let bytesize = height * width * channel * type.BYTES_PER_ELEMENT;
// 确保转换后的数组长度与期望的shape一致
if (bytesize !== arraybuffer.byteLength) {
throw new Error('长度不匹配');
}
let typedArray = new type(arraybuffer.slice()); //对arraybuffer拷贝
// 创建ImgArray实例,并将数据赋值
let newimgarr = new ImgArray({ height, width, channel, lazy: true });
newimgarr.data = typedArray;
return newimgarr;
}
/**
* buffer,获取 ImgArray 的 ArrayBuffer。
* @type {ArrayBuffer}
*/
get buffer() {
return this.data.buffer;
}
/**
* typedarray,获取 ImgArray 的 TypedArray。
* @type {TypedArray}
*/
get typedarray() {
return this.data;
}
/**
* fromArray,从一个数组创建一个 ImgArray。
* @static
* @param {Array} arr - 用于创建 ImgArray 的数组。
* @param {ImgArray} shape - 创建 ImgArray 的形状。
* @param {string} [dtype='float32'] - 创建 ImgArray 的数据类型,默认为 'float32'。
* @returns {ImgArray} 返回一个ImgArray 对象,表示从数组创建的 ImgArray
* @throws {Error} 如果数组的类型与 ImgArray 的类型不匹配,则抛出异常。
*/
static fromArray(arr, shape, dtype = 'float32') {
//从数组创建ImgArray,arr的类型typedarray或array,检查个数、长度、类型是否一致,多维array的话flatten为一维
if (!Array.isArray(arr) && !ArrayBuffer.isView(arr)) {
throw new Error('arr输入类型必须是:array or TypedArray');
}
let flatArray;
if (Array.isArray(arr)) {
// 展开数组
flatArray = arr.flat(Infinity); // 当 depth 设置为 Infinity 时,会将数组的所有嵌套层级全部展开
} else {
// 转换为普通数组
flatArray = arr;
}
let { height, width, channel } = shape;
if (flatArray.length !== height * width * channel) {
throw new Error('长度不匹配');
}
let newimgarr = new ImgArray({ height, width, channel, lazy: true });
newimgarr.data = new ImgArray.dtypes[dtype](flatArray);
return newimgarr;
}
/**
* dist,计算数组在通道方向与一个向量的欧氏距离。
* @param {number[]} [v2 = [3, 4, 5]] - 用于计算距离的向量。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示计算得到的距离数组。
*/
dist(v2 = [3, 4, 5]) {
//计算数组在通道方向与一个向量的欧氏距离
if (v2.length != this.channel) {
console.error('数组长度与通道数不匹配。');
return;
}
let func = (v1) => {
let res = 0
for (let i = 0; i < v2.length; i++) {
res += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
return Math.sqrt(res);
}
return this.apply_along_channel(func)
}
/**
* cossimdist,计算数组在通道方向与一个向量的余弦距离。
* @param {number[]} [v2 = [3, 4, 5]] - 用于计算距离的向量。
* @returns {ImgArray} 返回一个 ImgArray 对象,表示计算得到的距离数组。
*/
cossimdist(v2 = [3, 4, 5]) {
//计算数组在通道方向与一个向量的余弦距离
if (v2.length != this.channel) {
console.error('数组长度与通道数不匹配。');
return;
}
let l2 = Math.hypot(...v2)
let func = (v1) => {
let res = 0
for (let i = 0; i < v2.length; i++) {
res += v1[i] * v2[i]
}
return 1 - res / (l2 * Math.hypot(...v1) + 0.0000001)
}
return this.apply_along_channel(func)
}
/**
* kmeans,使用 K 均值聚类算法对图像数据进行聚类。
* @param {number} k - 聚类的数量。
* @param {Array<Array<number>> | null} centers - 初始聚类中心,是一个二维数组(kxchannel)。如果为 `null`,程序将随机选择初始中心。
* @param {string} [disttype='dist'] - 距离度量方式,可选值为 'dist'(欧氏距离)或 'cossimdist'(余弦相似度)。
* @returns {Array} 返回两个值:
* - `newcenters`:更新后的聚类中心。
* - `clusterresult`:一个 ImgArray 对象,表示每个像素的聚类结果。
*/
kmeans(k = 4, centers = null, disttype = 'dist') {
//k均值聚类
//k表示聚类数量,
//centers是一个kxchannel的二维列表构成的数组,表示k个聚类中心,当为null时程序自动随机选择
//disttype是距离度量方式可以为dist或者是cossimdist,
//初始化聚类中心
if (centers === null) {
centers = []
for (let i = 0; i < k; i++) {
let tmpx = parseInt(Math.random() * this.width)
let tmpy = parseInt(Math.random() * this.height)
let idx = this.idxtoelidx(tmpy, tmpx, 0)
let v2 = this.data.slice(idx, idx + this.channel)
v2 = Array.from(v2)
centers.push(v2)
}
}
else {
k = centers.length
}
//对聚类中心按照特征值排一下序,保证不同的初始情况具有相似的聚类效果
centers.sort((a, b) => Math.max(...a) - Math.max(...b))
//计算像素到各中心的距离
let dists = []
for (let i = 0; i < k; i++) {
let tmpdist = disttype == 'dist' ? this.dist(centers[i]) : this.cossimdist(centers[i])
dists.push(tmpdist)
}
//计算聚类结果
let clusternum = Array(k).fill(0)
let newcenters = Array(k)
for (let i = 0; i < k; i++) {
newcenters[i] = new Array(this.channel).fill(0);
}
let clusterresult = new ImgArray({ height: this.height, width: this.width, channel: 1, lazy: false, dtype: 'cuint8' })
for (let i = 0; i < dists[0].data.length; i++) {
let minv = Infinity;
let minidx = 0;
for (let idx = 0; idx < dists.length; idx++) {
if (dists[idx].data[i] < minv) {
minv = dists[idx].data[i]
minidx = idx
}
}
//console.log(minv,minidx)
clusterresult.data[i] = minidx
clusternum[minidx] += 1 //类别计数器加1
let [h, w, c] = dists[0].elidxtoidx(i)
let idx = this.idxtoelidx(h, w, 0)
let v2 = this.data.slice(idx, idx + this.channel)
for (let idx = 0; idx < this.channel; idx++) {
newcenters[minidx][idx] = v2[idx] + newcenters[minidx][idx]
}
}
//更新聚类中心
for (let idx = 0; idx < centers.length; idx++) {
for (let i = 0; i < this.channel; i++) {
newcenters[idx][i] = newcenters[idx][i] / clusternum[idx];
}
}
return [newcenters, clusterresult]
}
/**
* totensor,将数组转为onnxruntime中的张量。
* @param {string} [dtype='float32'] - 张量数据类型,可以是 'float32' 或 'uint8'。
* @returns {ort.Tensor} 返回一个张量对象。
*/
totensor(dtype = 'float32') {
if (typeof ort == 'undefined') console.error('需要加载onnnxruntime.js')
if (dtype == 'float32')
return new ort.Tensor(dtype, this.data.slice(), this.shape)
if (dtype == 'uint8') {
let imgardata = new Uint8Array(this.data)
return new ort.Tensor(dtype, imgardata, this.shape)
}
}
/**
* fromtensor,将onnxruntime中的张量转为数组。
* @static
* @param {ort.Tensor} tensor - 张量对象。
* @returns {ImgArray} 返回一个 ImgArray 对象。
*/
static fromtensor(tensor) {
let dim = tensor.dims.length;
let arr = null;
if (dim == 3)
arr = new ImgArray(t.dims[0], t.dims[1], t.dims[2]);
else
arr = new ImgArray(t.dims[0], t.dims[1], 1);
arr.data = new Float32Array(t.data);
return arr;
}
/**
* hwc2chw,用于将hxwxc转为cxhxw的方法,返回一个排列为cxhxw的一维float32数组
* @returns {ImgArray} 返回一个形状为 CHW 格式的 ImgArray 对象。
*/
hwc2chw() {
let chs = this.dsplit();
let res = new ImgArray({ height: this.channel, width: this.height, channel: this.width, dtype: this.dtype });
for (let i = 0; i < chs.length; i++) {
res.data.set(chs[i].data, i * chs[i].data.length);
}
return res;
}
/**
* chw2hwc,将图像或数组的形状从 CHW 转换为 HWC 格式。
* hwc2chw的逆操作,hwc2chw和chw2hwc都是transpose的简单化
* @returns {ImgArray} 返回一个形状为 HWC 格式的 ImgArray 对象。
*/
chw2hwc() {
let { height, width, channel } = this.shape;
let [c, h, w] = [height, width, channel];
let baselen = h * w;
let res = new ImgArray({ height: h, width: w, channel: c, dtype: this.dtype });
let offsets = [];
for (let i = 0; i < c; i++) {
offsets.push(i * h * w);
}
for (let i = 0; i < baselen; i++) {
let idx = i * c;
for (let j = 0; j < c; j++) {
let oidx = offsets[j] + i;
res.data[idx + j] = this.data[oidx];
}
}
return res;
}
/**
* astype,将数组的元素类型转换为指定的类型,要注意不同类型数值表示范围差异可能带来的精度损失!
* @param {string} [dtype='float32'] - 要转换的元素类型,变量{@link ImgArray.dtypenames}里的所有类型,如 'float32' , 'uint8'等。
* @returns {ImgArray} 返回一个具有指定元素类型的 ImgArray 对象。
*/
astype(dtype = 'float32') {
if (!ImgArray.dtypes[dtype]) {
console.error('不支持的dtype类型');
return;
}
let res = this.empty(true);
res.data = new ImgArray.dtypes[dtype](this.data);
res.dtype = dtype;
return res;
}
/**
* show,显示数组的内容,当数组为图像时,直接在网页中显示图像,方便观察。
* 与matploblib的imshow很相似
* @param {Object} options - 可选参数对象。
* @param {number} [options.vmin=0] - 数组的最小值。
* @param {number} [options.vmax=255] - 数组的最大值。
* @param {number} [options.cas=null] - 可选的画布对象。
* @returns {Img} 返回显示的图像对象。
*/
show({ vmin = 0, vmax = 255, cas = null } = {}) {
if ([1, 3, 4].includes(this.channel)) {
let data = this.span(vmin, vmax, 0, 255);
let img = Img.fromarray(data);
img.show(cas);
return img; //返回显示的图像对象,从而可以调用其方法
}
else {
console.error('array channel is not correct, channel should be 1(gray),3(RGB) or 4(RGBA)')
console.error('数组的通道数不正确,当通道数是1(灰度), 3(RGB彩色), 或 4(带有透明通道的彩色图像)时才可以显示为图像!')
}
}
/**
* toString,参照Numpy的方式将数组转为字符显示,对于大数组的显示存在问题。
* @returns {string} 数组的字符串表示
*/
toString() {
let str = `shape: [${this.height}, ${this.width}, ${this.channel}]\ndtype: ${this.dtype}\ndata :\n[`;
for (let h = 0; h < this.height; h++) {
let block = h ? ' [' : '[';
for (let w = 0; w < this.width; w++) {
let line = w ? ' [' : '[';
for (let c = 0; c < this.channel; c++) {
let v = this.getel(h, w, c);
line += v.toFixed(2) + ' ';
}
if (w < this.width - 1)
block += line + ']\n';
else
block += line + ']';
}
if (h < this.height - 1)
str += block + ']\n\n';
else
str += block + ']';
}
return str + ']';
}
/**
* 在控制台以格式化的字符串打印数组。
*/
print() {
console.log(this.toString())
}
/**
* tofile,将数组保存为文件。*实验性质的方法,先解决有没有,后续根据需要再完善*
* @param {string} [name='download'] - 文件名。
* @param {string} [type='txt'] - 文件类型,可以是 'txt' 或 'bin'。
*/
tofile(name = 'download', type = 'bin') {
let buffer;
if (type === 'txt') {
let encoder = new TextEncoder();
buffer = encoder.encode(this.data.toString());
} else if (type === 'bin') {
buffer = this.buffer;
} else {
console.error("Unsupported type specified");
return;
}
let blob = new Blob([buffer]);
let url = URL.createObjectURL(blob);
const save_link = document.createElement("a");
save_link.href = url;
save_link.download = `${name}.${type}`;
save_link.dataset.downloadurl = [type === 'txt' ? 'text/plain' : 'application/octet-stream', save_link.download, url].join(':');
save_link.click();
// 释放URL对象
URL.revokeObjectURL(url);
}
}
/**
* ImgArray类是一个用于表示图像的类,继承自ImgArray类。
* Img 继承自ImgArray类,虽然支持ImgArray的所有方法,但是不保证运行正确,
* 更主要的是用于图像的存取和简单变换,Img 本身就是图像
* Img的图像类型只支持RGBA排列一种
* @extends ImgArray
*/
class Img extends ImgArray {
/**
* 创建一个Img对象。
* @param {Object} options - 可选参数对象。
* @param {number} [options.height=256] - 图像的高度。
* @param {number} [options.width=256] - 图像的宽度。
* @param {boolean} [options.lazy=false] - 是否延迟创建图像数据。
* @returns {Img} 返回一个Img对象。
*/
constructor({ height = 256, width = 256, lazy = false } = {}) {
//调用父类ImgArray完成初始化
super({ height, width, channel: 4, lazy, dtype: 'cuint8' });
//创建一个保存图像的canvas
let cas = document.createElement('canvas');
this.cas = cas;
this.cas.height = height;
this.cas.width = width;
this.ctx = cas.getContext('2d');
this.iscasnew = false; //用于标记图像数据和画布哪个新,要用新的去同步旧的
}
/**
* update,更新图像数据到画布中,根据标志位进行数据和canvas同步。
*/
update() {
if (this.iscasnew) {
let imgdata = this.ctx.getImageData(0, 0, this.cas.width, this.cas.height);
this.data = imgdata.data.slice();
this.iscasnew = false;
}
else {
let imgdata = new ImageData(this.data, this.width, this.height)
this.ctx.putImageData(imgdata, 0, 0)
}
}
/**
* fromarray,将 ImgArray 对象转换为 Img 对象。
* 该方法支持灰度图 (channel = 1)、RGB 图 (channel = 3)、RGBA 图 (channel = 4) 的转换。
* @static
* @param {ImgArray} imgarray - 包含图像数据的 ImgArray 对象。
* @param {number} imgarray.height - 图像的高度。
* @param {number} imgarray.width - 图像的宽度。
* @param {number} imgarray.channel - 图像的通道数:
* - 1 表示灰度图。
* - 3 表示 RGB 图。
* - 4 表示 RGBA 图。
* @param {Uint8Array} imgarray.data - 一维数组,存储图像的像素值。
* - 若 `channel = 1`,数组长度为 height * width。
* - 若 `channel = 3`,数组长度为 height * width * 3。
* - 若 `channel = 4`,数组长度为 height * width * 4。
* @returns {Img|null} - 转换后的 Img 对象(包含 RGBA 格式的像素值),或在不支持的通道数时返回 `null`。
* @throws {Error} 如果 `imgarray.channel` 不是 1、3 或 4,则打印错误信息。
*/
static fromarray(imgarray) {
let img = null;
let pixelnum = imgarray.data.length / imgarray.channel;
if (imgarray.channel == 1) {
img = new Img({ height: imgarray.height, width: imgarray.width });
for (let i = 0; i < pixelnum; i += 1) {
img.data[i * 4] = imgarray.data[i];
img.data[i * 4 + 1] = imgarray.data[i];
img.data[i * 4 + 2] = imgarray.data[i];
img.data[i * 4 + 3] = 255;//不透明
}
} else if (imgarray.channel == 3) {
img = new Img({ height: imgarray.height, width: imgarray.width });
for (let i = 0; i < pixelnum; i += 1) {
img.data[i * 4] = imgarray.data[i * 3];
img.data[i * 4 + 1] = imgarray.data[i * 3 + 1];
img.data[i * 4 + 2] = imgarray.data[i * 3 + 2];
img.data[i * 4 + 3] = 255;
}
} else if (imgarray.channel == 4) {
img = new Img({ height: imgarray.height, width: imgarray.width });
img.data.set(imgarray.data);
} else {
console.error("不支持的通道数:" + imgarray.channel);
}
return img;
}
/**
* fromcanvas,从canvas元素生成图像。
* @static
* @param {HTMLCanvasElement} canvasele - canvas元素。
* @returns {Img} - 生成的图像对象。
*/
static fromcanvas(canvasele) {
let ctx = canvasele.getContext("2d");
let imgdata = ctx.getImageData(0, 0, canvasele.width, canvasele.height);
let oimg = new Img({ height: imgdata.height, width: imgdata.width, lazy: true });
oimg.data = imgdata.data.slice(); //.slice() 复制数据
return oimg
}
/**
* fromimg,从图像元素生成图像。
* @static
* @param {HTMLImageElement} imgele - 图像元素。
* @returns {Img} - 生成的图像对象。
*/
static fromimg(imgele) {
if (!imgele.width) {
console.error('no image');
return null;
}
let width = imgele.width
let height = imgele.height
let img = new Img({ width, height, lazy: true })
img.ctx.drawImage(imgele, 0, 0, img.width, img.height);
img.iscasnew = true;
img.update()
return img
}
/**
* fromurl,从图像链接生成图像。
* @static
* @param {string} imgurl - 图像链接。
* @returns {Promise<Img>} - 生成的图像对象,需要使用await。
*/
static fromurl(imgurl) {
return new Promise(function (r, e) {
let img = new Image();
img.src = imgurl;
img.setAttribute('crossOrigin', '');//解决网络图片跨域的问题
img.onload = (ex) => {
r(Img.fromimg(img));
}
img.onerror = (ex) => { return e(ex) }
})
}
/**
* fromblob,从blob对象生成图像。
* @static
* @param {Blob} blob - blob对象。
* @returns {Promise<Img>} - 生成的图像对象。
*/
static fromblob(blob) {
//返回是个promise对象,需要使用await
//从编码的图像中生成图像,即将blob对象转为Image
//测试代码:
//fetch('/a.jpg').then(x=>{ return x.blob()}).then(b=>{return Img.fromblob(b)}).then(img=>{img.tocanvas(cas)})
let src = URL.createObjectURL(blob)
return this.fromurl(src)
}
/**
* fromfile,从本地文件生成图像。
* @static
* @param {File} fileobj - 本地文件对象。
* @returns {Promise<Img>} - 生成的图像对象。
*/
static fromfile(fileobj) {
//从本地文件生成图像
/*
使用方法
html:
<input type="file" id="fileInput">
js:
var finput=document.querySelector('#fileInput')
finput.addEventListener('change',()=>{
console.log(finput.files)
let fileobj=finput.files[0]
console.log(fileobj)
Img.fromfile(fileobj).then((img)=>{
img.tocanvas(cas)})
})
*/
return new Promise(function (r, e) {
const reader = new FileReader()
// console.log(fileobj.type)
if (!fileobj.type.startsWith('image')) return e('not image')
reader.readAsDataURL(fileobj)
reader.onload = () => {
let url = reader.result
r(Img.fromurl(url))
}
})
}
/**
* fromvideo,从视频流生成图像。
* @static
* @param {HTMLVideoElement} videoele - 视频元素。
* @param {number} [h] - 输出图像的高度。
* @param {number} [w] - 输出图像的宽度。
* @returns {Img} - 生成的图像对象。
*/
static fromvideo(videoele, h = null, w = null) {
//从video的视频流中截取图像
let vcas = document.createElement('canvas');
vcas.width = w ? w : videoele.videoWidth;
vcas.height = h ? h : videoele.videoHeight;
let context = vcas.getContext('2d');
context.drawImage(videoele, 0, 0);
return Img.fromcanvas(vcas);
}
/**
* videotocanvas,在canvas element 上显示画布。
* @static
* @param {HTMLCanvasElement} canvasel - canvas元素。
*/
static videotocanvas(canvasel) {
//在canvas element 上显示画布
let context = canvasel.getContext('2d');
navigator.mediaDevices.getUserMedia({ video: true })
.then(function (stream) {
var video = document.createElement('video');
video.srcObject = stream;
video.play();
video.addEventListener('loadedmetadata', function () {
canvasel.width = video.videoWidth;
canvasel.height = video.videoHeight;
});
function drawFrame() {
context.drawImage(video, 0, 0);
requestAnimationFrame(drawFrame);
}
requestAnimationFrame(drawFrame);
})
.catch(function (error) {
console.log('无法获取视频流: ', error);
});
}
/**
* fromcamera,从摄像头头获取一幅图像。
* @static
* @param {number} [h] - 输出图像的高度。
* @param {number} [w] - 输出图像的宽度。
* @returns {Promise<Img>} - 生成的图像对象。
*/
static fromcamera(h = null, w = null) {
//从摄像头头获取一幅图像
//返回是个promise对象,需要使用await
return new Promise(function (r, e) {
try {
navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
let video = document.createElement('video');
video.srcObject = stream;
video.play();
video.addEventListener('loadeddata', (e) => {
r(Img.fromvideo(video, h, w))
})
});
}
catch (error) {
e('打开相机失败!');
}
})
}
/**
* tofile,将图像保存为文件。
* @param {string} [name = 'download'] - 文件名。
*/
tofile(name = 'download') {
//将图像以指定名称保存,只支持png格式
const save_link = document.createElement("a");
save_link.href = this.tourl();
save_link.download = name + '.png';
save_link.dataset.downloadurl = ["image/png", save_link.download, save_link.href].join(':');
save_link.click();
}
/**
* tourl,将图像转为url。
* @returns {string} - url
*/
tourl() {
//将图像转为url
if (!this.iscasnew) this.update();
return this.cas.toDataURL('image/png', 1);//图像不会被压缩
}
/**
* toimg,将图像转为img标签元素在网页中展示出来。
* @param {HTMLImageElement} imgele - img标签元素。
*/
toimg(imgele) {
//将Img转换为img标签元素在网页中展示出来
let data = this.tourl()
imgele.src = data
}
/**
* tocanvas,将图像转为canvas标签元素在网页中展示出来。
* @param {HTMLCanvasElement} canvasele - canvas标签元素。
* @param {boolean} [isfull = false] - 是否使canvas适配图像。
*/
tocanvas(canvasele, isfull = false) {
if (this.iscasnew) this.update();
//将Img转换为画布元素在网页中展示出来,需要canvasele的宽高与图像一致
//需要使canvas适配图像的话将isfull置true
if (isfull) {
canvasele.width = this.width;
canvasele.height = this.height;
}
let ctx = canvasele.getContext("2d");
let imgdata = new ImageData(this.data, this.width, this.height)
ctx.putImageData(imgdata, 0, 0)
}
/**
* toarray,将图像转为数组。
* @param {boolean} [droptrans = true] - 是否丢弃通道信息。
* @returns {ImgArray} - 生成的数组对象。
*/
toarray(droptrans = true) {
//将图像转为数组,droptrans表示通道丢弃,即只保留RGB三个通道的数据
if (this.iscasnew) this.update();
let channel = droptrans ? 3 : 4;
if (droptrans) {
let oimgar = new ImgArray({ height: this.height, width: this.width, channel });
oimgar.data.forEach((x, idx, arr) => { arr[idx] = this.data[parseInt(idx / 3) * 4 + idx % 3] }, this);
return oimgar;
}
else {
let oimgar = new ImgArray({ height: this.height, width: this.width, channel, lazy: true });
oimgar.data = new Float32Array(this.data);
return oimgar;
}
}
/**
* hist,获取直方图。
* @param {number} [channel = 3] - 通道数。
* @returns {Uint32Array|Array<Uint32Array>} - 生成的直方图。
*/
hist(channel = 3) {
//函数接收通道数(0,1,2),输入3时表示三通道的直方图
if (typeof channel !== 'number' || channel < 0 || channel > 3) {
throw new TypeError('Invalid channel parameter');
}
if (channel === 3) {
let histList = [
new Uint32Array(256),
new Uint32Array(256),
new Uint32Array(256)
];
this.data.forEach(function (x, idx) {
let c = idx % 4;
if (c !== 3) {
histList[c][x]++;
}
});
return histList;
} else {
let hist = new Uint32Array(256);
this.data.forEach(function (x, idx) {
if (idx % 4 === channel) {
if (hist[x] === undefined) {
hist[x] = 0;
}
hist[x]++;
}
});
return hist;
}
}
/**
* getpixel,获取图像的像素值
* @param {number} x - 像素的x坐标,横坐标
* @param {number} y - 像素的y坐标,纵坐标
* @returns {Uint8ClampedArray(4)} rgba,范围为0~255
*/
getpixel(x, y) {
let idx = this.idxtoelidx(y, x, 0);
return this.data.slice(idx, idx + this.channel);
}
/**
* setpixel,设置图像的像素值
* @param {number} x - 像素的x坐标,横坐标
* @param {number} y - 像素的y坐标,纵坐标
* @param {number[]} [rgba = [0, 0, 0, 255]] ,元素值的范围要为0~255
*/
setpixel(x, y, rgba = [0, 0, 0, 255]) {
rgba = new Uint8ClampedArray(rgba);
if (rgba.length > 4)
rgba = rgba.slice(0, 4);
let idx = this.idxtoelidx(y, x, 0);
this.data.set(rgba, idx);
}
//试验性函数
/**
* applycolormap,将图像转灰度后,为灰度图像添加warm to cool的通过添加伪彩色warm to cool
* cool to warm from vtkjs colormaps.json
* 颜色映射的计算来自于vtkjs colormaps.json中的定义
* @returns {Img} - 生成的图像对象。
*/
applycolormap() {
let colormap = new Map();
function calc(x) {
x = x / 255;
let r, g, b;
let wa, wb;
if (x < 0.5) {
wa = (0.5 - x) / 0.5
wb = x / 0.5
r = wa * 0.705882352941 + wb * 0.865;
g = wa * 0.0156862745098 + wb * 0.865;
b = wa * 149019607843 + wb * 0.865;
}
else {
wa = (1 - x) / 0.5
wb = (x - 0.5) / 0.5
r = wa * 0.865 + wb * 0.23137254902;
g = wa * 0.865 + wb * 0.298039215686;
b = wa * 0.865 + wb * 0.752941176471;
}
return [r * 255, g * 255, b * 255]
}
for (let i = 0; i < 256; i++) {
colormap.set(i, calc(i));
}
let outimg = this.empty(false);
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
let [r, g, b, a] = this.getpixel(x, y);
[r, g, b] = colormap.get(parseInt((r + g + b) / 3));
outimg.setpixel(x, y, [r, g, b, a]);
}
}
return outimg;
}
/**
* resize,将图像resize到指定大小
* @param {number} [height = 256] - 输出图像的高度,默认为256。
* @param {number} [width = 256] - 输出图像的宽度,默认为256。
* @returns {Img} - 生成的图像对象。
*/
resize(height = 256, width = 256) {
if (!this.iscasnew) this.update();
let canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');
//ctx.imageSmoothingEnabled = true; // Enable interpolation
ctx.drawImage(this.cas, 0, 0, this.width, this.height, 0, 0, width, height)
return Img.fromcanvas(canvas)
}
/**
* rotate,将图像旋转deg角度
* @param {number} [deg = 45] - 旋转角度,默认为45度。
* @returns {Img} - 生成的图像对象。
*/
rotate(deg = 45) {
if (!this.iscasnew) this.update();
let angleInRadians = deg * Math.PI / 180; // 将角度转换为弧度
let canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
let ctx = canvas.getContext('2d');
ctx.translate(this.cas.width / 2, this.cas.height / 2); // 将原点移动到图像中心
ctx.rotate(-angleInRadians); // 旋转图像
ctx.drawImage(this.cas, -this.width / 2, -this.height / 2); // 绘制旋转后的图像
return Img.fromcanvas(canvas);
}
/**
* opacity,设置图像不透明度,越大越不透明,取值0~255
* @param {number} [value = 255] - 不透明度的值,默认为255。
* @returns {Img} - 生成的图像对象。
*/
opacity(value = 255) {
//设置图像不透明度,越大越不透明,取值0~255
//this.fill(value,3); 这种和下面的用哪种实现?
if (this.iscasnew) this.update();
this.data.forEach(function (x, idx, arr) { if (idx % 4 == 3) arr[idx] = value });
return this;
}
/**
* bitplane,计算比特面,cidx取哪个通道,bitnum取哪个比特位,返回值是0、1的数组Imgarray对象
* @param {number} cidx - 取哪个通道,取值0~3。
* @param {number} bitnum - 取哪个比特位,取值0~7。
* @returns {Img} - 生成的图像对象。
*/
bitplane(cidx, bitnum) {
//计算比特面,cidx取哪个通道,bitnum取哪个比特位,返回值是0、1的数组Imgarray对象
//this.dsplit或循环 this.vectorize
// 推荐双层循环,对宽和高进行遍历,通过使用getelement获取通道的值
const bitplaneArray = new ImgArray({ height: this.height, width: this.width, channel: 1 })
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
//计算当前像素在data数组的索引
//let index = this.idxtoelidx(y, x, cidx);
//let val = this.data[index]; //获得指定通道的值
let val = this.getel(y, x, cidx);
const bitValue = (val >> bitnum) & 1; //提取指定比特位的值
//bitplaneArray.data[y * this.width + x] = bitValue * 255;
bitplaneArray.setel(y, x, 0, bitValue * 255);
}
}
return bitplaneArray;
}
/**
* RGBtoYCbCr,将RGB图像转换为YCbCr图像
* @returns {Img} - 生成的图像对象。
*/
RGBtoYCbCr() {
//将RGB图像转换为YCbCr图像
// 创建一个与原始图像相同大小的新图像对象
let ycbcrImg = new Img({ width: this.width, height: this.height, channel: this.channel });
if (this.channel < 3) {
console.error("该图像不是RGB图像,无法进行转换")
}
for (let i = 0; i < this.data.length; i += this.channel) {
// 获取当前像素的RGB值
let r = this.data[i];
let g = this.data[i + 1];
let b = this.data[i + 2];
// 计算YCbCr值
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let Cb = (-0.168736 * r - 0.331264 * g + 0.5 * b) + 128;
let Cr = (0.5 * r - 0.418688 * g - 0.081312 * b) + 128;
ycbcrImg.data[i] = y;
ycbcrImg.data[i + 1] = Cb;
ycbcrImg.data[i + 2] = Cr;
ycbcrImg.data[i + 3] = this.data[i + 3];
}
// console.log(ycbcrImg);
return ycbcrImg;
}
/**
* YCbCrtoRGB,将YCbCr图像转换为RGB图像
* @returns {Img} - 生成的图像对象。
*/
YCbCrtoRGB() {
let rgbImg = new Img({ width: this.width, height: this.height, channel: this.channel });
if (this.channel < 3) {
console.error("该图像无法转换为RGB图像")
}
for (let i = 0; i < this.data.length; i += this.channel) {
// 获取当前像素的RGB值
let y = this.data[i];
let Cb = this.data[i + 1];
let Cr = this.data[i + 2];
// 计算YCbCr值
let r = y + 1.402 * (Cr - 128);
let g = y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128);
let b = y + 1.772 * (Cb - 128);
rgbImg.data[i] = r;
rgbImg.data[i + 1] = g;
rgbImg.data[i + 2] = b;
rgbImg.data[i + 3] = this.data[i + 3];
}
// console.log(rgbImg);
return rgbImg;
}
/**
* drawbox,在图像上画一个指定大小的矩形
* @param {number} [x = 50] - 矩形左上角的x坐标,默认为50。
* @param {number} [y = 50] - 矩形左上角的y坐标,默认为50。
* @param {number} [w = 50] - 矩形的宽度,默认为50。
* @param {number} [h = 50] - 矩形的高度,默认为50。
* @param {string} [color = '#000000'] - 矩形的颜色,默认为'#000000'。
* @param {number} [linewidth = 2] - 矩形的线宽,默认为2。
* @param {boolean} [fill = false] - 是否填充矩
* @param {string} [fillcolor = '#000000'] - 填充的颜色,默认为'#000000'。
* @returns {Img} - 生成的图像对象。
*/
drawbox(x = 50, y = 50, w = 50, h = 50, color = '#000000', linewidth = 2, fill = false, fillcolor = '#000000') {
//在图像上画一个指定大小的矩形
if (!this.iscasnew) this.update();
// 设置线宽和颜色
this.ctx.lineWidth = linewidth;
this.ctx.strokeStyle = color;
// 绘制矩形框
if (fill) {
this.ctx.fillStyle = fillcolor;
this.ctx.fillRect(x, y, w, h);
} else {
this.ctx.strokeRect(x, y, w, h);
}
// 将绘制后的图像数据更新到Img对象中
//this.data = self.ctx.getImageData(0, 0, this.width, this.height).data;
this.iscasnew = true;
return this;
}
/**
* drawtext,在图像上绘制文字
* @param {number} [x = 50] - 文字的x坐标,默认为50。
* @param {number} [y = 50] - 文字的y坐标,默认为50。
* @param {string} [text = 'Hello'] - 要绘制的文字,默认为'Hello'。
* @param {string} [color = 'red'] - 文字的颜色,默认为'red'。
* @param {number} [linewidth = 2] - 文字的线宽,默认为2。
* @param {number} [fontSize = 20] - 文字的字体大小,默认为20。
* @returns {Img} - 生成的图像对象。
*/
drawtext({ x = 50, y = 50, text = 'Hello', color = 'red', linewidth = 2, fontSize = 20 } = {}) {
//在图像上绘制文字
// 设置文字属性,包括字体大小
if (!this.iscasnew) this.update();
this.ctx.font = `${fontSize}px Arial`; // 设置字体大小和样式
this.ctx.fillStyle = color;
this.ctx.lineWidth = linewidth;
// 写入文字
this.ctx.fillText(text, x, y);
// 将绘制后的图像数据更新到Img对象中
//this.data = self.ctx.getImageData(0, 0, this.width, this.height).data;
this.iscasnew = true;
return this;
}
/**
* show,向网页中插入图像
* @param {HTMLCanvasElement} [cas = null] - 要插入的canvas元素,默认为null。
* @returns {Img} - 生成的图像对象。
*/
show(cas = null) {
//向网页中插入图像
let cs = cas ? cas : document.createElement('canvas');
cs.width = this.width;
cs.height = this.height;
if (this.iscasnew) this.update();
this.tocanvas(cs);
cas ? null : document.body.appendChild(cs);
return this;
}
}
/**
* @class DICM,a simple DICOM format parser
* 一个简单的DICM格式解析器,读取的数组可转为ImgArray数组
* 参考资料:
* https://www.cnblogs.com/assassinx/archive/2013/01/09/dicomViewer.html
* https://www.cnblogs.com/sean-zeng/p/18061402
* dicom 文件可分为5个部分:
* 1. 前128个字节为导言区,全都是0;
* 2. 128-132字节,共4个字节为“DICM”字符串,用于识别检测DICM格式。
* 3. 文件元数据区, 从132字节开始,使用显示的VR格式记录, 格式有两种形式:
* 当VR值为OB OW OF UT SQ UN这几种时,格式为:组号(2),元素号(2),vr(2),预留(2),值长度(4),值 ;
* 当VR值为其他类型时,格式为:组号(2),元素号(2),vr(2),值长度(4),值;
* 4. 数据元数据区,可以使用显示VR或隐式VR,VR为显式时,数据元数据区为:
* VR为隐匿时,数据元数据区为:
* 组号(2),元素号(2),值长度(4),值;
* 不包含VR项,VR项需要根据组号和元素号确定;
* 5. 数据区域
*/
class DICM {
/**
* 构造函数
* @param {ArrayBuffer} bf - 二进制数据
*/
constructor(bf) {
this.bf = bf;
this.bytearr = new Uint8Array(bf)
//识别DICM格式
this.offset = this.#checkformat(); //the offset should be 132
//3.解析文件元数据
[this.islitteendian, this.isexplicitvr] = [true, true];
this.filemeta = this.#parseheadmeta();
//4.解析数据元数据
this.datameta = this.#parsedatameta();
}
/**
* checkformat,检查DICM格式
* @returns {number} - 检测到DICM格式的偏移量。
*/
#checkformat() {
//1. 前128个字节为导言区,全都是0;需要跳过
//2. 128-132字节,共4个字节为“DICM”字符串,用于识别检测DICM格式。
let offset = 128;
let formatstr = String.fromCharCode(...this.bytearr.slice(offset, offset += 4))
if (formatstr != 'DICM') throw ("it's not dicm image");
return offset;
}
/**
* getheaderonetag,获取 DICOM 文件的元数据头部信息。
* 该方法解析文件中的特定位置并提取出元数据区域的相关内容。
*
* 元数据区域包含了多种信息,其中包括:
* - 组号和元素号(2 字节)
* - VR(值表示数据类型,2 字节)
* - 值长度(4 字节或 2 字节,取决于 VR 类型)
* - 数据值(根据 VR 和长度变化)
* @param {boolean} [islitteendian=true] - 是否采用小端字节序,默认为 `true`,如果使用大端字节序,则传入 `false`。
* @returns {Object} 返回一个包含以下属性的对象:
* - {string} attrcode - 由组号和元素号组成的字符串,格式为 "组号,元素号"。
* - {string} vr - VR 字段的值,表示数据的类型。
* - {number} datalength - 数据部分的长度(以字节为单位)。
* - {Uint8Array} data - 解析出的原始数据值。
* - {any} value - 根据 VR 解析后的值。
*/
#getheaderonetag(islitteendian = true) {
//3. 文件元数据区, 从132字节开始,使用显示的VR格式记录, 格式有两种形式:
let offset = this.offset
let arr = this.bytearr;
//当VR值为OB OW OF UT SQ UN这几种时,格式为:组号(2),元素号(2),vr(2),预留(2),值长度(4),值 ;
//当VR值为其他类型时,格式为:组号(2),元素号(2),vr(2),值长度(2),值;
let attrcode = arr.slice(offset, offset += 4);
attrcode = Array.from(attrcode).map(byte => byte.toString(16).padStart(2, '0'));
attrcode = attrcode[1] + attrcode[0] + "," + attrcode[3] + attrcode[2];
let vr = String.fromCharCode(...arr.slice(offset, offset += 2));
let datalength = 0;
if (['OB', 'OW', 'OF', 'UT', 'SQ', 'UN'].includes(vr)) {
offset += 2;
datalength = new DataView(arr.buffer.slice(offset, offset += 4)).getUint32(0, islitteendian);
}
else {
datalength = new DataView(arr.buffer.slice(offset, offset += 2)).getUint16(0, islitteendian);
}
let data = arr.slice(offset, offset += datalength);
let value = this.vrparse(vr, data, islitteendian);
this.offset = offset;
return { attrcode, vr, datalength, data, value }
}
/**
* parseheadmeta
* @returns {Map} - 文件元数据。
*
*/
#parseheadmeta() {
// 3. 文件元数据区, 从132字节开始,使用显示的VR格式记录, 格式有两种形式:
let firsthead = this.#getheaderonetag();
let headendpos = 0;
//第一个数据块给出整个文件元数据区的长度,不包含该firsthead的长度
if (firsthead.attrcode == '0002,0000') //其值记录了整个文件元数据的长度)
{
headendpos = this.offset + firsthead.value;
}
else throw new Error('file head error');
let headmeta = new Map([[firsthead.attrcode, firsthead]]);
while (this.offset < headendpos) {
let metadata = this.#getheaderonetag();
if (metadata.attrcode == '0002,0010') {
[this.islitteendian, this.isexplicitvr] = this.#getdatametamode(metadata.attrcode, metadata.value);
}
headmeta.set(metadata.attrcode, metadata);
}
return headmeta;
}
/**
* getdatametamode
* @param {string} attrcode - 元数据项的属性代码。
* @param {Uint8Array} value - 元数据项的值。
* @returns {Array} - 返回一个包含两个元素的数组
* - 第一个元素表示是否采用小端字节序,第二个元素表示是否采用显式VR格式。
*/
#getdatametamode(attrcode, value) {
//获取数据元数据的模式,有两种显示的或隐式的,以及数值的大小端
if (attrcode != "0002,0010") {
return [null, null];
}
let isLitteEndian = null;
let isExplicitVR = null;
switch (value) {
case "1.2.840.10008.1.2.1\x00"://显示little
isLitteEndian = true;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2.2\x00"://显示big
isLitteEndian = false;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2\x00"://隐式little
isLitteEndian = true;
isExplicitVR = false;
break;
default:
break;
}
return [isLitteEndian, isExplicitVR]
}
/**
* parsedatameta,解析数据元数据。
* 该方法解析文件中的数据元数据区域,并提取出其中的元数据信息。
* @returns {Map} 返回一个包含数据元数据信息的 Map 对象。
*/
#parsedatameta() {
let datameta = new Map();
while (this.offset < this.bytearr.length) {
let metadata = null;
if (this.isexplicitvr) {
metadata = this.#getheaderonetag(this.islitteendian);
}
else {
metadata = this.#getoneimplicitmetadata(this.islitteendian);
}
datameta.set(metadata.attrcode, metadata);
}
return datameta;
}
/**
* getoneimplicitmetadata,解析隐式数据元数据。
* 该方法解析文件中的隐式数据元数据区域,并提取出其中的元数据信息。
* @returns {Object} 返回一个包含数据元数据信息的对象。
* - {string} attrcode - 元数据项的属性代码。
* - {string} vr - 元数据项的VR。
* - {number} datalength - 元数据项的值的长度。
* - {Uint8Array} data - 元数据项的值。
* - {any} value - 根据VR解析后的值。
*/
#getoneimplicitmetadata() {
let arr = this.bytearr;
let offset = this.offset;
let attrcode = arr.slice(offset, offset += 4);
attrcode = Array.from(attrcode).map(byte => byte.toString(16).padStart(2, '0'));
attrcode = attrcode[1] + attrcode[0] + "," + attrcode[3] + attrcode[2];
let vr = 'UI';
let datalength = new DataView(arr.buffer.slice(offset, offset += 4)).getInt32(0, true);
let data = arr.slice(offset, offset += datalength);
vr = this.attricodetovr(attrcode);
let value = this.vrparse(vr, data, this.islitteendian);
this.offset = offset;
return { attrcode, vr, datalength, data, value }
}
/**
* fromurl,从URL中加载DICM文件。
* @static
* @param {string} url - 文件的URL地址。
* @returns {Promise<DICM>} - 返回一个 Promise,resolve 时返回一个 DICM 对象。
*/
static async fromurl(url) {
let bf = await (await fetch(url)).arrayBuffer();
return new DICM(bf);
}
/**
* vrparse,解析VR。
* @param {string} vr - VR 的字符串表示。
* @param {Uint8Array} arr - 要解析的数组。
* @param {boolean} islitteendian - 是否采用小端字节序。
* @returns {string | number } - 解析后的值。
*/
vrparse(vr, arr, islitteendian) {
switch (vr) {
case 'CS':
case 'SH': //short string
case 'LO':
case 'ST':
case 'LT':
case 'UT':
case 'AE': //application entity
case 'PN': //person name
case 'UI': //Unique Identifier
case 'DA':
case 'TM':
case 'DT':
case 'AS':
case 'IS':
case 'DS':
case 'OW':
case 'OF':
return new TextDecoder().decode(arr);
case 'UL': //unsigned long
return new DataView(arr.buffer).getUint32(0, islitteendian);
case 'SS': //signed short
return new DataView(arr.buffer).getInt16(0, islitteendian);
case 'US': //unsigned short
case 'AT': //attribute tag
return new DataView(arr.buffer).getUint16(0, islitteendian);
case 'SL': //signed long
return new DataView(arr.buffer).getInt32(0, islitteendian);
case 'FL': //float
return new DataView(arr.buffer).getFloat32(0, islitteendian);
case 'FD': //double
return new DataView(arr.buffer).getFloat64(0, islitteendian);
case 'OB':
case 'UN':
break;
}
return 'unknow';
}
/**
* attricodetovr,将属性代码转换为VR。
* @param {string} attrcode - 属性代码。
* @returns {string} - 对应的VR。
*/
attricodetovr(attrcode) {
switch (attrcode) {
case "0002,0000"://文件元信息长度
return "UL";
case "0002,0010"://传输语法
return "UI";
case "0002,0013"://文件生成程序的标题
return "SH";
case "0008,0005"://文本编码
return "CS";
case "0008,0008":
return "CS";
case "0008,1032"://成像时间
return "SQ";
case "0008,1111":
return "SQ";
case "0008,0020"://检查日期
return "DA";
case "0008,0060"://成像仪器
return "CS";
case "0008,0070"://成像仪厂商
return "LO";
case "0008,0080":
return "LO";
case "0010,0010"://病人姓名
return "PN";
case "0010,0020"://病人id
return "LO";
case "0010,0030"://病人生日
return "DA";
case "0018,0060"://电压
return "DS";
case "0018,1030"://协议名
return "LO";
case "0018,1151":
return "IS";
case "0020,0010"://检查ID
return "SH";
case "0020,0011"://序列
return "IS";
case "0020,0012"://成像编号
return "IS";
case "0020,0013"://影像编号
return "IS";
case "0028,0002"://像素采样1为灰度3为彩色
return "US";
case "0028,0004"://图像模式MONOCHROME2为灰度
return "CS";
case "0028,0010"://row高
return "US";
case "0028,0011"://col宽
return "US";
case "0028,0100"://单个采样数据长度
return "US";
case "0028,0101"://实际长度
return "US";
case "0028,0102"://采样最大值
return "US";
case "0028,1050"://窗位
return "DS";
case "0028,1051"://窗宽
return "DS";
//https://dicom.innolitics.com/ciods/ct-image/clinical-trial-subject
//https://blog.csdn.net/m0_37477175/article/details/103803321
case "0028,1052": //Rescale Intercept
return "DS";
case "0028,1053": //Rescale Slope
return "DS";
case "0040,0008"://文件夹标签
return "SQ";
case "0040,0260"://文件夹标签
return "SQ";
case "0040,0275"://文件夹标签
return "SQ";
case '7fe0,0000': //像素长度
return "UL";
case "7fe0,0010"://像素数据开始处
return "OB";
default:
return "UI";
}
}
/**
* getdata,获取图像数据。
* @returns {Object} - 包含图像数据信息的对象。
* - height {number} - 图像高度。
* - width {number} - 图像宽度。
* - channel {number} - 图像通道数。
* - array {Array} - 图像数据数组。
*/
getdata() {
let b = this.datameta.get("0028,1052").value; //Rescale Intercept
let a = this.datameta.get("0028,1053").value;//Rescale Slope
let v = new Int16Array(this.datameta.get("7fe0,0010").data.buffer);
a = parseInt(a);
b = parseInt(b);
v = v.map(x => x * a + b); //change to HU;
let height = this.datameta.get("0028,0010").value; //image height;
let width = this.datameta.get("0028,0011").value;
return { height, width, channel: 1, array: v, unit: 'HU' }
}
/**
* toImgArray,将图像数据转换为ImgArray对象。
* @returns {ImgArray} - 包含图像数据的ImgArray对象。
*/
toImgArray() {
let v = this.getdata();
return ImgArray.fromArray(v.array, v);
}
}
/**
* delay,创建一个异步的延时函数。
* @param {number} n - 延时的毫秒数。
* @returns {Promise} - 返回一个Promise对象,在指定时间后解析。
*/
function delay(n) {
//异步的延时单位是ms
return new Promise(function (resolve) {
setTimeout(resolve, n);
});
}
//当将该文件用于ES6 Module时,取消以下注释,导出Img, ImgArray 和DICM类
//export {Img, ImgArray,DICM} ;