Skip to content

Commit

Permalink
Merge pull request #844 from jadh4v/ENH-handle-CSPS-using-DCMTK
Browse files Browse the repository at this point in the history
feat(CSPS): add preliminary support for CSPS
  • Loading branch information
thewtex authored Jun 2, 2023
2 parents 01cfe1d + d5a7bcc commit 00cb87f
Show file tree
Hide file tree
Showing 36 changed files with 345 additions and 85 deletions.
1 change: 1 addition & 0 deletions .github/workflows/python-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ jobs:
if: ${{ matrix.python-minor-version < 10 }}
working-directory: ./packages/dicom/python/itkwasm-dicom-wasi
run: |
python -m pip install pillow
python -m pip install -e .
pytest
- name: Test dicom-emscripten chrome
Expand Down
2 changes: 2 additions & 0 deletions include/itkPipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@

// Parse options while allowing extra flags, not exiting with help flags, and clearning parse state after finished.
// Use this to parse some positionals or options before all options have been added.
// WARNING: It is best to only add the pre-parse options and read them through ITK_WASM_PRE_PARSE before adding other options,
// as you may face issues(EXCEPTIONS) generating bindings (bindgen), if you add "required" options/flags before the PRE_PARSE.
#define ITK_WASM_PRE_PARSE(pipeline) \
try { \
(pipeline).set_help_flag(); \
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"scripts": {
"commit": "git cz",
"bindgen": "node ./src/itk-wasm-cli.js bindgen ./dist/dicom/src ./dist/dicom/public/pipelines/*.wasm",
"build": "npm run build:emscripten && npm run build:tsc && npm run build:tscWorkersModuleLoader && npm run build:tscWebWorkers && npm run build:workerBundles && npm run build:workerMinBundles && npm run build:webpack && node ./src/io/internal/packages/package-json-gen.cjs && npm run build:emscripten:packages",
"build:testData": "dam download packages/dicom/test/data packages/dicom/test/data.tar.gz bafybeig4g7wosycckndpiouphtwowakccbkmunsvvgrg5bvjoz63p4s6hi https://w3s.link/ipfs/bafkreicrqj3nps6xep75zafmevtseitj6md2dgbce22jgizefuouc5vfca",
"build": "npm run build:testData && npm run build:emscripten && npm run build:tsc && npm run build:tscWorkersModuleLoader && npm run build:tscWebWorkers && npm run build:workerBundles && npm run build:workerMinBundles && npm run build:webpack && node ./src/io/internal/packages/package-json-gen.cjs && npm run build:emscripten:packages",
"build:testData": "dam download packages/dicom/test/data packages/dicom/test/data.tar.gz bafybeic2ckitzhl5b476fgfewt7ilel3mkzi5x66o6gga5s7n34g2t36xm https://w3s.link/ipfs/bafkreibxuanogkwccski66azxafdpjjj3sbua6g3pqdvwwwe72h3d6agoi",
"build:debug": "npm run build:emscripten -- --debug && npm run build:tsc && npm run build:tscWorkersModuleLoader && npm run build:tscWebWorkers && npm run build:workerBundles && npm run build:workerMinBundles && npm run build:webpack -- --mode development",
"build:tsc": "tsc --pretty",
"build:tscWorkersModuleLoader": "tsc --types --lib es2017,webworker --rootDir ./src/ --outDir ./dist/ --moduleResolution node --target es2017 --module es2020 --strict --forceConsistentCasingInFileNames --declaration ./src/core/internal/loadEmscriptenModuleWebWorker.ts",
Expand All @@ -36,11 +36,11 @@
"build:webpack": "webpack --mode production --progress --color && webpack --mode development --progress --color",
"build:emscripten": "node ./src/build-emscripten.js",
"build:emscripten:compress-stringify": "node ./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build build ",
"build:bindgen:typescript:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build bindgen --package-version 0.4.5 --package-name @itk-wasm/compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b wasi-build bindgen --language python --package-name itkwasm-compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --package-version 0.4.5 --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:typescript:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b emscripten-build bindgen --package-version 0.5.0 --package-name @itk-wasm/compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:compress-stringify": "./src/itk-wasm-cli.js -s packages/compress-stringify -b wasi-build bindgen --language python --package-name itkwasm-compress-stringify --package-description \"Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.\" --package-version 0.5.0 --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:emscripten:dicom": "node ./src/itk-wasm-cli.js -s packages/dicom -b emscripten-build build ",
"build:bindgen:typescript:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b emscripten-build bindgen --package-version 2.0.3 --package-name @itk-wasm/dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b wasi-build bindgen --package-version 2.0.3 --language python --package-name itkwasm-dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:typescript:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b emscripten-build bindgen --package-version 2.1.0 --package-name @itk-wasm/dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:bindgen:python:dicom": "./src/itk-wasm-cli.js -s packages/dicom -b wasi-build bindgen --package-version 2.1.0 --language python --package-name itkwasm-dicom --package-description \"Read files and images related to DICOM file format.\" --repository 'https://github.com/InsightSoftwareConsortium/itk-wasm'",
"build:emscripten:packages": "npm run build:emscripten:compress-stringify && npm run build:bindgen:typescript:compress-stringify && npm run build:emscripten:dicom && npm run build:bindgen:typescript:dicom",
"build:wasi": "node ./src/build-wasi.js && npm run build:wasi:packages",
"build:wasi:compress-stringify": "node ./src/itk-wasm-cli.js -i itkwasm/wasi:latest -s packages/compress-stringify -b wasi-build build",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.5"
__version__ = "0.5.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.5"
__version__ = "0.5.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.5"
__version__ = "0.5.0"
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated file. Do not edit.

import os
from typing import Dict, Tuple, Optional
from typing import Dict, Tuple, Optional, List

from itkwasm import (
environment_dispatch,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated file. Do not edit.

import os
from typing import Dict, Tuple, Optional
from typing import Dict, Tuple, Optional, List

from itkwasm import (
environment_dispatch,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated file. Do not edit.

import os
from typing import Dict, Tuple, Optional
from typing import Dict, Tuple, Optional, List

from itkwasm import (
environment_dispatch,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated file. Do not edit.

import os
from typing import Dict, Tuple, Optional
from typing import Dict, Tuple, Optional, List

from itkwasm import (
environment_dispatch,
Expand Down
4 changes: 2 additions & 2 deletions packages/compress-stringify/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itk-wasm/compress-stringify",
"version": "0.4.5",
"version": "0.5.0",
"description": "Zstandard compression and decompression and base64 encoding and decoding in WebAssembly.",
"type": "module",
"module": "./dist/bundles/compress-stringify.js",
Expand Down Expand Up @@ -38,7 +38,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
"itk-wasm": "^1.0.0-b.79"
"itk-wasm": "^1.0.0-b.111"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.0",
Expand Down
113 changes: 80 additions & 33 deletions packages/dicom/apply-presentation-state-to-image.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
#include "dcmtk/dcmdata/cmdlnarg.h"
#include "dcmtk/ofstd/ofcmdln.h"
#include "dcmtk/ofstd/ofconapp.h"
#include "dcmtk/ofstd/ofvector.h"
#include "dcmtk/dcmdata/dcuid.h" /* for dcmtk version name */

#include "cpp-base64/base64.h"
Expand Down Expand Up @@ -145,6 +146,15 @@ static void dumpPresentationState(STD_NAMESPACE ostream &out, DVPresentationStat
doc.AddMember("CurrentVOIDescription", Value(StringRef(ps.getCurrentVOIDescription())), alloc);
}

// ICC color Profile
const OFVector<Uint8> iccProfile = ps.getICCProfile();
if (!iccProfile.empty())
{
// Encode the binary color profile data as a base64 string
std::string iccProfileAsString(iccProfile.begin(), iccProfile.end());
doc.AddMember("ICCProfile", Value(base64_encode(iccProfileAsString, false).c_str(), alloc), alloc);
}

doc.AddMember("Flip", Value(ps.getFlip()), alloc);
int rotation = 0;
switch (ps.getRotation())
Expand Down Expand Up @@ -468,10 +478,59 @@ static void dumpPresentationState(STD_NAMESPACE ostream &out, DVPresentationStat
out << buffer.GetString();
}

constexpr unsigned int Dimension = 2;
using GrayPixelType = uint8_t;
using GrayImageType = itk::Image<GrayPixelType, Dimension>;
using OutputGrayImageType = itk::wasm::OutputImage<GrayImageType>;

using ColorPixelType = itk::RGBPixel<uint8_t>;
using ColorImageType = itk::Image<ColorPixelType, Dimension>;
using OutputColorImageType = itk::wasm::OutputImage<ColorImageType>;

template<typename OutputImageType, typename PixelType, unsigned int Dim>
int GenerateOutputImage(OutputImageType& outputImage, const unsigned long width, const unsigned long height, const std::array<double, 2>& pixelSpacing, const void* pixelData)
{
using ImportFilterType = itk::ImportImageFilter<PixelType, Dim>;
auto importFilter = itk::ImportImageFilter<PixelType, Dim>::New();
typename ImportFilterType::SizeType size;
size[0] = width;
size[1] = height;

typename itk::ImportImageFilter<PixelType, Dim>::IndexType start;
start.Fill(0);

typename itk::ImportImageFilter<PixelType, Dim>::RegionType region;
region.SetIndex(start);
region.SetSize(size);
importFilter->SetRegion(region);

const itk::SpacePrecisionType origin[Dim] = { 0.0, 0.0 };
importFilter->SetOrigin(origin);
const itk::SpacePrecisionType spacing[Dim] = { pixelSpacing[0], pixelSpacing[1] };
importFilter->SetSpacing(spacing);
const unsigned int numberOfPixels = size[0] * size[1];
// ColorImageType::Internal
importFilter->SetImportPointer((PixelType*)pixelData, numberOfPixels, false);
importFilter->Update();

// set as output image
outputImage.Set(importFilter->GetOutput());
return EXIT_SUCCESS;
}

template int GenerateOutputImage<OutputGrayImageType, GrayPixelType, 2U>(OutputGrayImageType&, const unsigned long, const unsigned long, const std::array<double, 2>&, const void*);
template int GenerateOutputImage<OutputColorImageType, ColorPixelType, 2U>(OutputColorImageType&, const unsigned long, const unsigned long, const std::array<double, 2>&, const void*);

int main(int argc, char *argv[])
{
itk::wasm::Pipeline pipeline("apply-presentation-state-to-image", "Apply a presentation state to a given DICOM image and render output as pgm bitmap or dicom file.", argc, argv);
itk::wasm::Pipeline pipeline("apply-presentation-state-to-image", "Apply a presentation state to a given DICOM image and render output as bitmap, or dicom file.", argc, argv);

// Expecting color output
bool colorOutput{false};
pipeline.add_flag("--color-output", colorOutput, "output image as RGB (default: false)");

// pre-parse command line to determine if we need color or gray output image
ITK_WASM_PRE_PARSE(pipeline);

// Inputs
std::string imageIn;
Expand All @@ -485,14 +544,6 @@ int main(int argc, char *argv[])
itk::wasm::OutputTextStream pstateOutStream;
pipeline.add_option("presentation-state-out-stream", pstateOutStream, "Output overlay information")->type_name("OUTPUT_JSON");

// Processed output image
constexpr unsigned int Dimension = 2;
using PixelType = unsigned char;
using ImageType = itk::Image<PixelType, Dimension>;
using OutputImageType = itk::wasm::OutputImage<ImageType>;
OutputImageType outputImage;
pipeline.add_option("output-image", outputImage, "Output image")->type_name("OUTPUT_IMAGE");

// Parameters
std::string configFile;
pipeline.add_option("--config-file", configFile, "filename: string. Process using settings from configuration file");
Expand All @@ -506,6 +557,18 @@ int main(int argc, char *argv[])
bool noBitmapOutput{false};
pipeline.add_flag("--no-bitmap-output", noBitmapOutput, "Do not get resulting image as bitmap output stream.");

// Define output image and bind to CLI option
OutputGrayImageType outputGrayImage;
OutputColorImageType outputColorImage;
if (colorOutput)
{
pipeline.add_option("output-image", outputColorImage, "Output image")->type_name("OUTPUT_IMAGE");
}
else
{
pipeline.add_option("output-image", outputGrayImage, "Output image")->type_name("OUTPUT_IMAGE");
}

// DICOM output is currently not supported.
// bool outputFormatPGM{true};
// pipeline.add_flag("--pgm", outputFormatPGM, "save image as PGM (default)");
Expand Down Expand Up @@ -585,30 +648,14 @@ int main(int argc, char *argv[])
}
else
{
using ImportFilterType = itk::ImportImageFilter<PixelType, Dimension>;
auto importFilter = ImportFilterType::New();
ImportFilterType::SizeType size;
size[0] = width;
size[1] = height;

ImportFilterType::IndexType start;
start.Fill(0);

ImportFilterType::RegionType region;
region.SetIndex(start);
region.SetSize(size);
importFilter->SetRegion(region);

const itk::SpacePrecisionType origin[Dimension] = { 0.0, 0.0 };
importFilter->SetOrigin(origin);
const itk::SpacePrecisionType spacing[Dimension] = { pixelSpacing[0], pixelSpacing[1] };
importFilter->SetSpacing(spacing);
const unsigned int numberOfPixels = size[0] * size[1];
importFilter->SetImportPointer((PixelType*)pixelData, numberOfPixels, false);
importFilter->Update();

// set as output image
outputImage.Set(importFilter->GetOutput());
if (colorOutput)
{
return GenerateOutputImage<OutputColorImageType, ColorPixelType, 2U>(outputColorImage, width, height, pixelSpacing, pixelData);
}
else
{
return GenerateOutputImage<OutputGrayImageType, GrayPixelType, 2U>(outputGrayImage, width, height, pixelSpacing, pixelData);
}
}
}
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.3"
__version__ = "2.1.0"
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,23 @@
async def apply_presentation_state_to_image_async(
image_in: os.PathLike,
presentation_state_file: os.PathLike,
color_output: bool = False,
config_file: str = "",
frame: int = 1,
no_presentation_state_output: bool = False,
no_bitmap_output: bool = False,
) -> Tuple[Dict, Image]:
"""Apply a presentation state to a given DICOM image and render output as pgm bitmap or dicom file.
"""Apply a presentation state to a given DICOM image and render output as bitmap, or dicom file.
:param image_in: Input DICOM file
:type image_in: os.PathLike
:param presentation_state_file: Process using presentation state file
:type presentation_state_file: os.PathLike
:param color_output: output image as RGB (default: false)
:type color_output: bool
:param config_file: filename: string. Process using settings from configuration file
:type config_file: str
Expand All @@ -55,6 +59,8 @@ async def apply_presentation_state_to_image_async(
web_worker = js_resources.web_worker

kwargs = {}
if color_output:
kwargs["colorOutput"] = to_js(color_output)
if config_file:
kwargs["configFile"] = to_js(config_file)
if frame:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ def input_data():
from pathlib import Path
input_base_path = Path('..', '..', 'test', 'data')
test_files = [
Path('input') / 'csps-input-image.dcm',
Path('input') / 'csps-input-pstate.dcm',
Path('input') / 'gsps-pstate-test-input-image.dcm',
Path('input') / 'gsps-pstate-test-input-pstate.dcm',
Path('baseline') / 'csps-pstate-baseline.json',
Path('baseline') / 'csps-output-image-baseline.bmp',
Path('baseline') / 'gsps-pstate-baseline.json',
Path('baseline') / 'gsps-pstate-image-baseline.pgm',
Path('input') / '104.1-SR-printed-to-pdf.dcm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,74 @@ def write_input_data_to_fs(input_data, filename):
# the slice operation removes the last EOF char from the baseline file.
buffer = fp.read()[:-1]
baseline_json_object = json.loads(buffer)
assert baseline_json_object['PresentationLabel'] == p_state_json_out['PresentationLabel']
assert baseline_json_object['PresentationSizeMode'] == p_state_json_out['PresentationSizeMode']

for key in baseline_json_object:
assert baseline_json_object[key] == p_state_json_out[key]

for key in p_state_json_out:
assert p_state_json_out[key] == baseline_json_object[key]

baseline_image = 'gsps-pstate-image-baseline.pgm'
baseline_buffer = input_data[baseline_image]
# slice to get only the pixel buffer from the baseline image (pgm file)
baseline_pixels = baseline_buffer[15:]
assert np.array_equal(np.frombuffer(baseline_pixels, dtype=np.uint8), output_image.data.ravel())
assert np.array_equal(np.frombuffer(baseline_pixels, dtype=np.uint8), output_image.data.ravel())

@run_in_pyodide(packages=['micropip','pillow'])
async def test_apply_color_presentation_state_to_dicom_image(selenium, package_wheel, input_data):
import json

import micropip
await micropip.install(package_wheel)
def write_input_data_to_fs(input_data, filename):
with open(filename, 'wb') as fp:
fp.write(input_data[filename])

from itkwasm_dicom_emscripten import apply_presentation_state_to_image_async
import numpy as np

input_file = 'csps-input-image.dcm'
write_input_data_to_fs(input_data, input_file)

p_state_file = 'csps-input-pstate.dcm'
write_input_data_to_fs(input_data, p_state_file)

from itkwasm.pyodide import to_js
p_state_json_out, output_image = await apply_presentation_state_to_image_async(input_file, p_state_file, color_output=True)

assert p_state_json_out != None
assert output_image != None

assert output_image.imageType.dimension == 2
assert output_image.imageType.componentType == 'uint8'
assert output_image.imageType.pixelType == 'RGB'
assert output_image.imageType.components == 3

assert np.array_equal(output_image.origin, [0, 0])
assert np.array_equal(output_image.spacing, [0.683, 0.683])
assert np.array_equal(output_image.direction, [[1, 0], [0, 1]])
assert np.array_equal(output_image.size, [768, 1024])

baseline_json_file = 'csps-pstate-baseline.json'
write_input_data_to_fs(input_data, baseline_json_file)
with open(baseline_json_file, 'r') as fp:
# the slice operation removes the last EOF char from the baseline file.
buffer = fp.read()[:-1]
baseline_json_object = json.loads(buffer)

for key in baseline_json_object:
assert baseline_json_object[key] == p_state_json_out[key]

for key in p_state_json_out:
assert p_state_json_out[key] == baseline_json_object[key]

baseline_image = 'csps-output-image-baseline.bmp'
write_input_data_to_fs(input_data, baseline_image)

from PIL import Image
im = Image.open(baseline_image)

baseline_pixels = np.array(im).flatten()
output_pixels = output_image.data.ravel().flatten()

assert np.array_equal(output_pixels, baseline_pixels)
Loading

0 comments on commit 00cb87f

Please sign in to comment.