Skip to content
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
18 changes: 14 additions & 4 deletions packages/dicom-codec/src/codecs/codecFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,20 @@ function encode(context, codecConfig, imageFrame, imageInfo, options = {}) {
* @returns Object containing decoded image frame and imageInfo (current) data
*
*/
function decode(context, codecConfig, imageFrame, imageInfo) {
function decode(context, codecConfig, imageFrame, imageInfo, options = {}) {
if (!imageFrame?.length) {
throw new Error("Image frame not defined for decoding");
}
const decoderInstance = new codecConfig.Decoder();
const reuseDecoder = options.reuseDecoder === true;
let decoderInstance;
if (reuseDecoder) {
if (!codecConfig.reusedDecoder) {
codecConfig.reusedDecoder = new codecConfig.Decoder();
}
decoderInstance = codecConfig.reusedDecoder;
} else {
decoderInstance = new codecConfig.Decoder();
}

const { length } = imageFrame;
// get pointer to the source/encoded bit stream buffer in WASM memory
Expand All @@ -277,8 +286,9 @@ function decode(context, codecConfig, imageFrame, imageInfo) {
// get information about the decoded image
const decodedImageInfo = decoderInstance.getFrameInfo();

// cleanup allocated memory
decoderInstance.delete();
if (!reuseDecoder) {
decoderInstance.delete();
}

const processInfo = {
duration: context.timer.getDuration(),
Expand Down
4 changes: 3 additions & 1 deletion packages/dicom-codec/src/codecs/htj2k.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ async function decode(imageFrame, imageInfo) {
codecWasmModule,
codecWrapper.decoderName,
(context) => {
return codecFactory.decode(context, codecWrapper, imageFrame, imageInfo);
return codecFactory.decode(context, codecWrapper, imageFrame, imageInfo, {
reuseDecoder: true,
});
}
);
}
Expand Down
13 changes: 4 additions & 9 deletions packages/openjphjs/bench/decode.bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@
// "warm" = a shared decoder/encoder that has already done 5 decode/encode
// passes at module load (untimed). The bench body is the 6th+ call.
//
// Important caveat for HTJ2K: cornerstone3D's decodeHTJ2K.ts:69 actually
// creates a fresh `new HTJ2KDecoder()` for every frame (a comment in
// that file notes reuse is "much slower for some reason"). So for
// HTJ2K specifically, the production-cost approximation is:
// HTJ2K production path (dicom-codec / cornerstone codecs) reuses a single
// HTJ2KDecoder across frames. Per-frame cost ≈ decode — warm.
//
// per-frame cost ≈ instantiate+destroy HTJ2KDecoder + decode — cold
//
// The "warm" HTJ2K decode bench remains useful for regression detection
// on the openjph decoder kernel itself, but isn't what cornerstone3D
// actually pays per frame.
// "cold" benches still model a fresh decoder per frame for lifecycle regressions.
// "warm" benches model the reused-decoder production path.
//
// Bench bodies are symmetric between cold and warm — the only difference
// is module-load state, so the cold/warm delta isolates first-call
Expand Down
2 changes: 1 addition & 1 deletion packages/openjphjs/extern/openjph
Submodule openjph updated 86 files
+14 −40 .github/workflows/ccp-workflow.yml
+1 −1 .github/workflows/codeql.yml
+0 −1 .gitignore
+241 −123 CMakeLists.txt
+124 −10 README.md
+2 −0 bin/.gitignore
+1 −1 docs/DoxygenStyle.md
+0 −116 docs/compiling.md
+0 −16 docs/docker.md
+0 −9 docs/status.md
+0 −19 docs/usage_examples.md
+0 −5 docs/web_demos.md
+0 −29 ojph_version.cmake
+0 −24 src/apps/CMakeLists.txt
+0 −236 src/apps/common/ojph_sockets.h
+0 −155 src/apps/common/ojph_threads.h
+0 −65 src/apps/ojph_compress/CMakeLists.txt
+69 −68 src/apps/ojph_compress/ojph_compress.cpp
+0 −65 src/apps/ojph_expand/CMakeLists.txt
+25 −27 src/apps/ojph_expand/ojph_expand.cpp
+0 −31 src/apps/ojph_stream_expand/CMakeLists.txt
+0 −373 src/apps/ojph_stream_expand/ojph_stream_expand.cpp
+0 −464 src/apps/ojph_stream_expand/stream_expand_support.cpp
+0 −554 src/apps/ojph_stream_expand/stream_expand_support.h
+0 −64 src/apps/ojph_stream_expand/threaded_frame_processors.cpp
+0 −107 src/apps/ojph_stream_expand/threaded_frame_processors.h
+34 −42 src/apps/others/ojph_img_io.cpp
+0 −202 src/apps/others/ojph_sockets.cpp
+0 −108 src/apps/others/ojph_threads.cpp
+0 −135 src/core/CMakeLists.txt
+5 −7 src/core/codestream/ojph_codeblock.cpp
+40 −53 src/core/codestream/ojph_codeblock_fun.cpp
+0 −20 src/core/codestream/ojph_codestream.cpp
+46 −65 src/core/codestream/ojph_codestream_local.cpp
+11 −25 src/core/codestream/ojph_codestream_local.h
+51 −518 src/core/codestream/ojph_params.cpp
+86 −360 src/core/codestream/ojph_params_local.h
+10 −24 src/core/codestream/ojph_precinct.cpp
+3 −2 src/core/codestream/ojph_precinct.h
+459 −571 src/core/codestream/ojph_resolution.cpp
+8 −20 src/core/codestream/ojph_resolution.h
+19 −35 src/core/codestream/ojph_subband.cpp
+2 −21 src/core/codestream/ojph_subband.h
+17 −15 src/core/codestream/ojph_tile.cpp
+3 −2 src/core/codestream/ojph_tile.h
+4 −6 src/core/codestream/ojph_tile_comp.cpp
+1 −2 src/core/codestream/ojph_tile_comp.h
+5 −5 src/core/coding/ojph_block_common.cpp
+8 −8 src/core/coding/ojph_block_decoder.cpp
+9 −9 src/core/coding/ojph_block_decoder_ssse3.cpp
+2 −2 src/core/coding/ojph_block_decoder_wasm.cpp
+6 −45 src/core/common/ojph_arch.h
+0 −11 src/core/common/ojph_arg.h
+22 −259 src/core/common/ojph_codestream.h
+75 −87 src/core/common/ojph_file.h
+0 −10 src/core/common/ojph_mem.h
+30 −186 src/core/common/ojph_message.h
+59 −8 src/core/common/ojph_params.h
+2 −2 src/core/common/ojph_version.h
+3 −61 src/core/others/ojph_arch.cpp
+32 −82 src/core/others/ojph_file.cpp
+16 −35 src/core/others/ojph_message.cpp
+41 −53 src/core/transform/ojph_colour.cpp
+1 −1 src/core/transform/ojph_colour_sse.cpp
+1 −1 src/core/transform/ojph_colour_sse2.cpp
+356 −415 src/core/transform/ojph_transform.cpp
+32 −22 src/core/transform/ojph_transform.h
+236 −160 src/core/transform/ojph_transform_avx.cpp
+181 −434 src/core/transform/ojph_transform_avx2.cpp
+0 −818 src/core/transform/ojph_transform_avx512.cpp
+158 −278 src/core/transform/ojph_transform_local.h
+222 −160 src/core/transform/ojph_transform_sse.cpp
+175 −378 src/core/transform/ojph_transform_sse2.cpp
+391 −624 src/core/transform/ojph_transform_wasm.cpp
+9 −9 src/pkg-config.pc.cmake
+2 −2 subprojects/js/CMakeLists.txt
+ subprojects/js/html/libopenjph.wasm
+ subprojects/js/html/libopenjph_simd.wasm
+0 −68 target_arch.cmake
+6 −46 tests/CMakeLists.txt
+20 −25 tests/mse_pae.cmake
+2 −2 tests/mse_pae.cpp
+15 −0 tests/test.py
+58 −141 tests/test_executables.cpp
+2 −5 tests/test_helpers/convert_mse_pae_to_tests.cpp
+22 −27 tests/test_helpers/ht_cmdlines.txt
9 changes: 7 additions & 2 deletions packages/openjphjs/src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@

add_executable(openjphjs jslib.cpp)

target_link_libraries(openjphjs PRIVATE openjphsimd)
target_link_libraries(openjphjs PRIVATE openjph)
target_include_directories(
openjphjs
PRIVATE
${PROJECT_SOURCE_DIR}/extern/openjph/src/core/openjph
)
target_compile_options(openjphjs PRIVATE -DOJPH_ENABLE_WASM_SIMD -msimd128)
target_compile_features(openjphjs PUBLIC cxx_std_11)
set_target_properties(
Expand All @@ -11,7 +16,7 @@ set_target_properties(
-O3 \
-s WASM=1 \
--bind \
-s DISABLE_EXCEPTION_CATCHING=1 \
-s DISABLE_EXCEPTION_CATCHING=0 \
-s ASSERTIONS=0 \
-s MODULARIZE=1 \
-s NO_EXIT_RUNTIME=1 \
Expand Down
49 changes: 35 additions & 14 deletions packages/openjphjs/src/HTJ2KDecoder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,17 @@ class HTJ2KDecoder
/// </summary>
void readHeader()
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
try
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
}
catch (const std::exception &e)
{
OJPH_INFO(0x00010020, "readHeader failed: %s", e.what());
}
}

/// <summary>
Expand All @@ -148,11 +155,18 @@ class HTJ2KDecoder
/// </summary>
void decode()
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
decode_(codestream, frameInfo_, 0);
try
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
decode_(codestream, frameInfo_, 0);
}
catch (const std::exception &e)
{
OJPH_INFO(0x00010021, "decode failed (likely truncated stream): %s", e.what());
}
}

/// <summary>
Expand All @@ -163,11 +177,18 @@ class HTJ2KDecoder
/// </summary>
void decodeSubResolution(size_t decompositionLevel)
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
decode_(codestream, frameInfo_, decompositionLevel);
try
{
ojph::codestream codestream;
ojph::mem_infile mem_file;
mem_file.open(pEncoded_->data(), pEncoded_->size());
readHeader_(codestream, mem_file);
decode_(codestream, frameInfo_, decompositionLevel);
}
catch (const std::exception &e)
{
OJPH_INFO(0x00010022, "decodeSubResolution failed: %s", e.what());
}
}

/// <summary>
Expand Down
172 changes: 94 additions & 78 deletions packages/openjphjs/test/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,119 @@
// SPDX-License-Identifier: MIT

let openjphjs = require("../../dist/openjphjs.js")
const assert = require("assert")
const fs = require("fs")
const path = require("path")

function decode(openjph, encodedImagePath, iterations = 100) {
const encodedBitStream = fs.readFileSync(encodedImagePath)
const decoder = new openjph.HTJ2KDecoder()
const encodedBuffer = decoder.getEncodedBuffer(encodedBitStream.length)
encodedBuffer.set(encodedBitStream)
const rawPath = path.resolve(__dirname, "../fixtures/raw/CT1.RAW")
const frameInfo = {
width: 512,
height: 512,
bitsPerSample: 16,
componentCount: 1,
isSigned: true,
isUsingColorTransform: false,
}

function encodeFrame(openjph, rawBytes, imageFrame, options = {}) {
const encoder = new openjph.HTJ2KEncoder()
const decodedBytes = encoder.getDecodedBuffer(imageFrame)
decodedBytes.set(rawBytes)

// do the actual benchmark
const beginDecode = process.hrtime()
for (var i = 0; i < iterations; i++) {
decoder.decode()
if (typeof options.lossless === "boolean") {
encoder.setQuality(options.lossless, options.quantizationStep || 0)
}
const decodeDuration = process.hrtime(beginDecode) // hrtime returns seconds/nanoseconds tuple
const decodeDurationInSeconds =
decodeDuration[0] + decodeDuration[1] / 1000000000

// Print out information about the decode
console.log(
"Decode of " +
encodedImagePath +
" took " +
(decodeDurationInSeconds / iterations) * 1000 +
" ms"
)
const frameInfo = decoder.getFrameInfo()
console.log(" frameInfo = ", frameInfo)
console.log(" imageOffset = ", decoder.getImageOffset())
var decoded = decoder.getDecodedBuffer()
console.log(" decoded length = ", decoded.length)
encoder.encode()
const encoded = Uint8Array.from(encoder.getEncodedBuffer())
encoder.delete()
return encoded
}

function decodeFrame(openjph, encodedBytes) {
const decoder = new openjph.HTJ2KDecoder()
const encodedBuffer = decoder.getEncodedBuffer(encodedBytes.length)
encodedBuffer.set(encodedBytes)
decoder.decode()
const decoded = Uint8Array.from(decoder.getDecodedBuffer())
const decodedFrameInfo = decoder.getFrameInfo()
decoder.delete()
return { decoded, decodedFrameInfo }
}

function encode(
openjph,
pathToUncompressedImageFrame,
imageFrame,
pathToJ2CFile,
iterations = 100
) {
const uncompressedImageFrame = fs.readFileSync(pathToUncompressedImageFrame)
console.log("uncompressedImageFrame.length:", uncompressedImageFrame.length)
const encoder = new openjph.HTJ2KEncoder()
const decodedBytes = encoder.getDecodedBuffer(imageFrame)
decodedBytes.set(uncompressedImageFrame)
//encoder.setQuality(false, 0.001);
function meanAbsoluteErrorI16(originalBytes, decodedBytes) {
assert.strictEqual(
decodedBytes.length,
originalBytes.length,
"Decoded byte length mismatch"
)

const original = new Int16Array(
originalBytes.buffer,
originalBytes.byteOffset,
originalBytes.byteLength / Int16Array.BYTES_PER_ELEMENT
)
const decoded = new Int16Array(
decodedBytes.buffer,
decodedBytes.byteOffset,
decodedBytes.byteLength / Int16Array.BYTES_PER_ELEMENT
)

const encodeBegin = process.hrtime()
for (var i = 0; i < iterations; i++) {
encoder.encode()
let absoluteErrorSum = 0
for (let i = 0; i < original.length; i++) {
absoluteErrorSum += Math.abs(original[i] - decoded[i])
}
const encodeDuration = process.hrtime(encodeBegin)
const encodeDurationInSeconds =
encodeDuration[0] + encodeDuration[1] / 1000000000

// print out information about the encode
console.log(
"Encode of " +
pathToUncompressedImageFrame +
" took " +
(encodeDurationInSeconds / iterations) * 1000 +
" ms"
return absoluteErrorSum / original.length
}

function runLossyRoundTripTest(openjph, rawBytes) {
const encodedLossy = encodeFrame(openjph, rawBytes, frameInfo, {
lossless: false,
quantizationStep: 8,
})
const { decoded, decodedFrameInfo } = decodeFrame(openjph, encodedLossy)
const mae = meanAbsoluteErrorI16(rawBytes, decoded)

assert.strictEqual(decodedFrameInfo.width, frameInfo.width)
assert.strictEqual(decodedFrameInfo.height, frameInfo.height)
console.log(`Heavy lossy round-trip MAE: ${mae.toFixed(2)}`)
assert.ok(mae < 1500, `Heavy lossy MAE too large: ${mae}`)
}

function runTruncatedLosslessDecodeTest(openjph, rawBytes) {
const encodedLossless = encodeFrame(openjph, rawBytes, frameInfo, {
lossless: true,
quantizationStep: 0,
})
const truncatedSize = Math.min(10 * 1024, encodedLossless.length)
const truncatedBitstream = encodedLossless.slice(0, truncatedSize)
const { decoded, decodedFrameInfo } = decodeFrame(openjph, truncatedBitstream)
assert.ok(
decoded.length > 0,
`Expected a minimally decodable image from ${truncatedSize} bytes`
)
const encodedBytes = encoder.getEncodedBuffer()
console.log(" encoded length=", encodedBytes.length)
const mae = meanAbsoluteErrorI16(rawBytes, decoded)

if (pathToJ2CFile) {
//fs.writeFileSync(pathToJ2CFile, encodedBytes);
}
// cleanup allocated memory
encoder.delete()
assert.strictEqual(decodedFrameInfo.width, frameInfo.width)
assert.strictEqual(decodedFrameInfo.height, frameInfo.height)
console.log(
`Truncated lossless decode MAE (${truncatedSize} bytes kept): ${mae.toFixed(2)}`
)
assert.ok(mae > 10, `Expected degradation with truncated stream, MAE: ${mae}`)
assert.ok(mae < 300, `Truncated lossless MAE too large: ${mae}`)
}

function main(openjph) {
decode(openjph, "../fixtures/j2c/CT2.j2c")
decode(openjph, "../../extern/OpenJPH/subprojects/js/html/test.j2c")

encode(
openjph,
"../fixtures/raw/CT1.RAW",
{
width: 512,
height: 512,
bitsPerSample: 16,
componentCount: 1,
isSigned: true,
},
"../fixtures/j2c/CT1.j2c"
)
const rawBytes = fs.readFileSync(rawPath)
runLossyRoundTripTest(openjph, rawBytes)
runTruncatedLosslessDecodeTest(openjph, rawBytes)
console.log("openjphjs node tests passed")
}

if (typeof openjphjs !== "undefined") {
console.log("testing openjphjs...")
openjphjs().then(function (openjphwasm) {
main(openjphwasm)
})
console.log("running openjphjs node tests...")
openjphjs().then(main)
} else {
console.warn("openjphjs isn't defined");
console.warn("openjphjs isn't defined")
}
Loading
Loading