Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dcm support #40

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dist
/data
.nyc_output
.test_output
coverage
coverage
!ext.vsix
7 changes: 5 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}/data",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentPath=${workspaceRoot}",
"--disable-extensions"
]
],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/dist/**/*.js"]
},
{
"name": "Run Web Extension",
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.indentSize": 2
}
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Change Log
### [0.0.15]

* added DCM support

### [0.0.14]

* Better support for code-server, thanks to @TinasheMTapera for input
* Fix image loading when the affine is zeroed, thanks to @nx10 and @pierre-nedelec for helping debugging it
* Fix file access when running on Windows, thanks to @nx10 for providing with a fix for it
* Better support for code-server, thanks to [@TinasheMTapera](https://github.com/TinasheMTapera) for input
* Fix image loading when the affine is zeroed, thanks to [@nx10](https://github.com/nx10) and [@pierre-nedelec](https://github.com/pierre-nedelec) for helping debugging it
* Fix file access when running on Windows, thanks to [@nx10](https://github.com/nx10) for providing with a fix for it

### [0.0.13]

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
<!-- (https://mattermost.brainhack.org/brainhack/channels/vscode-neuro-viewer) -->

[![GitHub license](https://img.shields.io/github/license/anibalsolon/vscode-neuro-viewer.svg)](https://github.com/anibalsolon/vscode-neuro-viewer/blob/main/LICENSE)
[![Visual Studio Marketplace](https://vsmarketplacebadge.apphb.com/installs-short/anibalsolon.neuro-viewer.svg)](https://marketplace.visualstudio.com/items?itemName=anibalsolon.neuro-viewer)
[![Visual Studio Marketplace](https://img.shields.io/badge/Visual_Studio_Code-0078D4?style=plastic&logo=visual%20studio%20code&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=anibalsolon.neuro-viewer)
[![Coverage Status](https://coveralls.io/repos/github/anibalsolon/vscode-neuro-viewer/badge.svg?branch=develop)](https://coveralls.io/github/anibalsolon/vscode-neuro-viewer?branch=main)
![Build](https://github.com/anibalsolon/vscode-neuro-viewer/actions/workflows/test-and-deploy.yml/badge.svg?branch=main)
![Wakatime](https://user-images.githubusercontent.com/562525/159188432-2f20e2ca-4a57-4a4f-a935-6728751939dc.png)


If you got here, you might be familiar with Nifti files. In any case, Nifti is a file format for neuroimaging.
Expand All @@ -28,3 +27,6 @@ A quick way to view your Nifti files. It shows some metadata and renders a volum
## Known Issues

* It just renders the first volume of a fMRI.

@kubzoey95 Added dcm series support - click on any .dcm file in dir with series:
![DCM](https://raw.githubusercontent.com/kubzoey95/vscode-neuro-viewer/main/dcm-screenshot.png)
Binary file added dcm-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions extension/codec-openjpeg/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import OpenJPEGJS from "./openjpegjs";
import OpenJPEGWASM from "./openjpegwasm";
export {
OpenJPEGJS,
OpenJPEGWASM
};
38 changes: 38 additions & 0 deletions extension/codec-openjpeg/openjpegjs.js

Large diffs are not rendered by default.

Binary file added extension/codec-openjpeg/openjpegjs.js.mem
Binary file not shown.
36 changes: 36 additions & 0 deletions extension/codec-openjpeg/openjpegjs_decode.js

Large diffs are not rendered by default.

Binary file added extension/codec-openjpeg/openjpegjs_decode.js.mem
Binary file not shown.
21 changes: 21 additions & 0 deletions extension/codec-openjpeg/openjpegwasm.js

Large diffs are not rendered by default.

Binary file added extension/codec-openjpeg/openjpegwasm.wasm
Binary file not shown.
21 changes: 21 additions & 0 deletions extension/codec-openjpeg/openjpegwasm_decode.js

Large diffs are not rendered by default.

Binary file not shown.
91 changes: 91 additions & 0 deletions extension/getjpeg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @ts-nocheck
import daikon from 'daikon';
const concatDataViews = (dataViews: DataView[]): DataView => {
let length = 0;
let offset = 0;

for (let ctr = 0; ctr < dataViews.length; ctr += 1) {
length += dataViews[ctr].byteLength;
}

const tmp = new Uint8Array(length);
let dataView;
for (let ctr = 0; ctr < dataViews.length; ctr += 1) {
dataView = dataViews[ctr];
tmp.set(new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength), offset);
offset += dataViews[ctr].byteLength;
}

return new DataView(tmp.buffer);
};

const JPEG_MAGIC_NUMBER = [0xFF, 0xD8];
const JPEG2000_MAGIC_NUMBER = [0xFF, 0x4F, 0xFF, 0x51];

const isHeaderJPEG = (data: DataView): boolean => {
if (!data) {
return false;
}
if (data.getUint8(0) !== JPEG_MAGIC_NUMBER[0]) {
return false;
}

if (data.getUint8(1) !== JPEG_MAGIC_NUMBER[1]) {
return false;
}

return true;
};

const isHeaderJPEG2000 = (data: DataView): boolean => {
if (!data) {
return false;
}
for (let ctr = 0; ctr < JPEG2000_MAGIC_NUMBER.length; ctr += 1) {
if (data.getUint8(ctr) !== JPEG2000_MAGIC_NUMBER[ctr]) {
return false;
}
}
return true;
};

export const getJpegData = (inData: DataView): DataView[] => {
const encapTags = daikon.Series.parseImage(inData).getEncapsulatedData();
const data: Array<DataView[]> = [];
const dataConcat: DataView[] = [];

let currentJpeg;
// organize data as an array of an array of JPEG parts
if (encapTags) {
const numTags = encapTags.length;

for (let ctr = 0; ctr < numTags; ctr += 1) {
const dataView = encapTags[ctr].value as DataView;
if (isHeaderJPEG(dataView)
|| isHeaderJPEG2000(dataView)) {
currentJpeg = [];
currentJpeg.push(dataView);
data.push(currentJpeg);
}
else if (currentJpeg && dataView) {
currentJpeg.push(dataView);
}
}
}

// concat into an array of full JPEGs
for (let ctr = 0; ctr < data.length; ctr += 1) {
const buffers = data[ctr];
if (buffers.length > 1) {
// TODO: this will be slow...is it necessary?
dataConcat[ctr] = concatDataViews(buffers);
}
else {
[dataConcat[ctr]] = data[ctr];
}

delete data[ctr];
}

return dataConcat;
};
198 changes: 197 additions & 1 deletion extension/provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,188 @@
// @ts-nocheck
import * as vscode from 'vscode';
import { Buffer } from 'buffer';
import { NiftiDocument } from '../extension/document';
import { dirname } from 'path';
import daikon from 'daikon';
import { appendFileSync, readFileSync, renameSync, existsSync, writeFileSync } from 'fs';
import { v4 } from 'uuid';
import * as temp from 'temp';
import { assert } from 'console';
import glob from 'fast-glob';
import { OpenJPEGWASM } from './codec-openjpeg';
import { getJpegData } from './getjpeg';
temp.track();

function toArrayBuffer(buffer: Uint8Array) {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return arrayBuffer;
}

const arrayMinMax = (arr) =>
arr.reduce(([min, max], val) => [Math.min(min, val), Math.max(max, val)], [
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
]);


async function dcm2nii(uri: vscode.Uri, outUri: vscode.Uri): Promise<vscode.Uri> {
const openjpeg = await new OpenJPEGWASM();
const dirUri = vscode.Uri.parse(dirname(uri.path));
const firstImg = daikon.Series.parseImage(new DataView(toArrayBuffer(await vscode.workspace.fs.readFile(vscode.Uri.parse(uri)))));
const bitsAllocated = firstImg.getBitsAllocated();
const bitpix = bitsAllocated > 16 ? 32 : bitsAllocated;
const data_type = bitsAllocated > 16 ? 16 : {16: 4, 8: 2}[bitsAllocated];
const arrayType = bitsAllocated > 16 ? Float32Array : {16: Int16Array, 8: Int8Array}[bitsAllocated];
const seriesUID = firstImg.getSeriesInstanceUID();
const images = (await glob([dirUri.path + "/**/*.dcm"]).then(async (dcms) => {
return dcms.map(async (dcm) => {
//https://www.npmjs.com/package/@cornerstonejs/codec-openjpeg
let buffer = new DataView(toArrayBuffer(await vscode.workspace.fs.readFile(vscode.Uri.parse(dcm))));
let series = daikon.Series.parseImage(buffer);
if (series.isCompressedJPEG2000()) {
const jpegDatas = getJpegData(buffer);
const jpegData = jpegDatas[0];

const decoder = new openjpeg.J2KDecoder();
const pixelData = new Uint8Array(jpegData.buffer);
const encodedBuffer = decoder.getEncodedBuffer(pixelData.length);
encodedBuffer.set(pixelData);
decoder.decode();
const decodedBuffer = new DataView(new Uint8Array(decoder.getDecodedBuffer()).buffer);
series.getPixelData = () => {return {'value': decodedBuffer};};
series.isCompressed = () => false;
}
else{
series.getPixelData = () => {return {'value': new DataView(new Uint8Array(new arrayType(series.getInterpretedData()).buffer).buffer)};};
series.getDataScaleSlope = () => 1;
series.getDataScaleIntercept = () => 0;
}
return series;
});
}).then((promises) => Promise.all(promises))).filter((img) => img.getSeriesInstanceUID() === seriesUID).sort((a, b) => {
const a_slic = a.getSliceLocation();
const b_slic = b.getSliceLocation();
return a_slic < b_slic ? -1 : a_slic > b_slic ? 1 : 0;
});
const imgPath = outUri.path + "/" + v4();
const series = new daikon.Series();
let minVal = new arrayType(images[0].getInterpretedData())[0];
let maxVal = new arrayType(images[0].getInterpretedData())[0];
const l = new Uint8Array(images[0].getPixelData().value.buffer).length;
for (const image of images) {
if (image === null) {
console.error(daikon.Series.parserError);
} else if (image.hasPixelData()) {
const data = new Uint8Array(image.getPixelData().value.buffer);
const slope = image.getDataScaleSlope() || 1;
const intercept = image.getDataScaleIntercept() || 0;
const typedArray = new arrayType(data.buffer).map((el) => el * slope + intercept);
const [min, max] = arrayMinMax(typedArray);
if (max > maxVal) { maxVal = max; }
if (min < minVal) { minVal = min; }
assert(l === data.length);
assert(image.getBitsAllocated() === bitsAllocated);
appendFileSync(imgPath, new Uint8Array(typedArray.buffer));
series.addImage(image);
}
}
if (firstImg.getWindowCenter() && firstImg.getWindowWidth()) {
minVal = new arrayType(firstImg.getWindowCenter())[0] - new arrayType(firstImg.getWindowWidth())[0] / 2;
maxVal = new arrayType(firstImg.getWindowCenter())[0] + new arrayType(firstImg.getWindowWidth())[0] / 2;
}
series.buildSeries();
const ori = images[0].getTag(0x0020, 0x0037).value;
const firstPos = images[0].getTag(0x0020, 0x0032).value;
const lastPos = images[images.length - 1].getTag(0x0020, 0x0032).value;
const thi = images[0].getPixelSpacing();
const n = images.length;
// https://brainder.org/2015/04/03/the-nifti-2-file-format/
// https://core.ac.uk/download/pdf/79518053.pdf
const bytes = [
new Uint8Array(new Int32Array([540]).buffer), // sizeof_hdr
Buffer.from("n+2"), // magic[0-2]
new Uint8Array(5),
new Uint8Array(new Int16Array([data_type]).buffer), // data_type
new Uint8Array(new Int16Array([bitpix]).buffer), // bitpix
new Uint8Array(new BigInt64Array([
BigInt(3), // dim[0]
BigInt(series.images[0].getRows()), // dim[1]
BigInt(series.images[0].getCols()), // dim[2]
BigInt(series.images.length), // dim[3]
BigInt(1), // dim[4]
BigInt(1), // dim[5]
BigInt(1), // dim[6]
BigInt(1), // dim[7]
]).buffer),
new Uint8Array(new Float64Array([
0, // intent_p1
0, // intent_p2
0, // intent_p3
]).buffer),
new Uint8Array(new Float64Array([
0, // pixdim[0]
...series.images[0].getPixelSpacing(), // pixdim[1] pixdim[2]
series.images[0].getSliceThickness(), // pixdim[3]
0, // pixdim[4]
0, // pixdim[5]
0, // pixdim[6]
0, // pixdim[7]
]).buffer),
new Uint8Array(new BigInt64Array([BigInt(544)]).buffer), // vox_offset
new Uint8Array(new Float64Array([
1, // scl_slope
0, // scl_inter
// 0, //1, // scl_slope
// 0, //0, // scl_inter
maxVal, // cal_max
minVal, // cal_min
0, // slice_duration
0, // toffset
]).buffer),
new Uint8Array(new BigInt64Array([
BigInt(0), // slice_start
BigInt(0) // slice_end
]).buffer),
new Uint8Array(80), // descrip[80] // some
new Uint8Array(24), // aux_file[24] // none
new Uint8Array(new Int32Array([
0, // qform_code
4, // sform_code
]).buffer),
new Uint8Array(new Float64Array([
0, // quatern_b
0, // quatern_c
0, // quatern_d
...images[0].getImagePosition(), // qoffset_x qoffset_y qoffset_z
-ori[0]*thi[0], -ori[3]*thi[1], -(lastPos[0] - firstPos[0]) / (n - 1), -firstPos[0], // srow_x[4]
-ori[1]*thi[0], -ori[4]*thi[1], -(lastPos[1] - firstPos[1]) / (n - 1), -firstPos[1], // srow_y[4]
ori[2]*thi[0], ori[5]*thi[1], (lastPos[2] - firstPos[2]) / (n - 1), firstPos[2] // srow_z[4]
// -1, 0, 0, 0, // srow_x[4]
// 0, -1, 0, 0, // srow_y[4]
// 0, 0, 1, 0 // srow_z[4]
]).buffer),
new Uint8Array(new Int32Array([
0, // slice_code
2, // xyzt_units
0, // intent_code
]).buffer),
new Uint8Array(16), // intent_name[16]
new Uint8Array(1), // dim_info
new Uint8Array(15), // unused_str[15]
new Uint8Array(4), // additional 4 bytes
];
const hdrPath = outUri.path + "/" + v4();
bytes.map((buf) => appendFileSync(hdrPath, buf));
console.log(new Uint8Array(readFileSync(imgPath)).length);
appendFileSync(hdrPath, new Uint8Array(readFileSync(imgPath)));
const outcome = hdrPath + ".nii";
renameSync(hdrPath, outcome);
return vscode.Uri.parse(outcome);
}

export class NiftiEditorProvider implements vscode.CustomReadonlyEditorProvider<NiftiDocument> {

Expand Down Expand Up @@ -30,7 +211,22 @@ export class NiftiEditorProvider implements vscode.CustomReadonlyEditorProvider<
uri: vscode.Uri
): Promise<NiftiDocument> {
console.log(`Open document ${uri}`);
const data: Uint8Array = await vscode.workspace.fs.readFile(uri);
let data: Uint8Array = await vscode.workspace.fs.readFile(uri);
if (uri.path.endsWith(".dcm")) {
const seriesUID = daikon.Series.parseImage(new DataView(toArrayBuffer(await vscode.workspace.fs.readFile(vscode.Uri.parse(uri))))).getSeriesInstanceUID();
let cache = this._context.globalState.get('neuro-viewer') || {};
let uriNii = null;
if (seriesUID in cache && existsSync(cache[seriesUID])) {
uriNii = vscode.Uri.parse(cache[seriesUID]);
}
else {
const outDir = temp.mkdirSync(v4());
uriNii = await dcm2nii(uri, vscode.Uri.parse(outDir));
cache[seriesUID] = uriNii.path;
this._context.globalState.update('neuro-viewer', cache);
}
data = await vscode.workspace.fs.readFile(uriNii);
}
const document: NiftiDocument = new NiftiDocument(uri, data);
return document;
}
Expand Down
Loading