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());
}
Comment on lines +163 to +169

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)HTJ2KDecoder\.hpp$|openjphjs'

echo "== HTJ2KDecoder outline =="
ast-grep outline packages/openjphjs/src/HTJ2KDecoder.hpp || true

echo "== relevant HTJ2KDecoder.hpp lines =="
wc -l packages/openjphjs/src/HTJ2KDecoder.hpp
cat -n packages/openjphjs/src/HTJ2KDecoder.hpp | sed -n '1,240p'

echo "== search result state methods/usages =="
rg -n "pDecoded_|decoded|resize\\(|decode\\(|decodeSubResolution|readHeader_|OJPH_INFO\\(0x0001002" packages/openjphjs/src packages/openjphjs -g '!**/node_modules/**' || true

Repository: cornerstonejs/codecs

Length of output: 31773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== readHeader_/decode_ implementation =="
cat -n packages/openjphjs/src/HTJ2KDecoder.hpp | sed -n '270,455p'

echo "== decoder class members before implementation =="
cat -n packages/openjphjs/src/HTJ2KDecoder.hpp | sed -n '236,290p'

echo "== tests using reuse/failure paths =="
cat -n packages/openjphjs/test/truncated.test.js | sed -n '140,285p'
cat -n packages/openjphjs/test/node/index.js | sed -n '84,105p'

Repository: cornerstonejs/codecs

Length of output: 16378


🌐 Web query:

C++ std::vector resize preserves elements unchanged when size stays same documentation

💡 Result:

Yes, the C++ standard explicitly states that std::vector::resize does nothing if the requested size equals the current size of the vector [1][2][3]. Because the function performs no operations when the size remains the same, all existing elements are preserved unchanged, and no iterators, references, or pointers to the elements are invalidated [4]. The C++ standard defines the effects of resize as follows: if the new size is less than the current size, it erases the excess elements at the end; if it is greater, it appends additional default-inserted or specified elements [5]. When the new size is identical to the current size, no elements are erased or appended, resulting in no changes to the container's state [5][4].

Citations:


Reset decoder result state after incomplete decode.

decode() and decodeSubResolution() catch failures and return normally, but they leave frameInfo_, metadata, and pDecoded_ from previous successful decodes unchanged or only resized. Reused decoders must clear result state before each decode attempt, initialize the destination buffer before line pulls, and report a failure/completion status so callers do not return partial pixels or stale metadata.

This applies to:

  • packages/openjphjs/src/HTJ2KDecoder.hpp#L156-L170
  • packages/openjphjs/src/HTJ2KDecoder.hpp#L178-L192
📍 Affects 1 file
  • packages/openjphjs/src/HTJ2KDecoder.hpp#L163-L169 (this comment)
  • packages/openjphjs/src/HTJ2KDecoder.hpp#L185-L191
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openjphjs/src/HTJ2KDecoder.hpp` around lines 163 - 169, Update
decode() at packages/openjphjs/src/HTJ2KDecoder.hpp#L156-L170 and
decodeSubResolution() at packages/openjphjs/src/HTJ2KDecoder.hpp#L178-L192 to
clear frameInfo_, metadata, and pDecoded_ before each attempt, initialize the
destination buffer before pulling lines, and return or expose an explicit
failure/completion status when exceptions occur so callers cannot observe
partial pixels or stale results.

}

/// <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)
Comment on lines +90 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guarantee partial-stream coverage.

Math.min(limit, encodedLength) permits a full stream when the fixture is small. Require an encoded fixture larger than the limit, then slice at the fixed limit.

  • packages/openjphjs/test/node/index.js#L90-L91: assert that encodedLossless.length exceeds 10 KiB before slicing at 10 KiB.
  • packages/openjphjs/test/truncated.test.js#L112-L113: assert that encodedLossless.length exceeds TRUNCATED_BYTE_LIMIT before slicing.
  • packages/openjphjs/test/truncated.test.js#L164-L165: apply the same assertion and fixed slice before the truncated performance benchmark.
📍 Affects 2 files
  • packages/openjphjs/test/node/index.js#L90-L91 (this comment)
  • packages/openjphjs/test/truncated.test.js#L112-L113
  • packages/openjphjs/test/truncated.test.js#L164-L165
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openjphjs/test/node/index.js` around lines 90 - 91, Ensure the
encoded lossless fixture exceeds the truncation limit before creating partial
streams, then always slice at the fixed limit. In
packages/openjphjs/test/node/index.js lines 90-91, assert encodedLossless.length
exceeds 10 KiB and replace the Math.min-based slice; apply the same assertion
and fixed TRUNCATED_BYTE_LIMIT slice in
packages/openjphjs/test/truncated.test.js lines 112-113 and 164-165 before the
benchmark.

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