Volume.js 13 KB

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