rii-mango / NIFTI-Reader-JS

A JavaScript NIfTI file format reader.
MIT License
137 stars 30 forks source link

coronal view #18

Closed farzam-khodajoo closed 1 year ago

farzam-khodajoo commented 2 years ago

Hi, thanks for this handy library this is not really an issue, but I have struggle getting coronal view instead of axial for brain images. I have brain MRI images in axial view with dimention of (240, 240, 155) and this example successfuly draws images on screen

but now I'm looking for coronal view like this image which happens when treat columns as slices and slices as column :

(rows, column, slices) to (rows, slices, column)

I would appreciate your guidance.

znshje commented 2 years ago

Hi,

maybe you can try this library: https://github.com/scijs/ndarray, which operates like NumPy. Since the image data is stored in a TypedArray, you can easily reshape the data using ndarray and call array.pick() to select the columns you want.

This is a brief sample:

    let header = nifti.readHeader(binaryData)   // binaryData is an arrayBuffer you download or read locally
    let dims = header.dims
    let stride = [1, dims[1], dims[1] * dims[2]]
    let array = ndarray(imageData, [dims[1], dims[2], dims[3]], stride).step(1, 1, -1)   // imageData is a TypedArray, see the docs

    const getImage = (dim, slice) => {
        let image
        if (dim === 1) {
            image = array.pick(slice, null, null)
        } else if (dim === 2) {
            image = array.pick(null, slice, null)
        } else if (dim === 3) {
            image = array.pick(null, null, header.dims[3] - slice - 1)
        }
        return image
    }

Thus, you can get images from coronal view as getImage(2, SLICE).

Hope this will help!

neurolabusc commented 2 years ago

If you want to display axial, coronal and sagittal views from a 3D volume it is easiest to use WebGL2. This version introduced 3D textures, which are like 3D bitmaps (e.g. 2D bitmaps are pixel based, 3D are voxel based). This means that you do not have to send data between the CPU and GPU for each slice and every frame, rather the entire 3D array is in the GPU memory and the shader samples the desired plane.

You can see this in NiiVue where the shader uniform axCorSag selects between axial, coronal and sagittal slices. Since Texture dimensions are always sampled from 0..1, you would choose an axial slice would be [0,0,z],[1,0,z],[1,1,z],[0,1,z], a coronal slice would be [0,y,0],[1,y,0],[1,y,1],[0,y,1] and the sagittal slice would be [x,0,0],[x,1,0],[x,1,1],[x,0,1] assuming your voxels are in RAS orientation (NiiVue includes code that will swizzle voxel order to RAS).

Hermanboi97 commented 1 year ago

Hi znshje, I'm not familiar with the ndarray library, and I was wondering how you would put the ndarray from the getImage function into the image data, as it is done in the example.

znshje commented 1 year ago

Hi @Hermanboi97 , the ndarray takes a TypedArray as the first parameter. The imageData in my code is identical to the typedData in the example. Then, use the function image.get(i, j) to get the image value at (i, j).

The render process is like:

        let ctx = canvas.getContext('2d')
        let canvasImageData = ctx.createImageData(cols, rows)
        // draw pixels
        for (let row = 0; row < rows; row++) {
            let rowOffset = row * cols;
            for (let col = 0; col < cols; col++) {
                let value = image.get(col, row)

                /*
                   Assumes data is 8-bit, otherwise you would need to first convert
                   to 0-255 range based on datatype range, data range (iterate through
                   data to find), or display range (cal_min/max).

                   Other things to take into consideration:
                     - data scale: scl_slope and scl_inter, apply to raw value before
                       applying display range
                     - orientation: displays in raw orientation, see nifti orientation
                       info for how to orient data
                     - assumes voxel shape (pixDims) is isometric, if not, you'll need
                       to apply transform to the canvas
                     - byte order: see littleEndian flag
                */
                canvasImageData.data[(rowOffset + col) * 4] = value & 0xFF;
                canvasImageData.data[(rowOffset + col) * 4 + 1] = value & 0xFF;
                canvasImageData.data[(rowOffset + col) * 4 + 2] = value & 0xFF;
                canvasImageData.data[(rowOffset + col) * 4 + 3] = 0xFF;
            }
        }
        ctx.putImageData(canvasImageData, 0, 0);
neurolabusc commented 1 year ago

Closing this issue based on solution from @znshje