Volume.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. ( function () {
  2. /**
  3. * This class had been written to handle the output of the NRRD loader.
  4. * It contains a volume of data and informations about it.
  5. * For now it only handles 3 dimensional data.
  6. * See the webgl_loader_nrrd.html example and the loaderNRRD.js file to see how to use this class.
  7. * @class
  8. * @param {number} xLength Width of the volume
  9. * @param {number} yLength Length of the volume
  10. * @param {number} zLength Depth of the volume
  11. * @param {string} type The type of data (uint8, uint16, ...)
  12. * @param {ArrayBuffer} arrayBuffer The buffer with volume data
  13. */
  14. function Volume( xLength, yLength, zLength, type, arrayBuffer ) {
  15. if ( arguments.length > 0 ) {
  16. /**
  17. * @member {number} xLength Width of the volume in the IJK coordinate system
  18. */
  19. this.xLength = Number( xLength ) || 1;
  20. /**
  21. * @member {number} yLength Height of the volume in the IJK coordinate system
  22. */
  23. this.yLength = Number( yLength ) || 1;
  24. /**
  25. * @member {number} zLength Depth of the volume in the IJK coordinate system
  26. */
  27. this.zLength = Number( zLength ) || 1;
  28. /**
  29. * @member {Array<string>} The order of the Axis dictated by the NRRD header
  30. */
  31. this.axisOrder = [ 'x', 'y', 'z' ];
  32. /**
  33. * @member {TypedArray} data Data of the volume
  34. */
  35. switch ( type ) {
  36. case 'Uint8':
  37. case 'uint8':
  38. case 'uchar':
  39. case 'unsigned char':
  40. case 'uint8_t':
  41. this.data = new Uint8Array( arrayBuffer );
  42. break;
  43. case 'Int8':
  44. case 'int8':
  45. case 'signed char':
  46. case 'int8_t':
  47. this.data = new Int8Array( arrayBuffer );
  48. break;
  49. case 'Int16':
  50. case 'int16':
  51. case 'short':
  52. case 'short int':
  53. case 'signed short':
  54. case 'signed short int':
  55. case 'int16_t':
  56. this.data = new Int16Array( arrayBuffer );
  57. break;
  58. case 'Uint16':
  59. case 'uint16':
  60. case 'ushort':
  61. case 'unsigned short':
  62. case 'unsigned short int':
  63. case 'uint16_t':
  64. this.data = new Uint16Array( arrayBuffer );
  65. break;
  66. case 'Int32':
  67. case 'int32':
  68. case 'int':
  69. case 'signed int':
  70. case 'int32_t':
  71. this.data = new Int32Array( arrayBuffer );
  72. break;
  73. case 'Uint32':
  74. case 'uint32':
  75. case 'uint':
  76. case 'unsigned int':
  77. case 'uint32_t':
  78. this.data = new Uint32Array( arrayBuffer );
  79. break;
  80. case 'longlong':
  81. case 'long long':
  82. case 'long long int':
  83. case 'signed long long':
  84. case 'signed long long int':
  85. case 'int64':
  86. case 'int64_t':
  87. case 'ulonglong':
  88. case 'unsigned long long':
  89. case 'unsigned long long int':
  90. case 'uint64':
  91. case 'uint64_t':
  92. throw new Error( 'Error in Volume constructor : this type is not supported in JavaScript' );
  93. break;
  94. case 'Float32':
  95. case 'float32':
  96. case 'float':
  97. this.data = new Float32Array( arrayBuffer );
  98. break;
  99. case 'Float64':
  100. case 'float64':
  101. case 'double':
  102. this.data = new Float64Array( arrayBuffer );
  103. break;
  104. default:
  105. this.data = new Uint8Array( arrayBuffer );
  106. }
  107. if ( this.data.length !== this.xLength * this.yLength * this.zLength ) {
  108. throw new Error( 'Error in Volume constructor, lengths are not matching arrayBuffer size' );
  109. }
  110. }
  111. /**
  112. * @member {Array} spacing Spacing to apply to the volume from IJK to RAS coordinate system
  113. */
  114. this.spacing = [ 1, 1, 1 ];
  115. /**
  116. * @member {Array} offset Offset of the volume in the RAS coordinate system
  117. */
  118. this.offset = [ 0, 0, 0 ];
  119. /**
  120. * @member {Martrix3} matrix The IJK to RAS matrix
  121. */
  122. this.matrix = new THREE.Matrix3();
  123. this.matrix.identity();
  124. /**
  125. * @member {Martrix3} inverseMatrix The RAS to IJK matrix
  126. */
  127. /**
  128. * @member {number} lowerThreshold The voxels with values under this threshold won't appear in the slices.
  129. * If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
  130. */
  131. let lowerThreshold = - Infinity;
  132. Object.defineProperty( this, 'lowerThreshold', {
  133. get: function () {
  134. return lowerThreshold;
  135. },
  136. set: function ( value ) {
  137. lowerThreshold = value;
  138. this.sliceList.forEach( function ( slice ) {
  139. slice.geometryNeedsUpdate = true;
  140. } );
  141. }
  142. } );
  143. /**
  144. * @member {number} upperThreshold The voxels with values over this threshold won't appear in the slices.
  145. * If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
  146. */
  147. let upperThreshold = Infinity;
  148. Object.defineProperty( this, 'upperThreshold', {
  149. get: function () {
  150. return upperThreshold;
  151. },
  152. set: function ( value ) {
  153. upperThreshold = value;
  154. this.sliceList.forEach( function ( slice ) {
  155. slice.geometryNeedsUpdate = true;
  156. } );
  157. }
  158. } );
  159. /**
  160. * @member {Array} sliceList The list of all the slices associated to this volume
  161. */
  162. this.sliceList = [];
  163. /**
  164. * @member {Array} RASDimensions This array holds the dimensions of the volume in the RAS space
  165. */
  166. }
  167. Volume.prototype = {
  168. constructor: Volume,
  169. /**
  170. * @member {Function} getData Shortcut for data[access(i,j,k)]
  171. * @memberof Volume
  172. * @param {number} i First coordinate
  173. * @param {number} j Second coordinate
  174. * @param {number} k Third coordinate
  175. * @returns {number} value in the data array
  176. */
  177. getData: function ( i, j, k ) {
  178. return this.data[ k * this.xLength * this.yLength + j * this.xLength + i ];
  179. },
  180. /**
  181. * @member {Function} access compute the index in the data array corresponding to the given coordinates in IJK system
  182. * @memberof Volume
  183. * @param {number} i First coordinate
  184. * @param {number} j Second coordinate
  185. * @param {number} k Third coordinate
  186. * @returns {number} index
  187. */
  188. access: function ( i, j, k ) {
  189. return k * this.xLength * this.yLength + j * this.xLength + i;
  190. },
  191. /**
  192. * @member {Function} reverseAccess Retrieve the IJK coordinates of the voxel corresponding of the given index in the data
  193. * @memberof Volume
  194. * @param {number} index index of the voxel
  195. * @returns {Array} [x,y,z]
  196. */
  197. reverseAccess: function ( index ) {
  198. const z = Math.floor( index / ( this.yLength * this.xLength ) );
  199. const y = Math.floor( ( index - z * this.yLength * this.xLength ) / this.xLength );
  200. const x = index - z * this.yLength * this.xLength - y * this.xLength;
  201. return [ x, y, z ];
  202. },
  203. /**
  204. * @member {Function} map Apply a function to all the voxels, be careful, the value will be replaced
  205. * @memberof Volume
  206. * @param {Function} functionToMap A function to apply to every voxel, will be called with the following parameters :
  207. * value of the voxel
  208. * index of the voxel
  209. * the data (TypedArray)
  210. * @param {Object} context You can specify a context in which call the function, default if this Volume
  211. * @returns {Volume} this
  212. */
  213. map: function ( functionToMap, context ) {
  214. const length = this.data.length;
  215. context = context || this;
  216. for ( let i = 0; i < length; i ++ ) {
  217. this.data[ i ] = functionToMap.call( context, this.data[ i ], i, this.data );
  218. }
  219. return this;
  220. },
  221. /**
  222. * @member {Function} extractPerpendicularPlane Compute the orientation of the slice and returns all the information relative to the geometry such as sliceAccess, the plane matrix (orientation and position in RAS coordinate) and the dimensions of the plane in both coordinate system.
  223. * @memberof Volume
  224. * @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
  225. * @param {number} index the index of the slice
  226. * @returns {Object} an object containing all the usefull information on the geometry of the slice
  227. */
  228. extractPerpendicularPlane: function ( axis, RASIndex ) {
  229. let firstSpacing, secondSpacing, positionOffset, IJKIndex;
  230. const axisInIJK = new THREE.Vector3(),
  231. firstDirection = new THREE.Vector3(),
  232. secondDirection = new THREE.Vector3(),
  233. planeMatrix = new THREE.Matrix4().identity(),
  234. volume = this;
  235. const dimensions = new THREE.Vector3( this.xLength, this.yLength, this.zLength );
  236. switch ( axis ) {
  237. case 'x':
  238. axisInIJK.set( 1, 0, 0 );
  239. firstDirection.set( 0, 0, - 1 );
  240. secondDirection.set( 0, - 1, 0 );
  241. firstSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
  242. secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
  243. IJKIndex = new THREE.Vector3( RASIndex, 0, 0 );
  244. planeMatrix.multiply( new THREE.Matrix4().makeRotationY( Math.PI / 2 ) );
  245. positionOffset = ( volume.RASDimensions[ 0 ] - 1 ) / 2;
  246. planeMatrix.setPosition( new THREE.Vector3( RASIndex - positionOffset, 0, 0 ) );
  247. break;
  248. case 'y':
  249. axisInIJK.set( 0, 1, 0 );
  250. firstDirection.set( 1, 0, 0 );
  251. secondDirection.set( 0, 0, 1 );
  252. firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
  253. secondSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
  254. IJKIndex = new THREE.Vector3( 0, RASIndex, 0 );
  255. planeMatrix.multiply( new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) );
  256. positionOffset = ( volume.RASDimensions[ 1 ] - 1 ) / 2;
  257. planeMatrix.setPosition( new THREE.Vector3( 0, RASIndex - positionOffset, 0 ) );
  258. break;
  259. case 'z':
  260. default:
  261. axisInIJK.set( 0, 0, 1 );
  262. firstDirection.set( 1, 0, 0 );
  263. secondDirection.set( 0, - 1, 0 );
  264. firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
  265. secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
  266. IJKIndex = new THREE.Vector3( 0, 0, RASIndex );
  267. positionOffset = ( volume.RASDimensions[ 2 ] - 1 ) / 2;
  268. planeMatrix.setPosition( new THREE.Vector3( 0, 0, RASIndex - positionOffset ) );
  269. break;
  270. }
  271. firstDirection.applyMatrix4( volume.inverseMatrix ).normalize();
  272. firstDirection.arglet = 'i';
  273. secondDirection.applyMatrix4( volume.inverseMatrix ).normalize();
  274. secondDirection.arglet = 'j';
  275. axisInIJK.applyMatrix4( volume.inverseMatrix ).normalize();
  276. const iLength = Math.floor( Math.abs( firstDirection.dot( dimensions ) ) );
  277. const jLength = Math.floor( Math.abs( secondDirection.dot( dimensions ) ) );
  278. const planeWidth = Math.abs( iLength * firstSpacing );
  279. const planeHeight = Math.abs( jLength * secondSpacing );
  280. IJKIndex = Math.abs( Math.round( IJKIndex.applyMatrix4( volume.inverseMatrix ).dot( axisInIJK ) ) );
  281. const base = [ new THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 0, 1, 0 ), new THREE.Vector3( 0, 0, 1 ) ];
  282. const iDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
  283. return Math.abs( x.dot( base[ 0 ] ) ) > 0.9;
  284. } );
  285. const jDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
  286. return Math.abs( x.dot( base[ 1 ] ) ) > 0.9;
  287. } );
  288. const kDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
  289. return Math.abs( x.dot( base[ 2 ] ) ) > 0.9;
  290. } );
  291. function sliceAccess( i, j ) {
  292. const si = iDirection === axisInIJK ? IJKIndex : iDirection.arglet === 'i' ? i : j;
  293. const sj = jDirection === axisInIJK ? IJKIndex : jDirection.arglet === 'i' ? i : j;
  294. const sk = kDirection === axisInIJK ? IJKIndex : kDirection.arglet === 'i' ? i : j; // invert indices if necessary
  295. const accessI = iDirection.dot( base[ 0 ] ) > 0 ? si : volume.xLength - 1 - si;
  296. const accessJ = jDirection.dot( base[ 1 ] ) > 0 ? sj : volume.yLength - 1 - sj;
  297. const accessK = kDirection.dot( base[ 2 ] ) > 0 ? sk : volume.zLength - 1 - sk;
  298. return volume.access( accessI, accessJ, accessK );
  299. }
  300. return {
  301. iLength: iLength,
  302. jLength: jLength,
  303. sliceAccess: sliceAccess,
  304. matrix: planeMatrix,
  305. planeWidth: planeWidth,
  306. planeHeight: planeHeight
  307. };
  308. },
  309. /**
  310. * @member {Function} extractSlice Returns a slice corresponding to the given axis and index
  311. * The coordinate are given in the Right Anterior Superior coordinate format
  312. * @memberof Volume
  313. * @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
  314. * @param {number} index the index of the slice
  315. * @returns {VolumeSlice} the extracted slice
  316. */
  317. extractSlice: function ( axis, index ) {
  318. const slice = new THREE.VolumeSlice( this, index, axis );
  319. this.sliceList.push( slice );
  320. return slice;
  321. },
  322. /**
  323. * @member {Function} repaintAllSlices Call repaint on all the slices extracted from this volume
  324. * @see THREE.VolumeSlice.repaint
  325. * @memberof Volume
  326. * @returns {Volume} this
  327. */
  328. repaintAllSlices: function () {
  329. this.sliceList.forEach( function ( slice ) {
  330. slice.repaint();
  331. } );
  332. return this;
  333. },
  334. /**
  335. * @member {Function} computeMinMax Compute the minimum and the maximum of the data in the volume
  336. * @memberof Volume
  337. * @returns {Array} [min,max]
  338. */
  339. computeMinMax: function () {
  340. let min = Infinity;
  341. let max = - Infinity; // buffer the length
  342. const datasize = this.data.length;
  343. let i = 0;
  344. for ( i = 0; i < datasize; i ++ ) {
  345. if ( ! isNaN( this.data[ i ] ) ) {
  346. const value = this.data[ i ];
  347. min = Math.min( min, value );
  348. max = Math.max( max, value );
  349. }
  350. }
  351. this.min = min;
  352. this.max = max;
  353. return [ min, max ];
  354. }
  355. };
  356. THREE.Volume = Volume;
  357. } )();