diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..08135a3a406045cd0dbea9588b868db183a6de81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:16-alpine +RUN apk add --no-cache python3 +RUN apk add --no-cache make +RUN apk add --no-cache g++ +RUN apk add --no-cache gcc +WORKDIR /app +COPY . . +RUN npm install +EXPOSE 8080 +CMD ["node","app.js"] \ No newline at end of file diff --git a/addon/common.h b/addon/common.h new file mode 100644 index 0000000000000000000000000000000000000000..4e7dbcb2b19032a02d0fa269c2ca4cce83d91af7 --- /dev/null +++ b/addon/common.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +using Array1D = std::vector; +using Array2D = std::vector; + +using Array1D_int = std::vector; +using Array2D_int = std::vector; diff --git a/addon/distFunkce.h b/addon/distFunkce.h new file mode 100644 index 0000000000000000000000000000000000000000..92d9e5389c2a847580518741140c1a788c2fdd9f --- /dev/null +++ b/addon/distFunkce.h @@ -0,0 +1,101 @@ +#pragma once + +#include + +#include "common.h" + +void distance( Array2D& u0, int n1, int n2, int N1, int N2, int rozsah, double h1, double h2, int pocetIteraci) +{ + float e=0.0001; + float t= 5.0e-7; + float rezid=100; + + // N1 radku na N2 sloupcu + Array2D u(N1, Array1D(N2)); + Array2D v(N1, Array1D(N2)); + + for (int i=n1; i<=n1+rozsah; i++){ + for (int j=n2; j<=n2+rozsah; j++){ + u[i][j] = u0[i][j]; + } + } + + + int n=0; + while(n0){ + clen1 = ((u[i][j] - u[i][j-1])/h1); + if (clen1 <0){ clen1 = 0.0; }; + clen2 = ((u[i][j+1] - u[i][j])/h1); + if (clen2 >0){ clen2 = 0.0; }; + if(clen1 < -clen2){clen1 = clen2;} + + clen3 = ((u[i][j] - u[i-1][j])/h2); + if (clen3 <0){ clen3 = 0.0; }; + clen4 = ((u[i+1][j] - u[i][j])/h2); + if (clen4 >0){ clen4 = 0.0; }; + if(clen3 < -clen4){clen3 = clen4;} + } + else{ + clen1 = ((u[i][j+1] - u[i][j])/h1); + if (clen1 <0){ clen1 = 0.0; }; + clen2 = ((u[i][j] - u[i][j-1])/h1); + if (clen2 >0){ clen2 = 0.0; }; + if(clen1 < -clen2){clen1 = clen2;} + + clen3 = ((u[i+1][j] - u[i][j])/h2); + if (clen3 <0){ clen3 = 0.0; }; + clen4 = ((u[i][j] - u[i-1][j])/h2); + if (clen4 >0){ clen4 = 0.0; }; + if(clen3 < -clen4){clen3 = clen4;} + } + + v[i][j] = u[i][j] + t*S*( 1.0 - sqrt(clen1*clen1 + clen3*clen3)); + } + + + for (int j=n2+1; j<=n2 + rozsah; j++){ v[n1][j]=u[n1+1][j];} + for (int j=n2+1; j<=n2 + rozsah; j++){ v[n1 + rozsah][j]=u[n1 + rozsah-1][j];} + + for (int i=n1+1; i<=n1 + rozsah; i++){ v[i][n2]=u[i][n2+1];} + for (int i=n1+1; i<=n1 + rozsah; i++){ v[i][n2 + rozsah]=u[i][n2 + rozsah-1];} + + v[n1][n2]=u[n1][n2+1]; + v[n1][n2 + rozsah]=u[n1][n2 + rozsah-1]; + + v[n1 + rozsah][n2]=u[n1 + rozsah -1][n2+1]; + v[n1 + rozsah][n2 + rozsah]=u[n1 + rozsah-1][n2 + rozsah-1]; + + + rezid=0.0; + for (int i=n1; i<=n1 + rozsah; i++){ + for (int j=n2; j<=n2 + rozsah; j++){ + rezid = rezid +(v[i][j] - u[i][j])*(v[i][j] - u[i][j]); + u[i][j] = v[i][j]; + } + } + rezid = sqrt(rezid); + + // vypis z GUI nema smysl + //if(n%10000==0){ cout << rezid << endl;} + } + + for (int i=n1; i<=n1+rozsah; i++){ + for (int j=n2; j<=n2+rozsah; j++){ + u0[i][j] = v[i][j]; + } + } + +} diff --git a/addon/filtr.h b/addon/filtr.h new file mode 100644 index 0000000000000000000000000000000000000000..ff155432063ed9e6f92fdbf417606685a5481cc1 --- /dev/null +++ b/addon/filtr.h @@ -0,0 +1,358 @@ +#pragma once + +#include +#include "common.h" + +int max_obr(Array2D& u, int N1, int N2, int* mi, int* mj) +{ + int maxV = u[0][0]; + + for (int i=0; imaxVal) + { + u[i][j] = maxVal; + } + sum[int(u[i][j])] ++; + } + } + + for (int i=0; i<= maxVal; i++) + { + cumsum = cumsum + sum[i]; + ratio[i] = float(cumsum)/float(N1*N2); + } + + for (int i=0; imaxVal) + { + u[i][j] = 0; + } + sum[int(u[i][j])] ++; + } + } + + for (int i=0; i<= maxVal; i++) + { + cumsum = cumsum + sum[i]; + ratio[i] = float(cumsum)/float((rozsah+1)*(rozsah+1)); + } + for (int i=n1; i<=n1+rozsah; i++) + { + for (int j=n2; j +#include +#include +#include "filtr.h" +#include "segmentFunkce.h" +#include "distFunkce.h" +#include "optfFunkce.h" + +Napi::ArrayBuffer level_set(const Napi::CallbackInfo & info) { + Napi::Env env = info.Env(); + float polomer_vnejsi = info[0].As(); + float polomer_vnitrni = info[1].As(); + int stred1 = info[2].As(); + int stred2 = info[3].As(); + float K1 = info[4].As(); + float K2 = info[5].As(); + const int N1 = info[6].As(); + const int N2 = info[7].As(); + + Napi::ArrayBuffer buf = info[8].As(); + double * data = reinterpret_cast(buf.Data()); + + Array2D u(N1, Array1D(N2)); + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + u[i][j] = data[i * N2 + j]; + + filtr(u, N1, N2); + + const int n1 = stred1 - polomer_vnejsi - 15; // svisle + const int n2 = stred2 - polomer_vnejsi - 15; // vodorovne + const int rozsah = 2*(polomer_vnejsi + 15); + const double h1 = 1.0 / std::max(N1, N2); + const double h2 = h1; + + Array2D_int A(N1, Array1D_int(N2)); + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + A[i][j] = std::round(u[i][j]); + + Array2D grad(N1, Array1D(N2)); + Array2D u_vnejsi(N1, Array1D(N2)); + Array2D u_vnitrni(N1, Array1D(N2)); + Array2D v(N1, Array1D(N2)); + +//filtrovani druha faze ________________________ + + ekvalizaceVyrez(A,N1,N2,n1,n2,rozsah); + filtrZaver(A,N1,N2); +//_____________________________________________- + + // vnejsi +// float K = 1.5e-6; + getGrad(A, grad, N1, N2, h1, h2, K1); + + inicializace(u_vnejsi, stred1, stred2, polomer_vnejsi, N1, N2, h1); + + segment(u_vnejsi, v, grad, n1, n2, rozsah, h1, h2, 6000); + distance(u_vnejsi, n1, n2, N1, N2, rozsah, h1, h2, 3.0e4); + + // modifikace - vnejsi -> vnitrni + for (int i=n1; i<=n1 + rozsah; i++) + for (int j=n2; j<=n2 + rozsah; j++) + u_vnitrni[i][j] = u_vnejsi[i][j] + (polomer_vnejsi - polomer_vnitrni) * h1; + + + // vnitrni +// K = 4.0e-6; + getGrad(A, grad, N1, N2, h1, h2, K2); + segment(u_vnitrni, v, grad, n1, n2, rozsah, h1, h2, 1200); + // now u_vnitrni == v + + // mezikruzi + for (int i=n1; i<=n1 + rozsah; i++) + for (int j=n2; j<=n2 + rozsah; j++) + if(u_vnitrni[i][j]<0) + v[i][j] = -u_vnitrni[i][j]; // vnitrek + else + v[i][j] = u_vnejsi[i][j]; // vnejsek + + distance(v, n1, n2, N1, N2, rozsah, h1, h2, 3.0e4); + + // normalizace pro vystup + #pragma omp parallel for collapse(2) schedule(static) + for (int i=0; i 10*h1) + v[i][j] = 10*h1; + if(v[i][j]< -10*h1) + v[i][j] = -10*h1; + } + + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + data[i * N2 + j] = v[i][j]; + + return Napi::ArrayBuffer::New(env, buf.Data(), buf.ByteLength()); +} + +Napi::ArrayBuffer OPTF(const Napi::CallbackInfo & info) { + Napi::Env env = info.Env(); + float polomer = info[0].As(); + int stred1 = info[1].As(); + int stred2 = info[2].As(); + const int N1 = info[3].As(); + const int N2 = info[4].As(); + + Napi::ArrayBuffer buf = info[5].As(); + double * data = reinterpret_cast(buf.Data()); + + Array2D J(N1, Array1D(N2)); + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + J[i][j] = data[i * N2 + j]; + + Array2D I(N1, Array1D(N2)); + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + I[i][j] = data[i * N2 + j + N1 * N2]; + + Array2D_int T(N1, Array1D_int(N2)); + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + T[i][j] = std::round(data[i * N2 + j + N1 * N2 * 2]); + + const int n1 = stred1 - polomer - 15; // svisle + const int n2 = stred2 - polomer - 15; // vodorovne + const int rozsah = 2*(polomer + 15); + const double h1 = 1.0 / std::max(N1, N2); + const double h2 = h1; + + Array2D u(N1, Array1D(N2)); + Array2D v(N1, Array1D(N2)); + + // pomocna pole + Array2D_int p(N1, Array1D_int(N2)); + + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) { + u[i][j] = 0.0; + v[i][j] = 0.0; + J[i][j] = std::max(N1, N2) * J[i][j]; + I[i][j] = std::max(N1, N2) * I[i][j]; + } + + for (int i = n1 + 1; i < n1 + rozsah; i++) + for (int j = n2 + 1; j < n2 + rozsah; j++){ + u[i][j] = 0.1 * h1; + v[i][j] = -0.1 * h1; + } + + const float gama = 4.5; // -> minimalizace + const float alfa = 0.75; // -> brightness const. + const float beta = 4.5; // -> vyhlazení + + const float e = 1.0e-3; + const float t = 7.0e-8; + + const int pocet_iteraci = 6.0e3; + + optf(u, v, I, J, n1, n2, N1, N2, rozsah, h1, h2, pocet_iteraci, e, t, alfa, beta, gama); + + #pragma omp parallel for collapse(2) schedule(static) + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) { + const int ra = i - std::round(v[i][j] / h2); + const int sl = j - std::round(u[i][j] / h1); + if (ra >= 0 && ra < N1 && sl >= 0 && sl < N2) + p[i][j] = T[ra][sl]; + else + p[i][j] = 0; + } + + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + data[i * N2 + j] = p[i][j]; + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + data[i * N2 + j + N1 * N2] = u[i][j]; + for (int i = 0; i < N1; i++) + for (int j = 0; j < N2; j++) + data[i * N2 + j + N1 * N2 * 2] = v[i][j]; + + return Napi::ArrayBuffer::New(env, buf.Data(), buf.ByteLength()); +} + +Napi::Object InitAll(Napi::Env env, Napi::Object exports) { + exports.Set("levelSet", Napi::Function::New(env, level_set)); + exports.Set("optf", Napi::Function::New(env, OPTF)); + return exports; +} + +NODE_API_MODULE(mriAddon, InitAll) \ No newline at end of file diff --git a/addon/optfFunkce.h b/addon/optfFunkce.h new file mode 100644 index 0000000000000000000000000000000000000000..fbd06b58f86bb24ff7348a57dd0f667d3ab424fe --- /dev/null +++ b/addon/optfFunkce.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include "common.h" + +void normy(const Array2D& I, const Array2D& J, double *L1, double *L2, int n1, int n2, int rozsah) +{ + *L1 = 0.0; + *L2 = 0.0; + + for (int i= n1; i<= n1 + rozsah;i++){ + for (int j=n2; j<= n2 + rozsah; j++){ + *L2 = *L2 + (J[i][j] - I[i][j])*(J[i][j] - I[i][j]); + } + } + *L2= sqrt(*L2); + + ////////////////// L1 norma + for (int i= n1; i<= n1 + rozsah;i++){ + for (int j=n2; j<= n2 + rozsah; j++){ + if((J[i][j] - I[i][j])>=0) + *L1 = *L1 + (J[i][j] - I[i][j]); + else + *L1 = *L1 + (-J[i][j] + I[i][j]); + } + } +} + +void optf(Array2D& u, Array2D& v, const Array2D& I, const Array2D& J, int n1, int n2, int N1, int N2, int rozsah, double h1, double h2, + int pocet_iteraci, double e, double t, double alfa, double beta, double gama) +{ + double h=1.0; + + Array2D u_n(N1, Array1D(N2)); + Array2D v_n(N1, Array1D(N2)); + + int iter=0; + while(iter < pocet_iteraci){ + iter = iter + 1; + + for (int i=n1 +1; i + +#include "common.h" + +void getGrad(const Array2D_int& I, Array2D& grad, int N1, int N2, double h1, double h2, float K = 7e-7) +{ + double E0,N0,S0,W0,grI; + + for (int i=1; i0) clen1 = a1*((u[i][j+1]-u[i][j])/h1); + else clen1 = a1*((u[i][j]-u[i][j-1])/h1); + if(a2>0) clen2 = a2*((u[i+1][j]-u[i][j])/h2); + else clen2 = a2*((u[i][j]-u[i-1][j])/h2); + + // vypocet clenu 3 + E= pow((1.0/h1)*(u[i][j+1] - u[i][j]),2) + pow((1.0/h2)*(u[i+1][j]+u[i+1][j+1]-u[i-1][j] -u[i-1][j+1]),2) ; + N= pow((1.0/h1)*(u[i][j] - u[i-1][j]),2) + pow((1.0/h2)*(u[i-1][j+1]+u[i][j+1]-u[i-1][j-1] -u[i][j-1]),2) ; + W= pow((1.0/h1)*(u[i][j] - u[i][j-1]),2) + pow((1.0/h2)*(u[i+1][j-1]+u[i+1][j]-u[i-1][j] -u[i-1][j-1]),2) ; + S= pow((1.0/h1)*(u[i+1][j] - u[i][j]),2) + pow((1.0/h2)*(u[i][j+1]+u[i+1][j+1]-u[i][j-1] -u[i+1][j-1]),2) ; + norma = sqrt(e*e + 0.25*(E+N+S+W)); + + g1 =1.0/ sqrt(e*e + E); /// E = (norma gradientu u na hrane E )^2 + g2 =1.0/ sqrt(e*e + N); + g3 =1.0/ sqrt(e*e + W); + g4 =1.0/ sqrt(e*e + S); + + clen3 = (1/(h1*h1))*(g1*(u[i][j+1] - u[i][j]) - g3*(u[i][j] - u[i][j-1])) + +(1/(h2*h2))*(g4*(u[i+1][j] - u[i][j]) - g2*(u[i][j] - u[i-1][j])); + + v[i][j]=u[i][j] + t*(clen1 + clen2)+ t*grad[i][j]*norma*f*clen3; + + } + } + + /*for (int j=0; j { + let height = req.body.parameters.height; + let width = req.body.parameters.width; + let centerX = req.body.parameters.inputParameters.centerX; + let centerY = req.body.parameters.inputParameters.centerY; + let outerX = req.body.parameters.inputParameters.outerX; + let outerY = req.body.parameters.inputParameters.outerY; + let innerX = req.body.parameters.inputParameters.innerX; + let innerY = req.body.parameters.inputParameters.innerY; + let K1 = req.body.parameters.inputParameters.K1 * Math.pow(10.0, -6.0); + let K2 = req.body.parameters.inputParameters.K2 * Math.pow(10.0, -6.0); + let outerRadius = Math.sqrt(Math.pow(outerX - centerX, 2.0) + Math.pow(outerY - centerY, 2.0)); + let innerRadius = Math.sqrt(Math.pow(innerX - centerX, 2.0) + Math.pow(innerY - centerY, 2.0)); + let pixelData = new Float64Array(req.body.pixelData); + let segmented = new Float64Array(mriAddon.levelSet(outerRadius, innerRadius, centerY, centerX, K1, K2, + height, width, pixelData.buffer)); + res.send(Object.values(segmented)); +}); + +app.post("/optf", (req, res) => { + let height = req.body.parameters.height; + let width = req.body.parameters.width; + let centerX = req.body.parameters.inputParameters.centerX; + let centerY = req.body.parameters.inputParameters.centerY; + let outerX = req.body.parameters.inputParameters.outerX; + let outerY = req.body.parameters.inputParameters.outerY; + let outerRadius = Math.sqrt(Math.pow(outerX - centerX, 2.0) + Math.pow(outerY - centerY, 2.0)); + let targetSegmented = new Float64Array(req.body.targetSegmented); + let sourceSegmented = new Float64Array(req.body.sourceSegmented); + let sourceOriginal = new Float64Array(req.body.sourceOriginal); + let mergedArray = new Float64Array(width * height * 3); + mergedArray.set(targetSegmented); + mergedArray.set(sourceSegmented, width * height); + mergedArray.set(sourceOriginal, width * height * 2); + let optf = new Float64Array(mriAddon.optf(outerRadius, centerY, centerX, height, width, mergedArray.buffer)); + res.send(Object.values(optf)); +}); + +app.listen(port, () => { + console.log(`App listening on port ${port}`); +}); \ No newline at end of file diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000000000000000000000000000000000000..77742dcdc278abeec0852696d995017333c46a23 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,28 @@ +{ + "targets": [{ + "target_name": "mriAddon", + 'msvs_settings': { + 'VCCLCompilerTool': { + 'AdditionalOptions': ['/MT', '/openmp'] + } + }, + "cflags!": ["-fno-exceptions"], + "cflags_cc!": ["-fno-exceptions"], + "sources": [ + "addon/mri.cpp", + "addon/common.h", + "addon/filtr.h", + "addon/segmentFunkce.h", + "addon/distFunkce.h", + "addon/optfFunkce.h" + ], + 'include_dirs': [ + " - - - - MRI - - - -
- - - - - \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..34315fc1a47d52078334a201ff3cb0115e88e655 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "MRI", + "version": "1.0.0", + "main": "app.js", + "gypfile": true, + "scripts": { + "build": "node-gyp rebuild", + "clean": "node-gyp clean" + }, + "dependencies": { + "body-parser": "^1.19.1", + "express": "^4.17.2", + "node-addon-api": "^4.3.0", + "node-gyp": "^8.4.1" + } +} diff --git a/public/cornerstone.min.js b/public/cornerstone.min.js new file mode 100644 index 0000000000000000000000000000000000000000..37d73fefda6932d552ad93bbeb1bbfc6a64bd224 --- /dev/null +++ b/public/cornerstone.min.js @@ -0,0 +1,3 @@ +/*! cornerstone-core - 2.6.1 - 2021-11-19 | (c) 2016 Chris Hafey | https://github.com/cornerstonejs/cornerstone */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("cornerstone-core",[],t):"object"==typeof exports?exports["cornerstone-core"]=t():e.cornerstone=t()}(window,function(){return function(r){var n={};function a(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,a),t.l=!0,t.exports}return a.m=r,a.c=n,a.d=function(e,t,r){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(t,e){if(1&e&&(t=a(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(a.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)a.d(r,n,function(e){return t[e]}.bind(null,n));return r},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=0)}([function(e,t,r){"use strict";function a(e,t){for(var r=0;rthis.TableRange[1])return this.AboveRangeColor;return this.Table[t]}},{key:"getIndex",value:function(e){var t={Range:[]};if(t.MaxIndex=this.NumberOfColors-1,t.Shift=-this.TableRange[0],this.TableRange[1]<=this.TableRange[0]?t.Scale=Number.MAX_VALUE:t.Scale=t.MaxIndex/(this.TableRange[1]-this.TableRange[0]),t.Range[0]=this.TableRange[0],t.Range[1]=this.TableRange[1],isNaN(e))return-1;var r,n,a,i=(a=(r=e)<(n=t).Range[0]?n.MaxIndex+0+1.5:r>n.Range[1]?n.MaxIndex+1+1.5:(r+n.Shift)*n.Scale,Math.floor(a));return i===this.NumberOfColors+0?i=0:i===this.NumberOfColors+1&&(i=this.NumberOfColors-1),i}},{key:"setTableValue",value:function(e,t){if(5===arguments.length&&(t=Array.prototype.slice.call(arguments,1)),e<0)throw new Error("Can't set the table value for negative index (".concat(e,")"));e>=this.NumberOfColors&&new Error("Index ".concat(e," is greater than the number of colors ").concat(this.NumberOfColors)),this.Table[e]=t,0!==e&&e!==this.NumberOfColors-1||this.buildSpecialColors()}}])&&M(t.prototype,r),n&&M(t,n),e}(),U=[0,0,0,0],N={hotIron:{name:"Hot Iron",numOfColors:256,colors:[[0,0,0,255],[2,0,0,255],[4,0,0,255],[6,0,0,255],[8,0,0,255],[10,0,0,255],[12,0,0,255],[14,0,0,255],[16,0,0,255],[18,0,0,255],[20,0,0,255],[22,0,0,255],[24,0,0,255],[26,0,0,255],[28,0,0,255],[30,0,0,255],[32,0,0,255],[34,0,0,255],[36,0,0,255],[38,0,0,255],[40,0,0,255],[42,0,0,255],[44,0,0,255],[46,0,0,255],[48,0,0,255],[50,0,0,255],[52,0,0,255],[54,0,0,255],[56,0,0,255],[58,0,0,255],[60,0,0,255],[62,0,0,255],[64,0,0,255],[66,0,0,255],[68,0,0,255],[70,0,0,255],[72,0,0,255],[74,0,0,255],[76,0,0,255],[78,0,0,255],[80,0,0,255],[82,0,0,255],[84,0,0,255],[86,0,0,255],[88,0,0,255],[90,0,0,255],[92,0,0,255],[94,0,0,255],[96,0,0,255],[98,0,0,255],[100,0,0,255],[102,0,0,255],[104,0,0,255],[106,0,0,255],[108,0,0,255],[110,0,0,255],[112,0,0,255],[114,0,0,255],[116,0,0,255],[118,0,0,255],[120,0,0,255],[122,0,0,255],[124,0,0,255],[126,0,0,255],[128,0,0,255],[130,0,0,255],[132,0,0,255],[134,0,0,255],[136,0,0,255],[138,0,0,255],[140,0,0,255],[142,0,0,255],[144,0,0,255],[146,0,0,255],[148,0,0,255],[150,0,0,255],[152,0,0,255],[154,0,0,255],[156,0,0,255],[158,0,0,255],[160,0,0,255],[162,0,0,255],[164,0,0,255],[166,0,0,255],[168,0,0,255],[170,0,0,255],[172,0,0,255],[174,0,0,255],[176,0,0,255],[178,0,0,255],[180,0,0,255],[182,0,0,255],[184,0,0,255],[186,0,0,255],[188,0,0,255],[190,0,0,255],[192,0,0,255],[194,0,0,255],[196,0,0,255],[198,0,0,255],[200,0,0,255],[202,0,0,255],[204,0,0,255],[206,0,0,255],[208,0,0,255],[210,0,0,255],[212,0,0,255],[214,0,0,255],[216,0,0,255],[218,0,0,255],[220,0,0,255],[222,0,0,255],[224,0,0,255],[226,0,0,255],[228,0,0,255],[230,0,0,255],[232,0,0,255],[234,0,0,255],[236,0,0,255],[238,0,0,255],[240,0,0,255],[242,0,0,255],[244,0,0,255],[246,0,0,255],[248,0,0,255],[250,0,0,255],[252,0,0,255],[254,0,0,255],[255,0,0,255],[255,2,0,255],[255,4,0,255],[255,6,0,255],[255,8,0,255],[255,10,0,255],[255,12,0,255],[255,14,0,255],[255,16,0,255],[255,18,0,255],[255,20,0,255],[255,22,0,255],[255,24,0,255],[255,26,0,255],[255,28,0,255],[255,30,0,255],[255,32,0,255],[255,34,0,255],[255,36,0,255],[255,38,0,255],[255,40,0,255],[255,42,0,255],[255,44,0,255],[255,46,0,255],[255,48,0,255],[255,50,0,255],[255,52,0,255],[255,54,0,255],[255,56,0,255],[255,58,0,255],[255,60,0,255],[255,62,0,255],[255,64,0,255],[255,66,0,255],[255,68,0,255],[255,70,0,255],[255,72,0,255],[255,74,0,255],[255,76,0,255],[255,78,0,255],[255,80,0,255],[255,82,0,255],[255,84,0,255],[255,86,0,255],[255,88,0,255],[255,90,0,255],[255,92,0,255],[255,94,0,255],[255,96,0,255],[255,98,0,255],[255,100,0,255],[255,102,0,255],[255,104,0,255],[255,106,0,255],[255,108,0,255],[255,110,0,255],[255,112,0,255],[255,114,0,255],[255,116,0,255],[255,118,0,255],[255,120,0,255],[255,122,0,255],[255,124,0,255],[255,126,0,255],[255,128,4,255],[255,130,8,255],[255,132,12,255],[255,134,16,255],[255,136,20,255],[255,138,24,255],[255,140,28,255],[255,142,32,255],[255,144,36,255],[255,146,40,255],[255,148,44,255],[255,150,48,255],[255,152,52,255],[255,154,56,255],[255,156,60,255],[255,158,64,255],[255,160,68,255],[255,162,72,255],[255,164,76,255],[255,166,80,255],[255,168,84,255],[255,170,88,255],[255,172,92,255],[255,174,96,255],[255,176,100,255],[255,178,104,255],[255,180,108,255],[255,182,112,255],[255,184,116,255],[255,186,120,255],[255,188,124,255],[255,190,128,255],[255,192,132,255],[255,194,136,255],[255,196,140,255],[255,198,144,255],[255,200,148,255],[255,202,152,255],[255,204,156,255],[255,206,160,255],[255,208,164,255],[255,210,168,255],[255,212,172,255],[255,214,176,255],[255,216,180,255],[255,218,184,255],[255,220,188,255],[255,222,192,255],[255,224,196,255],[255,226,200,255],[255,228,204,255],[255,230,208,255],[255,232,212,255],[255,234,216,255],[255,236,220,255],[255,238,224,255],[255,240,228,255],[255,242,232,255],[255,244,236,255],[255,246,240,255],[255,248,244,255],[255,250,248,255],[255,252,252,255],[255,255,255,255]]},pet:{name:"PET",numColors:256,colors:[[0,0,0,255],[0,2,1,255],[0,4,3,255],[0,6,5,255],[0,8,7,255],[0,10,9,255],[0,12,11,255],[0,14,13,255],[0,16,15,255],[0,18,17,255],[0,20,19,255],[0,22,21,255],[0,24,23,255],[0,26,25,255],[0,28,27,255],[0,30,29,255],[0,32,31,255],[0,34,33,255],[0,36,35,255],[0,38,37,255],[0,40,39,255],[0,42,41,255],[0,44,43,255],[0,46,45,255],[0,48,47,255],[0,50,49,255],[0,52,51,255],[0,54,53,255],[0,56,55,255],[0,58,57,255],[0,60,59,255],[0,62,61,255],[0,65,63,255],[0,67,65,255],[0,69,67,255],[0,71,69,255],[0,73,71,255],[0,75,73,255],[0,77,75,255],[0,79,77,255],[0,81,79,255],[0,83,81,255],[0,85,83,255],[0,87,85,255],[0,89,87,255],[0,91,89,255],[0,93,91,255],[0,95,93,255],[0,97,95,255],[0,99,97,255],[0,101,99,255],[0,103,101,255],[0,105,103,255],[0,107,105,255],[0,109,107,255],[0,111,109,255],[0,113,111,255],[0,115,113,255],[0,117,115,255],[0,119,117,255],[0,121,119,255],[0,123,121,255],[0,125,123,255],[0,128,125,255],[1,126,127,255],[3,124,129,255],[5,122,131,255],[7,120,133,255],[9,118,135,255],[11,116,137,255],[13,114,139,255],[15,112,141,255],[17,110,143,255],[19,108,145,255],[21,106,147,255],[23,104,149,255],[25,102,151,255],[27,100,153,255],[29,98,155,255],[31,96,157,255],[33,94,159,255],[35,92,161,255],[37,90,163,255],[39,88,165,255],[41,86,167,255],[43,84,169,255],[45,82,171,255],[47,80,173,255],[49,78,175,255],[51,76,177,255],[53,74,179,255],[55,72,181,255],[57,70,183,255],[59,68,185,255],[61,66,187,255],[63,64,189,255],[65,63,191,255],[67,61,193,255],[69,59,195,255],[71,57,197,255],[73,55,199,255],[75,53,201,255],[77,51,203,255],[79,49,205,255],[81,47,207,255],[83,45,209,255],[85,43,211,255],[86,41,213,255],[88,39,215,255],[90,37,217,255],[92,35,219,255],[94,33,221,255],[96,31,223,255],[98,29,225,255],[100,27,227,255],[102,25,229,255],[104,23,231,255],[106,21,233,255],[108,19,235,255],[110,17,237,255],[112,15,239,255],[114,13,241,255],[116,11,243,255],[118,9,245,255],[120,7,247,255],[122,5,249,255],[124,3,251,255],[126,1,253,255],[128,0,255,255],[130,2,252,255],[132,4,248,255],[134,6,244,255],[136,8,240,255],[138,10,236,255],[140,12,232,255],[142,14,228,255],[144,16,224,255],[146,18,220,255],[148,20,216,255],[150,22,212,255],[152,24,208,255],[154,26,204,255],[156,28,200,255],[158,30,196,255],[160,32,192,255],[162,34,188,255],[164,36,184,255],[166,38,180,255],[168,40,176,255],[170,42,172,255],[171,44,168,255],[173,46,164,255],[175,48,160,255],[177,50,156,255],[179,52,152,255],[181,54,148,255],[183,56,144,255],[185,58,140,255],[187,60,136,255],[189,62,132,255],[191,64,128,255],[193,66,124,255],[195,68,120,255],[197,70,116,255],[199,72,112,255],[201,74,108,255],[203,76,104,255],[205,78,100,255],[207,80,96,255],[209,82,92,255],[211,84,88,255],[213,86,84,255],[215,88,80,255],[217,90,76,255],[219,92,72,255],[221,94,68,255],[223,96,64,255],[225,98,60,255],[227,100,56,255],[229,102,52,255],[231,104,48,255],[233,106,44,255],[235,108,40,255],[237,110,36,255],[239,112,32,255],[241,114,28,255],[243,116,24,255],[245,118,20,255],[247,120,16,255],[249,122,12,255],[251,124,8,255],[253,126,4,255],[255,128,0,255],[255,130,4,255],[255,132,8,255],[255,134,12,255],[255,136,16,255],[255,138,20,255],[255,140,24,255],[255,142,28,255],[255,144,32,255],[255,146,36,255],[255,148,40,255],[255,150,44,255],[255,152,48,255],[255,154,52,255],[255,156,56,255],[255,158,60,255],[255,160,64,255],[255,162,68,255],[255,164,72,255],[255,166,76,255],[255,168,80,255],[255,170,85,255],[255,172,89,255],[255,174,93,255],[255,176,97,255],[255,178,101,255],[255,180,105,255],[255,182,109,255],[255,184,113,255],[255,186,117,255],[255,188,121,255],[255,190,125,255],[255,192,129,255],[255,194,133,255],[255,196,137,255],[255,198,141,255],[255,200,145,255],[255,202,149,255],[255,204,153,255],[255,206,157,255],[255,208,161,255],[255,210,165,255],[255,212,170,255],[255,214,174,255],[255,216,178,255],[255,218,182,255],[255,220,186,255],[255,222,190,255],[255,224,194,255],[255,226,198,255],[255,228,202,255],[255,230,206,255],[255,232,210,255],[255,234,214,255],[255,236,218,255],[255,238,222,255],[255,240,226,255],[255,242,230,255],[255,244,234,255],[255,246,238,255],[255,248,242,255],[255,250,246,255],[255,252,250,255],[255,255,255,255]]},hotMetalBlue:{name:"Hot Metal Blue",numColors:256,colors:[[0,0,0,255],[0,0,2,255],[0,0,4,255],[0,0,6,255],[0,0,8,255],[0,0,10,255],[0,0,12,255],[0,0,14,255],[0,0,16,255],[0,0,17,255],[0,0,19,255],[0,0,21,255],[0,0,23,255],[0,0,25,255],[0,0,27,255],[0,0,29,255],[0,0,31,255],[0,0,33,255],[0,0,35,255],[0,0,37,255],[0,0,39,255],[0,0,41,255],[0,0,43,255],[0,0,45,255],[0,0,47,255],[0,0,49,255],[0,0,51,255],[0,0,53,255],[0,0,55,255],[0,0,57,255],[0,0,59,255],[0,0,61,255],[0,0,63,255],[0,0,65,255],[0,0,67,255],[0,0,69,255],[0,0,71,255],[0,0,73,255],[0,0,75,255],[0,0,77,255],[0,0,79,255],[0,0,81,255],[0,0,83,255],[0,0,84,255],[0,0,86,255],[0,0,88,255],[0,0,90,255],[0,0,92,255],[0,0,94,255],[0,0,96,255],[0,0,98,255],[0,0,100,255],[0,0,102,255],[0,0,104,255],[0,0,106,255],[0,0,108,255],[0,0,110,255],[0,0,112,255],[0,0,114,255],[0,0,116,255],[0,0,117,255],[0,0,119,255],[0,0,121,255],[0,0,123,255],[0,0,125,255],[0,0,127,255],[0,0,129,255],[0,0,131,255],[0,0,133,255],[0,0,135,255],[0,0,137,255],[0,0,139,255],[0,0,141,255],[0,0,143,255],[0,0,145,255],[0,0,147,255],[0,0,149,255],[0,0,151,255],[0,0,153,255],[0,0,155,255],[0,0,157,255],[0,0,159,255],[0,0,161,255],[0,0,163,255],[0,0,165,255],[0,0,167,255],[3,0,169,255],[6,0,171,255],[9,0,173,255],[12,0,175,255],[15,0,177,255],[18,0,179,255],[21,0,181,255],[24,0,183,255],[26,0,184,255],[29,0,186,255],[32,0,188,255],[35,0,190,255],[38,0,192,255],[41,0,194,255],[44,0,196,255],[47,0,198,255],[50,0,200,255],[52,0,197,255],[55,0,194,255],[57,0,191,255],[59,0,188,255],[62,0,185,255],[64,0,182,255],[66,0,179,255],[69,0,176,255],[71,0,174,255],[74,0,171,255],[76,0,168,255],[78,0,165,255],[81,0,162,255],[83,0,159,255],[85,0,156,255],[88,0,153,255],[90,0,150,255],[93,2,144,255],[96,4,138,255],[99,6,132,255],[102,8,126,255],[105,9,121,255],[108,11,115,255],[111,13,109,255],[114,15,103,255],[116,17,97,255],[119,19,91,255],[122,21,85,255],[125,23,79,255],[128,24,74,255],[131,26,68,255],[134,28,62,255],[137,30,56,255],[140,32,50,255],[143,34,47,255],[146,36,44,255],[149,38,41,255],[152,40,38,255],[155,41,35,255],[158,43,32,255],[161,45,29,255],[164,47,26,255],[166,49,24,255],[169,51,21,255],[172,53,18,255],[175,55,15,255],[178,56,12,255],[181,58,9,255],[184,60,6,255],[187,62,3,255],[190,64,0,255],[194,66,0,255],[198,68,0,255],[201,70,0,255],[205,72,0,255],[209,73,0,255],[213,75,0,255],[217,77,0,255],[221,79,0,255],[224,81,0,255],[228,83,0,255],[232,85,0,255],[236,87,0,255],[240,88,0,255],[244,90,0,255],[247,92,0,255],[251,94,0,255],[255,96,0,255],[255,98,3,255],[255,100,6,255],[255,102,9,255],[255,104,12,255],[255,105,15,255],[255,107,18,255],[255,109,21,255],[255,111,24,255],[255,113,26,255],[255,115,29,255],[255,117,32,255],[255,119,35,255],[255,120,38,255],[255,122,41,255],[255,124,44,255],[255,126,47,255],[255,128,50,255],[255,130,53,255],[255,132,56,255],[255,134,59,255],[255,136,62,255],[255,137,65,255],[255,139,68,255],[255,141,71,255],[255,143,74,255],[255,145,76,255],[255,147,79,255],[255,149,82,255],[255,151,85,255],[255,152,88,255],[255,154,91,255],[255,156,94,255],[255,158,97,255],[255,160,100,255],[255,162,103,255],[255,164,106,255],[255,166,109,255],[255,168,112,255],[255,169,115,255],[255,171,118,255],[255,173,121,255],[255,175,124,255],[255,177,126,255],[255,179,129,255],[255,181,132,255],[255,183,135,255],[255,184,138,255],[255,186,141,255],[255,188,144,255],[255,190,147,255],[255,192,150,255],[255,194,153,255],[255,196,156,255],[255,198,159,255],[255,200,162,255],[255,201,165,255],[255,203,168,255],[255,205,171,255],[255,207,174,255],[255,209,176,255],[255,211,179,255],[255,213,182,255],[255,215,185,255],[255,216,188,255],[255,218,191,255],[255,220,194,255],[255,222,197,255],[255,224,200,255],[255,226,203,255],[255,228,206,255],[255,229,210,255],[255,231,213,255],[255,233,216,255],[255,235,219,255],[255,237,223,255],[255,239,226,255],[255,240,229,255],[255,242,232,255],[255,244,236,255],[255,246,239,255],[255,248,242,255],[255,250,245,255],[255,251,249,255],[255,253,252,255],[255,255,255,255]]},pet20Step:{name:"PET 20 Step",numColors:256,colors:[[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[0,0,0,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[96,0,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,80,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[48,48,112,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[80,80,128,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[96,96,176,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[112,112,192,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[128,128,224,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,96,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[48,144,48,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[80,192,80,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[64,224,64,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[224,224,80,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,208,96,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,176,64,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[208,144,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[192,96,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[176,48,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,0,0,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255]]},gray:{name:"Gray",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[1,1,1]],green:[[0,0,0],[1,1,1]],blue:[[0,0,0],[1,1,1]]}},jet:{name:"Jet",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[.35,0,0],[.66,1,1],[.89,1,1],[1,.5,.5]],green:[[0,0,0],[.125,0,0],[.375,1,1],[.64,1,1],[.91,0,0],[1,0,0]],blue:[[0,.5,.5],[.11,1,1],[.34,1,1],[.65,0,0],[1,0,0]]}},hsv:{name:"HSV",numColors:256,gamma:1,segmentedData:{red:[[0,1,1],[.15873,1,1],[.174603,.96875,.96875],[.333333,.03125,.03125],[.349206,0,0],[.666667,0,0],[.68254,.03125,.03125],[.84127,.96875,.96875],[.857143,1,1],[1,1,1]],green:[[0,0,0],[.15873,.9375,.9375],[.174603,1,1],[.507937,1,1],[.666667,.0625,.0625],[.68254,0,0],[1,0,0]],blue:[[0,0,0],[.333333,0,0],[.349206,.0625,.0625],[.507937,1,1],[.84127,1,1],[.857143,.9375,.9375],[1,.09375,.09375]]}},hot:{name:"Hot",numColors:256,gamma:1,segmentedData:{red:[[0,.0416,.0416],[.365079,1,1],[1,1,1]],green:[[0,0,0],[.365079,0,0],[.746032,1,1],[1,1,1]],blue:[[0,0,0],[.746032,0,0],[1,1,1]]}},cool:{name:"Cool",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[1,1,1]],green:[[0,1,1],[1,0,0]],blue:[[0,1,1],[1,1,1]]}},spring:{name:"Spring",numColors:256,gamma:1,segmentedData:{red:[[0,1,1],[1,1,1]],green:[[0,0,0],[1,1,1]],blue:[[0,1,1],[1,0,0]]}},summer:{name:"Summer",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[1,1,1]],green:[[0,.5,.5],[1,1,1]],blue:[[0,.4,.4],[1,.4,.4]]}},autumn:{name:"Autumn",numColors:256,gamma:1,segmentedData:{red:[[0,1,1],[1,1,1]],green:[[0,0,0],[1,1,1]],blue:[[0,0,0],[1,0,0]]}},winter:{name:"Winter",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[1,0,0]],green:[[0,0,0],[1,1,1]],blue:[[0,1,1],[1,.5,.5]]}},bone:{name:"Bone",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[.746032,.652778,.652778],[1,1,1]],green:[[0,0,0],[.365079,.319444,.319444],[.746032,.777778,.777778],[1,1,1]],blue:[[0,0,0],[.365079,.444444,.444444],[1,1,1]]}},copper:{name:"Copper",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[.809524,1,1],[1,1,1]],green:[[0,0,0],[1,.7812,.7812]],blue:[[0,0,0],[1,.4975,.4975]]}},spectral:{name:"Spectral",numColors:256,gamma:1,segmentedData:{red:[[0,0,0],[.05,.4667,.4667],[.1,.5333,.5333],[.15,0,0],[.2,0,0],[.25,0,0],[.3,0,0],[.35,0,0],[.4,0,0],[.45,0,0],[.5,0,0],[.55,0,0],[.6,0,0],[.65,.7333,.7333],[.7,.9333,.9333],[.75,1,1],[.8,1,1],[.85,1,1],[.9,.8667,.8667],[.95,.8,.8],[1,.8,.8]],green:[[0,0,0],[.05,0,0],[.1,0,0],[.15,0,0],[.2,0,0],[.25,.4667,.4667],[.3,.6,.6],[.35,.6667,.6667],[.4,.6667,.6667],[.45,.6,.6],[.5,.7333,.7333],[.55,.8667,.8667],[.6,1,1],[.65,1,1],[.7,.9333,.9333],[.75,.8,.8],[.8,.6,.6],[.85,0,0],[.9,0,0],[.95,0,0],[1,.8,.8]],blue:[[0,0,0],[.05,.5333,.5333],[.1,.6,.6],[.15,.6667,.6667],[.2,.8667,.8667],[.25,.8667,.8667],[.3,.8667,.8667],[.35,.6667,.6667],[.4,.5333,.5333],[.45,0,0],[.5,0,0],[.55,0,0],[.6,0,0],[.65,0,0],[.7,0,0],[.75,0,0],[.8,0,0],[.85,0,0],[.9,0,0],[.95,0,0],[1,.8,.8]]}},coolwarm:{name:"CoolWarm",numColors:256,gamma:1,segmentedData:{red:[[0,.2298057,.2298057],[.03125,.26623388,.26623388],[.0625,.30386891,.30386891],[.09375,.342804478,.342804478],[.125,.38301334,.38301334],[.15625,.424369608,.424369608],[.1875,.46666708,.46666708],[.21875,.509635204,.509635204],[.25,.552953156,.552953156],[.28125,.596262162,.596262162],[.3125,.639176211,.639176211],[.34375,.681291281,.681291281],[.375,.722193294,.722193294],[.40625,.761464949,.761464949],[.4375,.798691636,.798691636],[.46875,.833466556,.833466556],[.5,.865395197,.865395197],[.53125,.897787179,.897787179],[.5625,.924127593,.924127593],[.59375,.944468518,.944468518],[.625,.958852946,.958852946],[.65625,.96732803,.96732803],[.6875,.969954137,.969954137],[.71875,.966811177,.966811177],[.75,.958003065,.958003065],[.78125,.943660866,.943660866],[.8125,.923944917,.923944917],[.84375,.89904617,.89904617],[.875,.869186849,.869186849],[.90625,.834620542,.834620542],[.9375,.795631745,.795631745],[.96875,.752534934,.752534934],[1,.705673158,.705673158]],green:[[0,.298717966,.298717966],[.03125,.353094838,.353094838],[.0625,.406535296,.406535296],[.09375,.458757618,.458757618],[.125,.50941904,.50941904],[.15625,.558148092,.558148092],[.1875,.604562568,.604562568],[.21875,.648280772,.648280772],[.25,.688929332,.688929332],[.28125,.726149107,.726149107],[.3125,.759599947,.759599947],[.34375,.788964712,.788964712],[.375,.813952739,.813952739],[.40625,.834302879,.834302879],[.4375,.849786142,.849786142],[.46875,.860207984,.860207984],[.5,.86541021,.86541021],[.53125,.848937047,.848937047],[.5625,.827384882,.827384882],[.59375,.800927443,.800927443],[.625,.769767752,.769767752],[.65625,.734132809,.734132809],[.6875,.694266682,.694266682],[.71875,.650421156,.650421156],[.75,.602842431,.602842431],[.78125,.551750968,.551750968],[.8125,.49730856,.49730856],[.84375,.439559467,.439559467],[.875,.378313092,.378313092],[.90625,.312874446,.312874446],[.9375,.24128379,.24128379],[.96875,.157246067,.157246067],[1,.01555616,.01555616]],blue:[[0,.753683153,.753683153],[.03125,.801466763,.801466763],[.0625,.84495867,.84495867],[.09375,.883725899,.883725899],[.125,.917387822,.917387822],[.15625,.945619588,.945619588],[.1875,.968154911,.968154911],[.21875,.98478814,.98478814],[.25,.995375608,.995375608],[.28125,.999836203,.999836203],[.3125,.998151185,.998151185],[.34375,.990363227,.990363227],[.375,.976574709,.976574709],[.40625,.956945269,.956945269],[.4375,.931688648,.931688648],[.46875,.901068838,.901068838],[.5,.865395561,.865395561],[.53125,.820880546,.820880546],[.5625,.774508472,.774508472],[.59375,.726736146,.726736146],[.625,.678007945,.678007945],[.65625,.628751763,.628751763],[.6875,.579375448,.579375448],[.71875,.530263762,.530263762],[.75,.481775914,.481775914],[.78125,.434243684,.434243684],[.8125,.387970225,.387970225],[.84375,.343229596,.343229596],[.875,.300267182,.300267182],[.90625,.259301199,.259301199],[.9375,.220525627,.220525627],[.96875,.184115123,.184115123],[1,.150232812,.150232812]]}},blues:{name:"Blues",numColors:256,gamma:1,segmentedData:{red:[[0,.9686274528503418,.9686274528503418],[.125,.8705882430076599,.8705882430076599],[.25,.7764706015586853,.7764706015586853],[.375,.6196078658103943,.6196078658103943],[.5,.41960784792900085,.41960784792900085],[.625,.25882354378700256,.25882354378700256],[.75,.12941177189350128,.12941177189350128],[.875,.0313725508749485,.0313725508749485],[1,.0313725508749485,.0313725508749485]],green:[[0,.9843137264251709,.9843137264251709],[.125,.9215686321258545,.9215686321258545],[.25,.8588235378265381,.8588235378265381],[.375,.7921568751335144,.7921568751335144],[.5,.6823529601097107,.6823529601097107],[.625,.572549045085907,.572549045085907],[.75,.4431372582912445,.4431372582912445],[.875,.3176470696926117,.3176470696926117],[1,.1882352977991104,.1882352977991104]],blue:[[0,1,1],[.125,.9686274528503418,.9686274528503418],[.25,.9372549057006836,.9372549057006836],[.375,.8823529481887817,.8823529481887817],[.5,.8392156958580017,.8392156958580017],[.625,.7764706015586853,.7764706015586853],[.75,.7098039388656616,.7098039388656616],[.875,.6117647290229797,.6117647290229797],[1,.41960784792900085,.41960784792900085]]}}};function B(e,t){for(var r=0,n=e.length-1;r<=n;){var a=r+Math.floor((n-r)/2),i=e[a];if(i===t)return a;tt.timeStamp?-1:e.timeStamp=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,o=!0,l=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return o=e.done,e},e:function(e){l=!0,i=e},f:function(){try{o||null==r.return||r.return()}finally{if(l)throw i}}}}function ce(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r>8,r[n++]=t[a]<0?0:1}return r}};Ue.frag="precision mediump float;uniform sampler2D u_image;uniform float ww;uniform float wc;uniform float slope;uniform float intercept;uniform int invert;varying vec2 v_texCoord;void main() {vec4 color = texture2D(u_image, v_texCoord);float intensity = color.r*256.0 + color.g*65536.0;if (color.b == 0.0)intensity = -intensity;intensity = intensity * slope + intercept;float center0 = wc - 0.5;float width0 = max(ww, 1.0);intensity = (intensity - center0) / width0 + 0.5;intensity = clamp(intensity, 0.0, 1.0);gl_FragColor = vec4(intensity, intensity, intensity, 1.0);if (invert == 1)gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;}";var Be={};var ke={storedPixelDataToImageData:function(e){for(var t=e.getPixelData(),r=new Uint8Array(e.width*e.height*2),n=0,a=0;a>8}return r}};ze.frag="precision mediump float;uniform sampler2D u_image;uniform float ww;uniform float wc;uniform float slope;uniform float intercept;uniform int invert;varying vec2 v_texCoord;void main() {vec4 color = texture2D(u_image, v_texCoord);float intensity = color.r*256.0 + color.a*65536.0;intensity = intensity * slope + intercept;float center0 = wc - 0.5;float width0 = max(ww, 1.0);intensity = (intensity - center0) / width0 + 0.5;intensity = clamp(intensity, 0.0, 1.0);gl_FragColor = vec4(intensity, intensity, intensity, 1.0);if (invert == 1)gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;}";var We={};var je={storedPixelDataToImageData:function(e){return e.getPixelData()}};We.frag="precision mediump float;uniform sampler2D u_image;uniform float ww;uniform float wc;uniform float slope;uniform float intercept;uniform int invert;varying vec2 v_texCoord;void main() {vec4 color = texture2D(u_image, v_texCoord);float intensity = color.r*256.0;intensity = intensity * slope + intercept;float center0 = wc - 0.5;float width0 = max(ww, 1.0);intensity = (intensity - center0) / width0 + 0.5;intensity = clamp(intensity, 0.0, 1.0);gl_FragColor = vec4(intensity, intensity, intensity, 1.0);if (invert == 1)gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;}";var He={int16:Ue,int8:Be,rgb:Fe,uint16:ze,uint8:We},Xe={int16:Ne,int8:ke,rgb:Ge,uint16:qe,uint8:je},Ye="attribute vec2 a_position;attribute vec2 a_texCoord;uniform vec2 u_resolution;varying vec2 v_texCoord;void main() {vec2 zeroToOne = a_position / u_resolution;vec2 zeroToTwo = zeroToOne * 2.0;vec2 clipSpace = zeroToTwo - 1.0;gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);v_texCoord = a_texCoord;}",Ke={},Ze=[],$e=268435456,Je=0;function Qe(){if(!(Je<=$e)){for(Ze.sort(function(e,t){return e.timeStamp>t.timeStamp?-1:e.timeStampe.length)&&(t=e.length);for(var r=0,n=new Array(t);r>o,s=a.lut[a.lut.length-1]>>o,u=a.firstValueMapped+a.lut.length-1,function(e){return e>o:a.lut[e-a.firstValueMapped]>>o}):(d=e,m=t,function(e){return 255*((e-m)/d+.5)});var a,i,o,l,s,u,d,m},Et=function(e,t,r,n,a,i){var o=e.maxPixelValue,l=e.minPixelValue,s=Math.min(l,0);if(void 0===e.cachedLut){var u=o-s+1;e.cachedLut={},e.cachedLut.lutArray=new Uint8ClampedArray(u)}var d=e.cachedLut.lutArray,m=Boolean(e.slope%1)||Boolean(e.intercept%1),c=wt(e.slope,e.intercept,a),f=xt(t,r,i,m);if(!0===n)for(var g=l;g<=o;g++)d[g+-s]=255-f(c(g));else for(var v=l;v<=o;v++)d[v+-s]=f(c(v));return d},It=function(e,t,r){return void 0!==e.cachedLut&&e.cachedLut.windowCenter===t.voi.windowCenter&&e.cachedLut.windowWidth===t.voi.windowWidth&&pt(e.cachedLut.modalityLUT,t.modalityLUT)&&pt(e.cachedLut.voiLUT,t.voiLUT)&&e.cachedLut.invert===t.invert&&!0!==r||(ht(t,e),Et(e,t.voi.windowWidth,t.voi.windowCenter,t.invert,t.modalityLUT,t.voiLUT),e.cachedLut.windowWidth=t.voi.windowWidth,e.cachedLut.windowCenter=t.voi.windowCenter,e.cachedLut.invert=t.invert,e.cachedLut.voiLUT=t.voiLUT,e.cachedLut.modalityLUT=t.modalityLUT),e.cachedLut.lutArray},Tt=function(e,t){var r=e.renderingTools.lastRenderedImageId,n=e.renderingTools.lastRenderedViewport;return t.imageId!==r||!n||n.windowCenter!==e.viewport.voi.windowCenter||n.windowWidth!==e.viewport.voi.windowWidth||n.invert!==e.viewport.invert||n.rotation!==e.viewport.rotation||n.hflip!==e.viewport.hflip||n.vflip!==e.viewport.vflip||n.modalityLUT!==e.viewport.modalityLUT||n.voiLUT!==e.viewport.voiLUT||n.colormap!==e.viewport.colormap},Ct=function(e,t){var r=e.renderingTools.renderCanvas;r.width=t.width,r.height=t.height;var n=r.getContext("2d",{desynchronized:!0});n.fillStyle="white",n.fillRect(0,0,r.width,r.height);var a=n.getImageData(0,0,t.width,t.height);e.renderingTools.renderCanvasContext=n,e.renderingTools.renderCanvasData=a},Lt=function(e){var t=e.image.imageId,r=e.viewport,n=e.image.color;return e.renderingTools.lastRenderedImageId=t,e.renderingTools.lastRenderedIsColor=n,e.renderingTools.lastRenderedViewport={windowCenter:r.voi.windowCenter,windowWidth:r.voi.windowWidth,invert:r.invert,rotation:r.rotation,hflip:r.hflip,vflip:r.vflip,modalityLUT:r.modalityLUT,voiLUT:r.voiLUT,colormap:r.colormap},e.renderingTools};function Pt(e,t,r){var n=!(3e.byteArray.length-e.position&&(d=e.byteArray.length-e.position),t.fragments.push({offset:e.position-o-8,position:e.position,length:d}),e.seek(d),void(t.length=e.position-t.dataOffset);t.fragments.push({offset:e.position-o-8,position:e.position,length:d}),e.seek(d)}r&&r.push("pixel data element ".concat(t.tag," missing sequence delimiter tag xfffee0dd"))}function y(e,t){if(void 0===e)throw"dicomParser.findAndSetUNElementLength: missing required parameter 'byteStream'";for(var r=e.byteArray.length-8;e.position<=r;)if(65534===e.readUint16()){var a=e.readUint16();if(57565===a)return 0!==e.readUint32()&&e.warnings("encountered non zero length following item delimiter at position ".concat(e.position-4," while reading element of undefined length with tag ").concat(t.tag)),void(t.length=e.position-t.dataOffset)}t.length=e.byteArray.length-t.dataOffset,e.seek(e.byteArray.length-e.position)}function b(e,t,r){if(r<0)throw"dicomParser.readFixedString - length cannot be less than 0";if(t+r>e.length)throw"dicomParser.readFixedString: attempt to read past end of buffer";for(var a,n="",i=0;it.byteArray.length)throw"dicomParser.parseDicomDataSetExplicit: invalid value for parameter 'maxP osition'";for(var n=e.elements;t.positionr)throw"dicomParser:parseDicomDataSetExplicit: buffer overrun"}function T(e,t,r){var a=3t.byteArray.length)throw"dicomParser.parseDicomDataSetImplicit: invalid value for parameter 'maxPosition'";for(var n=e.elements;t.positione.length)throw"bigEndianByteArrayParser.readUint16: attempt to read past end of buffer";return(e[t]<<8)+e[t+1]},readInt16:function(e,t){if(t<0)throw"bigEndianByteArrayParser.readInt16: position cannot be less than 0";if(t+2>e.length)throw"bigEndianByteArrayParser.readInt16: attempt to read past end of buffer";t=(e[t]<<8)+e[t+1];return t=32768&t?t-65535-1:t},readUint32:function(e,t){if(t<0)throw"bigEndianByteArrayParser.readUint32: position cannot be less than 0";if(t+4>e.length)throw"bigEndianByteArrayParser.readUint32: attempt to read past end of buffer";return 256*(256*(256*e[t]+e[t+1])+e[t+2])+e[t+3]},readInt32:function(e,t){if(t<0)throw"bigEndianByteArrayParser.readInt32: position cannot be less than 0";if(t+4>e.length)throw"bigEndianByteArrayParser.readInt32: attempt to read past end of buffer";return(e[t]<<24)+(e[t+1]<<16)+(e[t+2]<<8)+e[t+3]},readFloat:function(e,t){if(t<0)throw"bigEndianByteArrayParser.readFloat: position cannot be less than 0";if(t+4>e.length)throw"bigEndianByteArrayParser.readFloat: attempt to read past end of buffer";var r=new Uint8Array(4);return r[3]=e[t],r[2]=e[t+1],r[1]=e[t+2],r[0]=e[t+3],new Float32Array(r.buffer)[0]},readDouble:function(e,t){if(t<0)throw"bigEndianByteArrayParser.readDouble: position cannot be less than 0";if(t+8>e.length)throw"bigEndianByteArrayParser.readDouble: attempt to read past end of buffer";var r=new Uint8Array(8);return r[7]=e[t],r[6]=e[t+1],r[5]=e[t+2],r[4]=e[t+3],r[3]=e[t+4],r[2]=e[t+5],r[1]=e[t+6],r[0]=e[t+7],new Float64Array(r.buffer)[0]}};function j(e,t,r){if("undefined"!=typeof Buffer&&e instanceof Buffer)return e.slice(t,t+r);if(e instanceof Uint8Array)return new Uint8Array(e.buffer,e.byteOffset+t,r);throw"dicomParser.from: unknown type for byteArray"}function C(e,t){for(var r=0;r=t.length)throw"dicomParser.ByteStream: parameter 'position' cannot be greater than or equal to 'byteArray' length";this.byteArrayParser=e,this.byteArray=t,this.position=r||0,this.warnings=[]}var e,t,r;return e=a,(t=[{key:"seek",value:function(e){if(this.position+e<0)throw"dicomParser.ByteStream.prototype.seek: cannot seek to position < 0";this.position+=e}},{key:"readByteStream",value:function(e){if(this.position+e>this.byteArray.length)throw"dicomParser.ByteStream.prototype.readByteStream: readByteStream - buffer overread";var t=j(this.byteArray,this.position,e);return this.position+=e,new a(this.byteArrayParser,t)}},{key:"readUint16",value:function(){var e=this.byteArrayParser.readUint16(this.byteArray,this.position);return this.position+=2,e}},{key:"readUint32",value:function(){var e=this.byteArrayParser.readUint32(this.byteArray,this.position);return this.position+=4,e}},{key:"readFixedString",value:function(e){var t=b(this.byteArray,this.position,e);return this.position+=e,t}}])&&C(e.prototype,t),r&&C(e,r),a}(),M={readUint16:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readUint16: position cannot be less than 0";if(t+2>e.length)throw"littleEndianByteArrayParser.readUint16: attempt to read past end of buffer";return e[t]+256*e[t+1]},readInt16:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readInt16: position cannot be less than 0";if(t+2>e.length)throw"littleEndianByteArrayParser.readInt16: attempt to read past end of buffer";t=e[t]+(e[t+1]<<8);return t=32768&t?t-65535-1:t},readUint32:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readUint32: position cannot be less than 0";if(t+4>e.length)throw"littleEndianByteArrayParser.readUint32: attempt to read past end of buffer";return e[t]+256*e[t+1]+256*e[t+2]*256+256*e[t+3]*256*256},readInt32:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readInt32: position cannot be less than 0";if(t+4>e.length)throw"littleEndianByteArrayParser.readInt32: attempt to read past end of buffer";return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24)},readFloat:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readFloat: position cannot be less than 0";if(t+4>e.length)throw"littleEndianByteArrayParser.readFloat: attempt to read past end of buffer";var r=new Uint8Array(4);return r[0]=e[t],r[1]=e[t+1],r[2]=e[t+2],r[3]=e[t+3],new Float32Array(r.buffer)[0]},readDouble:function(e,t){if(t<0)throw"littleEndianByteArrayParser.readDouble: position cannot be less than 0";if(t+8>e.length)throw"littleEndianByteArrayParser.readDouble: attempt to read past end of buffer";var r=new Uint8Array(8);return r[0]=e[t],r[1]=e[t+1],r[2]=e[t+2],r[3]=e[t+3],r[4]=e[t+4],r[5]=e[t+5],r[6]=e[t+6],r[7]=e[t+7],new Float64Array(r.buffer)[0]}};function G(e,t){if(void 0===e)throw"dicomParser.readPart10Header: missing required parameter 'byteArray'";var i=new J(M,e);return function(){!function(){if(i.seek(128),"DICM"!==i.readFixedString(4))throw"dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file."}();for(var e=[],t={};i.position= 0";if(r>=t.fragments.length)throw"dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'startFragmentIndex' must be < number of fragments";if(a<1)throw"dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'numFragments' must be > 0";if(r+a>t.fragments.length)throw"dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'startFragment' + 'numFragments' < number of fragments";var i=new J(e.byteArrayParser,e.byteArray,t.dataOffset),t=S(i);if("xfffee000"!==t.tag)throw"dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000";i.seek(t.length);var o=i.position;if(1===a)return j(i.byteArray,o+n[r].offset+8,n[r].length);for(var t=R(n,r,a),s=k(i.byteArray,t),d=0,f=r;f= 0";if(r>=a.length)throw"dicomParser.readEncapsulatedImageFrame: parameter 'frameIndex' must be < basicOffsetTable.length";var i=a[r],i=H(n,i);if(void 0===i)throw"dicomParser.readEncapsulatedImageFrame: unable to find fragment that matches basic offset table entry";return _(e,t,i,Q(r,a,n,i),n)}var W=!1;function $(e,t,r){if(W||(W=!0,console&&console.log&&console.log("WARNING: dicomParser.readEncapsulatedPixelData() has been deprecated")),void 0===e)throw"dicomParser.readEncapsulatedPixelData: missing required parameter 'dataSet'";if(void 0===t)throw"dicomParser.readEncapsulatedPixelData: missing required parameter 'element'";if(void 0===r)throw"dicomParser.readEncapsulatedPixelData: missing required parameter 'frame'";if("x7fe00010"!==t.tag)throw"dicomParser.readEncapsulatedPixelData: parameter 'element' refers to non pixel data tag (expected tag = x7fe00010)";if(!0!==t.encapsulatedPixelData)throw"dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data";if(!0!==t.hadUndefinedLength)throw"dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data";if(void 0===t.basicOffsetTable)throw"dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data";if(void 0===t.fragments)throw"dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data";if(r<0)throw"dicomParser.readEncapsulatedPixelData: parameter 'frame' must be >= 0";return 0!==t.basicOffsetTable.length?V(e,t,r):_(e,t,0,t.fragments.length)}t.default={isStringVr:d,isPrivateTag:f,parsePN:a,parseTM:n,parseDA:o,explicitElementToString:l,explicitDataSetToJS:u,createJPEGBasicOffsetTable:p,parseDicomDataSetExplicit:q,parseDicomDataSetImplicit:T,readFixedString:b,alloc:k,version:L,bigEndianByteArrayParser:N,ByteStream:J,sharedCopy:j,DataSet:w,findAndSetUNElementLength:y,findEndOfEncapsulatedElement:g,findItemDelimitationItemAndSetElementLength:x,littleEndianByteArrayParser:M,parseDicom:z,readDicomElementExplicit:B,readDicomElementImplicit:A,readEncapsulatedImageFrame:V,readEncapsulatedPixelData:$,readEncapsulatedPixelDataFromFragments:_,readPart10Header:G,readSequenceItemsExplicit:I,readSequenceItemsImplicit:F,readSequenceItem:S,readTag:h}}],i={},n.m=a,n.c=i,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)n.d(r,a,function(e){return t[e]}.bind(null,a));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1);function n(e){if(i[e])return i[e].exports;var t=i[e]={i:e,l:!1,exports:{}};return a[e].call(t.exports,t,t.exports,n),t.l=!0,t.exports}var a,i}); +//# sourceMappingURL=dicomParser.min.js.map \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..18a028e25c7526dbc087bf086c85f29f64289b7d --- /dev/null +++ b/public/index.html @@ -0,0 +1,198 @@ + + + + + MRI + + + + +
+
+
+

Source

+

(images before)

+ + +
Images
+
    +
    Parameters
    +
    +
    Center
    +
    + + + + +
    + +
    Outer circle
    +
    + + + + +
    + +
    Inner circle
    +
    + + + + +
    + +
    + + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +

    Target

    +

    (images after)

    + + +
    Images
    +
      +
      Parameters
      +
      +
      Center
      +
      + + + + +
      + +
      Outer circle
      +
      + + + + +
      + +
      Inner circle
      +
      + + + + +
      + +
      + + + + +
      +
      + +
      +
      +
      +
      + + + +
      + + +
      + + + + + + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000000000000000000000000000000000000..479c8beca6b9652e4f7e4744cf7084d195345b57 --- /dev/null +++ b/public/script.js @@ -0,0 +1,1007 @@ +const dotColor = [0, 191, 255]; +const innerCircleColor = [0, 255, 0]; +const outerCircleColor = [255, 0, 0]; +const contourLineColor = [255, 0, 0]; +const dartboardColors = [ + [255, 0, 0], + [255, 255, 0], + [0, 255, 0], + [0, 255, 255], + [0, 0, 255], + [255, 0, 255], + [255, 255, 255] +]; + +cornerstone.enable(document.getElementById("element1")); +cornerstone.enable(document.getElementById("element2")); +cornerstone.enable(document.getElementById("element3")); +let files = [[], []]; +let dicoms = []; +let initialDICOM = { + parameters: { + height: 0, + width: 0, + inputParameters: { + outerX: 0, + outerY: 0, + innerX: 0, + innerY: 0, + centerX: 0, + centerY: 0, + K1: 4.5, + K2: 4.5 + } + }, + pixelData: [], + segmented: [], + layers: [] +}; + +for (let i = 0; i < 5; i++) + dicoms[i] = JSON.parse(JSON.stringify(initialDICOM)); + +/** + * Returns array's maximum. + * + * @param {Object} arr Non-empty array of comparable elements. + * @return {number} Array's maximum. + */ +function arrayMax(arr) { + let max = arr[0]; + + for (let i = 1; i < arr.length; i++) + if (arr[i] > max) + max = arr[i]; + + return max; +} + +/** + * Returns array's minimum. + * + * @param {Object} arr Non-empty array of comparable elements. + * @return {number} Array's minimum. + */ +function arrayMin(arr) { + let min = arr[0]; + + for (let i = 1; i < arr.length; i++) + if (arr[i] < min) + min = arr[i]; + + return min; +} + +/** + * Returns dot product of two vectors. + * + * @param {number[]} u First vector (at least 2 elements). + * @param {number[]} v Second vector (at least 2 elements). + * @returns {number} Vectors' dot product. + */ +function dot(u, v) { + return u[0] * v[0] + u[1] * v[1]; +} + +/** + * Returns norm of a vector. + * + * @param {number[]} u Vector (at least 2 elements). + * @returns {number} Vector's norm. + */ +function norm(u) { + return Math.sqrt(dot(u, u)); +} + +/** + * Creates HTML button with given onclick attribute and inner text. + * + * @param {string} onclick Onclick attribute value. + * @param {string} text Inner text. + * @returns {HTMLButtonElement} Created button. + */ +function createButton(onclick, text) { + let button = document.createElement("button"); + button.setAttribute("onclick", onclick); + button.setAttribute("style", "width: 15%;"); + button.innerText = text; + return button; +} + +/** + * Shows all hidden HTML buttons. + */ +function showMore() { + const buttons = document.getElementById("buttons"); + buttons.textContent = ''; + buttons.appendChild(createButton("registration(0)", "Register (source --> target)")); + buttons.appendChild(createButton("registration(1)", "Register (source <-- target)")); + buttons.appendChild(createButton("computeT1(0, false)", "Register (T1s --> T1t)")); + buttons.appendChild(createButton("computeT1(1, false)", "Register (T1s <-- T1t)")); + buttons.appendChild(createButton("computeT1(0, true)", "ECV (source --> target)")); + buttons.appendChild(createButton("computeT1(1, true)", "ECV (source <-- target)")); +} + +/** + * Zooms in given DICOM image. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function zoomIn(dicom) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + const viewport = cornerstone.getViewport(element); + viewport.scale += 0.25; + cornerstone.setViewport(element, viewport); +} + +/** + * Zooms out given DICOM image. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function zoomOut(dicom) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + const viewport = cornerstone.getViewport(element); + viewport.scale -= 0.25; + cornerstone.setViewport(element, viewport); +} + +/** + * Updates window center of given DICOM image's canvas. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + * @param {string} value New window center value. + */ +function updateWindowCenter(dicom, value) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + let viewport = cornerstone.getViewport(element); + viewport.voi.windowCenter = parseFloat(value); + cornerstone.setViewport(element, viewport); +} + +/** + * Updates window width of given DICOM image's canvas. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + * @param {string} value New window width value. + */ +function updateWindowWidth(dicom, value) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + let viewport = cornerstone.getViewport(element); + viewport.voi.windowWidth = parseFloat(value); + cornerstone.setViewport(element, viewport); +} + +/** + * Returns radius of circle defined by its center and a point through which the circle goes. + * + * @param {number} centerX X coordinate of circle's center. + * @param {number} centerY Y coordinate of circle's center. + * @param {number} X X coordinate of point through which the circle goes. + * @param {number} Y Y coordinate of point through which the circle goes. + * @returns {number} Circle's radius. + */ +function countRadius(centerX, centerY, X, Y) { + return Math.sqrt(Math.pow(X - centerX, 2.0) + Math.pow(Y - centerY, 2.0)); +} + +/** + * Returns Cornerstone Image object for given parameters and pixel data, if pixel data is RGBA, object must be colorized afterwards. + * + * @param {number} height Image's height. + * @param {number} width Image's width. + * @param {Object} pixelData Either DICOM MR image greyscale pixel data (Uint16Array) or some RGBA pixel data (number[]). + * @returns {Object} Resulting Cornerstone Image object. + */ +function getImage(height, width, pixelData) { + function getPixelData () { + return pixelData; + } + + let image = { + minPixelValue : arrayMin(pixelData), + maxPixelValue : arrayMax(pixelData), + slope: 1, + intercept: 0, + windowCenter : 376, + windowWidth : 917, + getPixelData: getPixelData, + rows: height, + columns: width, + height: height, + width: width, + color: false, + sizeInBytes: width * height * 2 + }; + + return image; +} + +/** + * Returns colorized Cornerstone Image object for given Cornerstone Image object. + * + * @param image Cornerstone Image object with RGBA pixel data. + * @returns {Object} Colorized Cornerstone Image object. + */ +function getColorImage(image) { + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const canvasContext = canvas.getContext('2d', { + desynchronized: true + }); + const imageData = canvasContext.createImageData(image.width, image.height); + imageData.data = image.getPixelData; + canvasContext.putImageData(imageData, 0, 0); + + function getImageData () { + return imageData; + } + + function getCanvas () { + return canvas; + } + + image.color = true; + image.sizeInBytes = image.width * image.height * 4; + image.render = cornerstone.renderColorImage; + image.getImageData = getImageData; + image.getCanvas = getCanvas; + + return image; +} + +/** + * Updates given layer of given DICOM image with given pixel data and opacity. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + * @param {number} layer Image layer identifier (0 for base, 1 for center, 2 for outer circle, 3 for inner circle, 4 for dartboard, 5 for contour line). + * @param {Object} pixelData Either DICOM MR image greyscale pixel data (Uint16Array) or some RGBA pixel data (number[]). + * @param {number} opacity Opacity (Number between 0 and 1). + * @param {boolean} color Whether pixel data is RGBA. + */ +function updateLayer(dicom, layer, pixelData, opacity, color) { + let image = getImage(dicoms[dicom].parameters.height, dicoms[dicom].parameters.width, pixelData); + if (color) { + image = getColorImage(image); + } + + let options = { + opacity: opacity + }; + + let element = document.getElementById("element" + (dicom + 1).toString()); + cornerstone.removeLayer(element, dicoms[dicom].layers[layer]); + dicoms[dicom].layers[layer] = cornerstone.addLayer(element, image, options); + cornerstone.updateImage(element); +} + +/** + * Returns constructed RGBA pixel data with one pixel colored based on parameters. + * + * @param {number} width Image's width. + * @param {number} height Image's height. + * @param {number} X X coordinate of pixel. + * @param {number} Y Y coordinate of pixel. + * @param {number[]} color RGB color (array of 3 numbers). + * @returns {number[]} Constructed RGBA pixel data. + */ +function makeDot(width, height, X, Y, color) { + let pixelData = []; + + for (let i = 0; i < 4 * width * height; i++) { + pixelData[i] = 0; + } + pixelData[4 * (Y * width + X)] = color[0]; + pixelData[4 * (Y * width + X) + 1] = color[1]; + pixelData[4 * (Y * width + X) + 2] = color[2]; + pixelData[4 * (Y * width + X) + 3] = 255; + + return pixelData; +} + +/** + * Returns constructed RGBA pixel data with a circle colored based on parameters. + * + * @param {number} width Image's width. + * @param {number} height Image's height. + * @param {number} centerX X coordinate of center. + * @param {number} centerY Y coordinate of center. + * @param {number} X X coordinate of point through which the circle goes. + * @param {number} Y Y coordinate of point through which the circle goes. + * @param {number[]} color RGB color (array of 3 numbers). + * @returns {number[]} Constructed RGBA pixel data. + */ +function makeCircle(width, height, centerX, centerY, X, Y, color) { + let pixelData = []; + let radius = countRadius(centerX, centerY, X, Y); + + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) + if (Math.round(countRadius(centerX, centerY, x, y)) === Math.round(radius)) { + pixelData[4 * (y * width + x)] = color[0]; + pixelData[4 * (y * width + x) + 1] = color[1]; + pixelData[4 * (y * width + x) + 2] = color[2]; + pixelData[4 * (y * width + x) + 3] = 255; + } + else { + pixelData[4 * (y * width + x)] = 0; + pixelData[4 * (y * width + x) + 1] = 0; + pixelData[4 * (y * width + x) + 2] = 0; + pixelData[4 * (y * width + x) + 3] = 0; + } + + return pixelData; +} + +/** + * Returns constructed segmentation dartboard based on given parameters and data. + * + * @param {number} centerX X coordinate of center. + * @param {number} centerY Y coordinate of center. + * @param {number} outerX X coordinate of point through which the outer circle goes. + * @param {number} outerY Y coordinate of point through which the outer circle goes. + * @param {number[]} segmented Segmented pixel data. + * @param {number} width Image's width. + * @param {number} height Image's height. + * @returns {boolean[][]} Constructed dartboard (for each of 6 segments a mask indicating which pixels should be colored). + */ +function makeDartboard(centerX, centerY, outerX, outerY, segmented, height, width) { + let masks = []; + let v1 = [outerX - centerX + 0.5, outerY - centerY + 0.5]; + let nv1 = norm(v1); + v1 = [v1[0] / nv1, v1[1] / nv1]; + + for (let i = 0; i < 7; i++) { + let mask = []; + if (i < 6) + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) { + let v2 = [x - centerX + 0.5, y - centerY + 0.5]; + let nv2 = norm(v2); + let sector = null; + if (nv2 === 0) + sector = 0; + else { + v2 = [v2[0] / nv2, v2[1] / nv2]; + let arg = Math.min(1, Math.max(-1, dot(v1, v2))); + let phi = (Math.acos(arg) * 180) / Math.PI; + if (v1[0] * v2[1] - v1[1] * v2[0] > 0) + phi = 360 - phi; + sector = (Math.floor(phi / 60)) % 6; + } + mask[y * width + x] = (segmented[y * width + x] < 0 && sector === i); + } + else { + let radius = countRadius(centerX, centerY, outerX, outerY) / 2; + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) + mask[y * width + x] = segmented[y * width + x] > 0 && countRadius(centerX, centerY, x, y) <= radius; + } + masks[i] = mask; + } + + return masks; +} + +/** + * For given DICOM image returns constructed RGBA pixel data with colored segmentation dartboard. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup), DICOM image must be segmented. + * @returns {number[]} Constructed RGBA pixel data. + */ +function makeColoredDartboard(dicom) { + let pixelData = []; + let centerX = dicoms[dicom].parameters.inputParameters.centerX; + let centerY = dicoms[dicom].parameters.inputParameters.centerY; + let outerX = dicoms[dicom].parameters.inputParameters.outerX; + let outerY = dicoms[dicom].parameters.inputParameters.outerY; + let height = dicoms[dicom].parameters.height; + let width = dicoms[dicom].parameters.width; + + let masks = makeDartboard(centerX, centerY, outerX, outerY, dicoms[dicom].segmented, height, width); + + for (let i = 0; i < 4 * width * height; i++) + pixelData[i] = 0; + for (let j = 0; j < 6; j++) + for (let i = 0; i < width * height; i++) + if (masks[j][i]) { + pixelData[4 * i] = dartboardColors[j][0]; + pixelData[4 * i + 1] = dartboardColors[j][1]; + pixelData[4 * i + 2] = dartboardColors[j][2]; + pixelData[4 * i + 3] = 255; + } + + return pixelData; +} + +/** + * For given DICOM image returns constructed RGBA pixel data with colored contour line. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup), DICOM image must be segmented. + * @param {number[]} color RGB color (array of 3 numbers). + * @returns {number[]} Constructed RGBA pixel data. + */ +function makeContourLine(dicom, color) { + let height = dicoms[dicom].parameters.height; + let width = dicoms[dicom].parameters.width; + let v = dicoms[dicom].segmented; + let u = []; + let pixelData = []; + + for (let i = 0; i < height; i++) + for (let j = 0; j < width; j++) + if (i > 0 && j > 0 && (v[i * width + j] * v[(i - 1) * width + j] < 0 || v[i * width + j] * v[i * width + j - 1] < 0)) + u[i * width + j] = true; + else + u[i * width + j] = false; + + for (let i = 0; i < width * height; i++) + if (u[i]) { + pixelData[4 * i] = color[0]; + pixelData[4 * i + 1] = color[1]; + pixelData[4 * i + 2] = color[2]; + pixelData[4 * i + 3] = 255; + } + else { + pixelData[4 * i] = 0; + pixelData[4 * i + 1] = 0; + pixelData[4 * i + 2] = 0; + pixelData[4 * i + 3] = 0; + } + + return pixelData; +} + +/** + * For given DICOM image shows center, inner circle and outer circle based on its parameters. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function makeCircles(dicom) { + let height = dicoms[dicom].parameters.height; + let width = dicoms[dicom].parameters.width; + let centerX = dicoms[dicom].parameters.inputParameters.centerX; + let centerY = dicoms[dicom].parameters.inputParameters.centerY; + let outerX = dicoms[dicom].parameters.inputParameters.outerX; + let outerY = dicoms[dicom].parameters.inputParameters.outerY; + let innerX = dicoms[dicom].parameters.inputParameters.innerX; + let innerY = dicoms[dicom].parameters.inputParameters.innerY; + + cornerstone.removeLayer(document.getElementById("element" + (dicom + 1).toString()), dicoms[dicom].layers[4]); + cornerstone.removeLayer(document.getElementById("element" + (dicom + 1).toString()), dicoms[dicom].layers[5]); + let pixelData1 = makeDot(width, height, centerX, centerY, dotColor); + updateLayer(dicom, 1, pixelData1, 1.0, true); + let pixelData2 = makeCircle(width, height, centerX, centerY, outerX, outerY, outerCircleColor); + updateLayer(dicom, 2, pixelData2, 1.0, true); + let pixelData3 = makeCircle(width, height, centerX, centerY, innerX, innerY, innerCircleColor); + updateLayer(dicom, 3, pixelData3, 1.0, true); +} + +/** + * For given DICOM image checks validity of its input parameters' HTML input elements and updates both HTML and JS data accordingly. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + */ +function inputParametersUpdate(dicom) { + for (const [key, value] of Object.entries(dicoms[dicom].parameters.inputParameters)) { + let parameter = document.getElementById(key + (dicom + 1).toString()); + if (parameter.checkValidity()) { + dicoms[dicom].parameters.inputParameters[key] = Number(parameter.value); + } + else { + parameter.value = value; + } + } + + if (dicoms[dicom].parameters.width === 0) + return; + + dicoms[dicom].segmented = []; + makeCircles(dicom); +} + +/** + * Pans given DICOM image. + * + * @param {Object} event Mousedown event. + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function pan(event, dicom) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + let lastX = event.pageX; + let lastY = event.pageY; + + function mouseMoveHandler(event) { + const deltaX = event.pageX - lastX; + const deltaY = event.pageY - lastY; + lastX = event.pageX; + lastY = event.pageY; + + const viewport = cornerstone.getViewport(element); + viewport.translation.x += (deltaX / viewport.scale); + viewport.translation.y += (deltaY / viewport.scale); + cornerstone.setViewport(element, viewport); + } + + function mouseUpHandler() { + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + } + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); +} + +/** + * For given DICOM image sets maximal possible x and y coordinates of its input parameters. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + */ +function setMaxes(dicom) { + for (const element of ["centerX", "outerX", "innerX"]) { + let parameter = document.getElementById(element + (dicom + 1).toString()); + parameter.setAttribute("max", (dicoms[dicom].parameters.width - 1).toString()); + } + for (const element of ["centerY", "outerY", "innerY"]) { + let parameter = document.getElementById(element + (dicom + 1).toString()); + parameter.setAttribute("max", (dicoms[dicom].parameters.height - 1).toString()); + } +} + +/** + * Resets HTML range input values of given DICOM image's window width and window center to default values. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function resetRanges(dicom) { + document.getElementById("wc" + (dicom + 1).toString()).value = 376; + document.getElementById("ww" + (dicom + 1).toString()).value = 917; +} + +/** + * Resets given DICOM image's window width, window center, scale, and position to default values. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup). + */ +function resetAdjusting(dicom) { + let element = document.getElementById("element" + (dicom + 1).toString()); + cornerstone.enable(element); + updateWindowCenter(dicom, "376"); + updateWindowWidth(dicom, "917"); + resetRanges(dicom); +} + +/** + * Resets given DICOM image as if it was empty, only canvas remains. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + */ +function resetDICOM(dicom) { + let element = document.getElementById("element" + (dicom + 1).toString()); + for (let i = 0; i < 6; i++) + cornerstone.removeLayer(element, dicoms[dicom].layers[i]); + dicoms[dicom] = JSON.parse(JSON.stringify(initialDICOM)); + + for (const [key, value] of Object.entries(dicoms[dicom].parameters.inputParameters)) { + let parameter = document.getElementById(key + (dicom + 1).toString()); + parameter.value = value; + } + + setMaxes(dicom); + resetRanges(dicom); + element.setAttribute("class", "contextMenu"); + element.setAttribute('onmousedown', "pan(event, " + dicom.toString() + ")"); +} + +/** + * Toggles given popup. + * + * @param {number} popup Popup identifier (0 for DICOM popup, 1 for ECV popup). + */ +function togglePopup(popup) { + document.getElementById("popup" + (popup + 1).toString()).classList.toggle("show"); + resetRanges(2); +} + +/** + * Sets given input parameter of given DICOM image. + * + * @param {Object} event Mousedown event. + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @param {string} parameter Parameter that should be set (center, outer or inner). + */ +function setParameter(event, dicom, parameter) { + let element = document.getElementById("element" + (dicom + 1).toString()); + const pixelCoords = cornerstone.pageToPixel(element, event.pageX, event.pageY); + let x = Math.round(pixelCoords.x); + let y = Math.round(pixelCoords.y); + + if (x < 0) { + x = 0; + } + if (x > dicoms[dicom].parameters.width - 1) { + x = dicoms[dicom].parameters.width - 1; + } + if (y < 0) { + y = 0; + } + if (y > dicoms[dicom].parameters.height - 1) { + y = dicoms[dicom].parameters.height - 1; + } + + document.getElementById(parameter + "X" + (dicom + 1).toString()).value = x; + document.getElementById(parameter + "Y" + (dicom + 1).toString()).value = y; + element.setAttribute("class", "move"); + element.setAttribute('onmousedown', "pan(event, " + dicom.toString() + ")"); + inputParametersUpdate(dicom); +} + +/** + * Sets listener for setting given parameter of given DICOM image. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @param {string} parameter Parameter that should be set (center, outer or inner). + */ +function setParameterListener(dicom, parameter) { + if (dicoms[dicom].parameters.width === 0) + return; + let element = document.getElementById("element" + (dicom + 1).toString()); + element.setAttribute("class", "crosshair"); + element.setAttribute('onmousedown', "setParameter(event, " + dicom.toString() + ", \"" + parameter + "\")"); +} + +/** + * Sets dimensions of given Cornerstone enabled element and its canvas based on given parameters (also resets scale and position). + * + * @param {HTMLElement} element A DICOM image's Cornerstone enabled element. + * @param {number} height + * @param {number} width + */ +function countDimensions(element, height, width) { + if (height > width) { + let w = Math.round(width / height * 10000) / 100; + element.setAttribute("style", "width:" + w.toString() + "%;height:100%"); + } + else { + let h = Math.round(height / width * 10000) / 100; + element.setAttribute("style", "width:100%;height:" + h.toString() + "%"); + } + cornerstone.enable(element); +} + +/** + * Reads file loaded by given reader and for given DICOM image updates JS object's dimensions and pixel data. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right, 2 for popup, 3 for left T1, 4 for right T1). + * @param {Object} reader Loaded file reader reading file as array buffer. + * @throws Throws exception if file has wrong DICOM format. + */ +function readDICOM(dicom, reader) { + // form dataset from DICOM file + let arrayBuffer = reader.result; + let byteArray = new Uint8Array(arrayBuffer); + let dataSet = dicomParser.parseDicom(byteArray); + + let photometricInterpretation = dataSet.string('x00280004'); + let modality = dataSet.string('x00080060'); + if (modality !== "MR" || photometricInterpretation !== "MONOCHROME2") + throw "Wrong DICOM format, modality must be MR and photometric interpretation must be MONOCHROME2."; + + // get necessary data from dataset + dicoms[dicom].parameters.height = dataSet.uint16('x00280010'); + dicoms[dicom].parameters.width = dataSet.uint16('x00280011'); + let pixelDataElement = dataSet.elements.x7fe00010; + let data = dataSet.byteArray.buffer.slice(pixelDataElement.dataOffset, pixelDataElement.dataOffset + pixelDataElement.length); + dicoms[dicom].pixelData = new Uint16Array(data); +} + +/** + * Views file defined by parameters. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @param {number} index File index in given DICOM image's file list. + */ +function viewDICOM(dicom, index) { + for (let i = 0; i < files[dicom].length; i++) + document.getElementById("file" + (i + 1).toString() + (dicom+ 1).toString()).removeAttribute("class"); + document.getElementById("file" + (index + 1).toString() + (dicom+ 1).toString()).setAttribute("class", + "active"); + let file = files[dicom][index]; + let reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = function() { + resetDICOM(dicom); + let element = document.getElementById('element' + (dicom + 1).toString()); + + try { + readDICOM(dicom, reader); + } + catch (e) { + element.setAttribute("style", "visibility: hidden;"); + return; + } + + setMaxes(dicom); + countDimensions(element, dicoms[dicom].parameters.height, dicoms[dicom].parameters.width); + updateLayer(dicom, 0, dicoms[dicom].pixelData, 1.0, false); + inputParametersUpdate(dicom); + element.setAttribute("class", "move"); + }; +} + +/** + * Updates given DICOM image's file list. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @param {Object} input File input. + */ +function updateFiles(dicom, input) { + if (input.files.length === 0) + return; + files[dicom] = []; + const list = document.getElementById("list" + (dicom + 1).toString()); + list.textContent = ''; + for (let i = 0; i < input.files.length; i++) { + files[dicom][i] = input.files[i]; + const li = document.createElement("li"); + li.innerText = input.files[i].name; + li.setAttribute("id", "file" + (i + 1).toString() + (dicom + 1).toString()); + li.setAttribute("onclick", "viewDICOM(" + dicom.toString() + ", "+i.toString()+")"); + list.appendChild(li); + } + viewDICOM(dicom, 0); +} + +/** + * Updates given DICOM image's T1 map. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @param {Object} input File input. + */ +function loadT1Map(dicom, input) { + if (input.files.length === 0) + return; + let file = input.files[0]; + let reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = function() { + dicoms[dicom + 3].pixelData = []; + try { + readDICOM(dicom + 3, reader); + } + catch (e) { + input.value = input.defaultValue; + window.alert("Wrong DICOM format."); + } + }; +} + +/** + * Checks if given DICOM image's input parameters are ready for segmentation. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + * @returns {boolean} + */ +function checkInputParameters(dicom) { + let height = dicoms[dicom].parameters.height; + let width = dicoms[dicom].parameters.width; + let centerX = dicoms[dicom].parameters.inputParameters.centerX; + let centerY = dicoms[dicom].parameters.inputParameters.centerY; + let outerX = dicoms[dicom].parameters.inputParameters.outerX; + let outerY = dicoms[dicom].parameters.inputParameters.outerY; + let innerX = dicoms[dicom].parameters.inputParameters.innerX; + let innerY = dicoms[dicom].parameters.inputParameters.innerY; + let innerRadius = countRadius(centerX, centerY, innerX, innerY); + let outerRadius = countRadius(centerX, centerY, outerX, outerY); + + if (innerRadius >= outerRadius) { + window.alert("Inner radius must be smaller than outer radius."); + return false; + } + if (outerRadius > centerX - 16 || outerRadius > centerY - 16 || + outerRadius > width - centerX - 17 || outerRadius > height - centerY - 17) { + window.alert("Circles are too close to edges."); + return false; + } + return true; +} + +/** + * Performs segmentation of given DICOM image. + * + * @param {number} dicom DICOM image identifier (0 for left, 1 for right). + */ +function segmentation(dicom) { + if (dicoms[dicom].parameters.width === 0 || dicoms[dicom].segmented.length !== 0) + return; + if (! checkInputParameters(dicom)) + return; + document.getElementById("cover-spin").classList.toggle("show"); + + const xhttp = new XMLHttpRequest(); + xhttp.onload = function() { + dicoms[dicom].segmented = new Float64Array(JSON.parse(this.responseText)); + let pixelData1 = makeColoredDartboard(dicom); + updateLayer(dicom, 4, pixelData1, 0.2, true); + let pixelData2 = makeContourLine(dicom, contourLineColor); + updateLayer(dicom, 5, pixelData2, 1.0, true); + let element = document.getElementById("element" + (dicom + 1).toString()); + cornerstone.removeLayer(element, dicoms[dicom].layers[1]); + cornerstone.removeLayer(element, dicoms[dicom].layers[2]); + cornerstone.removeLayer(element, dicoms[dicom].layers[3]); + document.getElementById("cover-spin").classList.toggle("show"); + }; + xhttp.open("POST", "segmentation", true); + xhttp.setRequestHeader('Content-Type', 'application/json'); + xhttp.send(JSON.stringify({ + parameters: dicoms[dicom].parameters, + pixelData: Object.values(dicoms[dicom].pixelData) + })); +} + +/** + * Performs registration. + * + * @param {number} inverse Indicates direction of registration (0 for source -> target, 1 for source <- target). + */ +function registration(inverse) { + if (dicoms[0].segmented.length === 0 || dicoms[1].segmented.length === 0 || + dicoms[0].parameters.width !== dicoms[1].parameters.width || dicoms[0].parameters.height !== dicoms[1].parameters.height) { + window.alert("Both files must be segmented and both must have same dimensions."); + return; + } + document.getElementById("cover-spin").classList.toggle("show"); + + const xhttp = new XMLHttpRequest(); + xhttp.onload = function() { + let pixelData = new Uint16Array(JSON.parse(this.responseText).slice(0, dicoms[0].parameters.width * dicoms[0].parameters.height)); + dicoms[2].parameters.height = dicoms[0].parameters.height; + dicoms[2].parameters.width = dicoms[0].parameters.width; + let element = document.getElementById('element3'); + countDimensions(element, dicoms[2].parameters.height, dicoms[2].parameters.width); + updateLayer(2, 0, pixelData, 1.0, false); + togglePopup(0); + document.getElementById("cover-spin").classList.toggle("show"); + }; + xhttp.open("POST", "optf", true); + xhttp.setRequestHeader('Content-Type', 'application/json'); + xhttp.send(JSON.stringify({ + parameters: dicoms[(inverse) % 2].parameters, + targetSegmented: Object.values(dicoms[(1 + inverse) % 2].segmented), + sourceSegmented: Object.values(dicoms[(inverse) % 2].segmented), + sourceOriginal: Object.values(dicoms[(inverse) % 2].pixelData) + })); +} + +/** + * Shows ECV popup with given ECV values. + * + * @param {number[]} ecv Array of ECV values (one for each of 6 segments). + */ +function showECV(ecv) { + const list = document.getElementById("ecv"); + list.textContent = ''; + for (let i = 0; i < 6; i++) { + const li = document.createElement("li"); + li.innerText = ecv[i].toFixed(2).toString(); + list.appendChild(li); + } + togglePopup(1); +} + +/** + * Computes T1. + * + * @param {number} inverse Indicates direction (0 for source -> target, 1 for source <- target). + * @param {boolean} ecv Indicates if ecv values should be computed. + */ +function computeT1(inverse, ecv) { + if (dicoms[0].segmented.length === 0 || dicoms[1].segmented.length === 0 || + dicoms[0].parameters.width !== dicoms[1].parameters.width || dicoms[0].parameters.height !== dicoms[1].parameters.height) { + window.alert("Both files must be segmented and both must have same dimensions."); + return; + } + if (dicoms[3].pixelData.length === 0 || dicoms[4].pixelData.length === 0 || + dicoms[0].parameters.width !== dicoms[3].parameters.width || dicoms[0].parameters.height !== dicoms[3].parameters.height || + dicoms[0].parameters.width !== dicoms[4].parameters.width || dicoms[0].parameters.height !== dicoms[4].parameters.height) { + window.alert("Both T1 maps must be loaded and both must have the same dimensions as the segmented files."); + return; + } + document.getElementById("cover-spin").classList.toggle("show"); + + const xhttp = new XMLHttpRequest(); + xhttp.onload = function() { + let width = dicoms[0].parameters.width; + let height = dicoms[0].parameters.height; + let transX = new Float64Array(JSON.parse(this.responseText).slice(width * height, width * height * 2)); + let transY = new Float64Array(JSON.parse(this.responseText).slice(width * height * 2)); + + function applyDeformation(array) { + let deformed = []; + let h1 = 1.0 / Math.max(height, width); + let h2 = h1; + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) { + let dx = Math.trunc(x - Math.round(transX[y * width + x] / h1)); + let dy = Math.trunc(y - Math.round(transY[y * width + x] / h2)); + if (0 <= dx && dx < width && 0 <= dy && dy < height) + deformed[y * width + x] = array[Math.abs(dy) * width + Math.abs(dx)]; + else + deformed[y * width + x] = 0; + } + return deformed; + } + + let sourceT1 = dicoms[3].pixelData; + let targetT1 = dicoms[4].pixelData; + if (inverse === 0) + sourceT1 = applyDeformation(sourceT1); + else + targetT1 = applyDeformation(targetT1); + + if (ecv === false) { + let pixelData1; + if (inverse === 0) + pixelData1 = sourceT1; + else + pixelData1 = targetT1; + let pixelData2 = makeColoredDartboard((inverse + 1) % 2); + dicoms[2].parameters.height = height; + dicoms[2].parameters.width = width; + let element = document.getElementById('element3'); + countDimensions(element, dicoms[2].parameters.height, dicoms[2].parameters.width); + updateLayer(2, 0, pixelData1, 1.0, false); + updateLayer(2, 4, pixelData2, 0.2, true); + togglePopup(0); + } + else { + let centerX = dicoms[(inverse + 1) % 2].parameters.inputParameters.centerX; + let centerY = dicoms[(inverse + 1) % 2].parameters.inputParameters.centerY; + let outerX = dicoms[(inverse + 1) % 2].parameters.inputParameters.outerX; + let outerY = dicoms[(inverse + 1) % 2].parameters.inputParameters.outerY; + let masks = makeDartboard(centerX, centerY, outerX, outerY, dicoms[(inverse + 1) % 2].segmented, height, width); + let centralMask = masks[6]; + + function average(T1, mask) { + let pixSum = 0; + let pixCount = 0; + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) { + pixSum += mask[y * width + x] * T1[y * width + x]; + pixCount += mask[y * width + x]; + } + return pixSum / pixCount; + } + + let T3 = average(targetT1, centralMask); + let T4 = average(sourceT1, centralMask); + let ecv = []; + for (let i = 0; i < 6; i++) { + let T1 = average(targetT1, masks[i]); + let T2 = average(sourceT1, masks[i]); + ecv[i] = (1 / T1 - 1 / T2) / (1 / T3 - 1 / T4); + } + showECV(ecv); + } + document.getElementById("cover-spin").classList.toggle("show"); + }; + xhttp.open("POST", "optf", true); + xhttp.setRequestHeader('Content-Type', 'application/json'); + xhttp.send(JSON.stringify({ + parameters: dicoms[(inverse) % 2].parameters, + targetSegmented: Object.values(dicoms[(1 + inverse) % 2].segmented), + sourceSegmented: Object.values(dicoms[(inverse) % 2].segmented), + sourceOriginal: Object.values(dicoms[(inverse) % 2].pixelData) + })); +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..b304d9155a76c543ffd1689d85eb5b8789cf0810 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,239 @@ +/* region General */ +* { + box-sizing: border-box; +} + +h5, h2 { + margin: 0; +} + +p { + margin: 0; + padding-bottom: 7%; +} + +ul { + margin: 0; + list-style-type: none; + padding: 0; +} +/* endregion */ + +/* region Viewers */ +#viewers { + display: flex; + width: 100%; +} + +.viewer { + display: flex; + width: 50%; +} + +/* region Image */ +.image { + width: 70%; + display: flex; + justify-content: center; +} + +.DICOMImage { + width:33vw; + height:33vw; + border-style: solid; + border-width: thin; + display: flex; + justify-content: center; + align-items: center; +} + +.adjusting { + width: 100%; + display: flex; + padding-top: 1%; +} + +.brightness { + width: 60%; +} + +.brightness > * { + width: 80%; + display: block; +} + +.zoom, .reset { + width: 20%; +} + +.T1 > * { + display: block; +} + +.T1 > label { + padding-top: 3%; +} + +.T1 > input { + margin-top: 1%; + margin-left: 3%; +} +/* endregion */ + +/* region Settings */ +.settings { + width: 30%; +} + +.files { + margin-bottom: 3%; +} + +.list { + width: 100%; + height: 30vh; + padding: 2%; + border-style: solid; + border-width: thin; + cursor: context-menu; + overflow-y: scroll; + overflow-wrap: break-word; + margin-bottom: 3%; +} + +.list > *:hover { + background-color: lightcyan; +} + +.list > .active { + background-color: lightblue; +} + +.parameters { + border-style: solid; + border-width: thin; + padding: 2%; + margin-bottom: 3%; +} + +.circles { + margin-left: 3%; +} + +.parameters > button { + margin-left: 3%; + margin-top: 1%; + margin-bottom: 3%; +} + +.parameters input { + display: inline-block; + width: 30%; +} +/* endregion */ +/* endregion */ + +/* region Buttons */ +.buttons { + display: flex; + justify-content: space-between; + padding-top: 3%; + padding-left: 10%; + padding-right: 10%; +} + +.buttons > * { + width: 25%; +} +/* endregion */ + +/* region Popups */ +.popup { + visibility: hidden; + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + z-index: 9; + display: flex; + justify-content: center; + align-items: flex-start; +} + +.popupContainer { + border-style: solid; + background-color: white; + padding: 2%; + margin-top: 2%; +} + +.cross { + width: 100%; + padding-bottom: 5vh; +} + +#popup2 ul { + display: inline-block; +} +/* endregion */ + +/* region Spin */ +.cover-spin { + position: fixed; + width: 100%; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.7); + z-index: 9999; + visibility: hidden; +} + +@-webkit-keyframes spin { + from {-webkit-transform: rotate(0deg);} + to {-webkit-transform: rotate(360deg);} +} + +@keyframes spin { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} + +.cover-spin::after { + content: ''; + display: block; + position: absolute; + left: 48%; + top: 40%; + width: 40px; + height: 40px; + border-style: solid; + border-color: black; + border-top-color: transparent; + border-width: 4px; + border-radius: 50%; + -webkit-animation: spin .8s linear infinite; + animation: spin .8s linear infinite; +} +/* endregion */ + +/* region Others */ +.show { + visibility: visible; +} + +.move { + cursor: move; +} + +.contextMenu { + cursor: context-menu; +} + +.crosshair { + cursor: crosshair; +} +/* endregion */ \ No newline at end of file