Skip to content
Closed
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
15 changes: 12 additions & 3 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: PR checks
# runs concurrently, any failure fails the workflow.
#
# CodSpeed's first-class GHA integration replaces the manual codspeed-bench
# job from CircleCI: `CodSpeedHQ/action@v3` installs valgrind, sets up
# job from CircleCI: `CodSpeedHQ/action` installs valgrind, sets up
# instrumentation, and uploads results in one step.

on:
Expand Down Expand Up @@ -187,7 +187,13 @@ jobs:
codspeed-bench:
needs: [detect-changes, build]
if: needs.detect-changes.outputs.any == 'true'
runs-on: ubuntu-latest
# Pin the OS (not ubuntu-latest) so PR runs are measured in the same
# runtime environment as the baseline seeded on main. A drifting
# ubuntu-latest image changes the valgrind/toolchain fingerprint between
# baseline and PR, which makes CodSpeed report "Different runtime
# environments detected" and refuse to trust the comparison. The baseline
# on main must be re-seeded after changing this so both sides match.
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down Expand Up @@ -236,7 +242,10 @@ jobs:
# See BENCHMARKING.md at the repo root for the full measurement
# model, how to read the cold/warm bench split, and what the
# CodSpeed dashboard warnings mean.
uses: CodSpeedHQ/action@v4
# Pinned to a full version (not the floating @v4) so the
# instrumentation environment stays identical between the main
# baseline and PR runs.
uses: CodSpeedHQ/action@v4.18.2
with:
mode: simulation
run: yarn run bench
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"scripts": {
"build:all": "lerna run build:ci --parallel --stream",
"test": "vitest run",
"test:all": "lerna run test:ci --parallel --stream",
"bench": "lerna run bench --parallel --stream",
"bench:ci": "lerna run bench --parallel --stream",
Expand Down
116 changes: 62 additions & 54 deletions packages/dicom-codec/src/codecs/codecFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,38 +203,42 @@ function getImageFrame(typedArray) {
function encode(context, codecConfig, imageFrame, imageInfo, options = {}) {
const { iterations = 1 } = options;
const encoderInstance = new codecConfig.Encoder();
const decodedTypedArray = encoderInstance.getDecodedBuffer(imageInfo);
decodedTypedArray.set(imageFrame);
try {
const decodedTypedArray = encoderInstance.getDecodedBuffer(imageInfo);
decodedTypedArray.set(imageFrame);

const { beforeEncode = () => {} } = options;
const { beforeEncode = () => {} } = options;

beforeEncode(encoderInstance, codecConfig);
beforeEncode(encoderInstance, codecConfig);

context.timer.init("To encode length: " + imageFrame.length);
for (let i = 0; i < iterations; i++) {
encoderInstance.encode();
}
context.timer.init("To encode length: " + imageFrame.length);
for (let i = 0; i < iterations; i++) {
encoderInstance.encode();
}

context.timer.end();
context.timer.end();

const encodedTypedArray = encoderInstance.getEncodedBuffer();
context.logger.log("Encoded length:" + encodedTypedArray.length);
context.logger.log(
"Encoded is a Typed array of: " + encodedTypedArray.constructor.name
);
const encodedTypedArray = encoderInstance.getEncodedBuffer();
context.logger.log("Encoded length:" + encodedTypedArray.length);
context.logger.log(
"Encoded is a Typed array of: " + encodedTypedArray.constructor.name
);

// cleanup allocated memory
encoderInstance.delete();
const imageFrameOut = getImageFrame(encodedTypedArray);

const processInfo = {
duration: context.timer.getDuration(),
};
const processInfo = {
duration: context.timer.getDuration(),
};

return {
imageFrame: getImageFrame(encodedTypedArray),
imageInfo: getTargetImageInfo(imageInfo, imageInfo),
processInfo,
};
return {
imageFrame: imageFrameOut,
imageInfo: getTargetImageInfo(imageInfo, imageInfo),
processInfo,
};
} finally {
// cleanup allocated memory
encoderInstance.delete();
}
}

/**
Expand All @@ -255,40 +259,44 @@ function decode(context, codecConfig, imageFrame, imageInfo) {
}
const decoderInstance = new codecConfig.Decoder();

const { length } = imageFrame;
// get pointer to the source/encoded bit stream buffer in WASM memory
// that can hold the encoded bitstream
const encodedTypedArray = decoderInstance.getEncodedBuffer(length);

// copy the encoded bitstream into WASM memory buffer
encodedTypedArray.set(imageFrame);
context.timer.init("To decode length: " + length);
// decode it
decoderInstance.decode();
context.timer.end();
try {
const { length } = imageFrame;
// get pointer to the source/encoded bit stream buffer in WASM memory
// that can hold the encoded bitstream
const encodedTypedArray = decoderInstance.getEncodedBuffer(length);

// copy the encoded bitstream into WASM memory buffer
encodedTypedArray.set(imageFrame);
context.timer.init("To decode length: " + length);
// decode it
decoderInstance.decode();
context.timer.end();

const decodedTypedArray = decoderInstance.getDecodedBuffer();

context.logger.log("Decoded length:" + decodedTypedArray.length);
context.logger.log(
"Decoded is a Typed array of: " + decodedTypedArray.constructor.name
);

const decodedTypedArray = decoderInstance.getDecodedBuffer();
// get information about the decoded image
const decodedImageInfo = decoderInstance.getFrameInfo();

context.logger.log("Decoded length:" + decodedTypedArray.length);
context.logger.log(
"Decoded is a Typed array of: " + decodedTypedArray.constructor.name
);
const imageFrameOut = getImageFrame(decodedTypedArray);

// get information about the decoded image
const decodedImageInfo = decoderInstance.getFrameInfo();
const processInfo = {
duration: context.timer.getDuration(),
};

// cleanup allocated memory
decoderInstance.delete();

const processInfo = {
duration: context.timer.getDuration(),
};

return {
imageFrame: getImageFrame(decodedTypedArray),
imageInfo: getTargetImageInfo(imageInfo, decodedImageInfo),
processInfo,
};
return {
imageFrame: imageFrameOut,
imageInfo: getTargetImageInfo(imageInfo, decodedImageInfo),
processInfo,
};
} finally {
// cleanup allocated memory
decoderInstance.delete();
}
}

exports.runProcess = runProcess;
Expand Down
36 changes: 27 additions & 9 deletions packages/dicom-codec/src/codecs/libjpegTurbo12bit.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
const codecModule = require("@cornerstonejs/codec-libjpeg-turbo-12bit");
const codecWasmModule = require("@cornerstonejs/codec-libjpeg-turbo-12bit/wasmjs");
const codecFactory = require("./codecFactory");

/**
* @type {CodecWrapper}
*/
const codecWrapper = {
// assign it and prevent initialization
codec: undefined,
Decoder: undefined,
Encoder: undefined,
decoderName: "codec libjpeg turbo 12bit",
encoderName: "codec libjpeg turbo 12bit",
encoderName: "JPEGEncoder",
decoderName: "JPEGDecoder",
};

/**
* Decode imageFrame using libjpegTurbo 12bit decoder.
*
* @param {TypedArray} imageFrame to decode.
* @param {ExtendedImageInfo} imageInfo image info options.
* @returns Object containing decoded image frame and imageInfo (current) data.
*/
async function decode(imageFrame, imageInfo) {
throw Error("Decoder not found for codec:" + codecWrapper.encoderName);
return codecFactory.runProcess(
codecWrapper,
codecModule,
codecWasmModule,
codecWrapper.decoderName,
(context) => {
return codecFactory.decode(context, codecWrapper, imageFrame, imageInfo);
}
);
}

/**
* <<Not available yet>> Encode imageFrame to libjpegTurbo 12bits format.
* <<Not available>> The libjpeg-turbo 12bit build does not expose an
* encoder (see src/jslib.cpp — the JPEGEncoder bindings are disabled), so
* encoding is not supported for this codec.
*
* @param {TypedArray} imageFrame to encode.
* @param {ExtendedImageInfo} imageInfo image info options.
* @param {Object} options encode option.
* @returns Object containing encoded image frame and imageInfo (current) data
*/
async function encode(imageFrame, imageInfo, options = {}) {
throw Error("Encoder not found for codec:" + codecWrapper.encoderName);
throw Error("Encoder not supported for codec: libjpeg-turbo 12bit");
}

function getPixelData(imageFrame, imageInfo) {
throw Error(
"GetPixel not found or not applied for codec:" + codecWrapper.encoderName
);
return codecFactory.getPixelData(imageFrame, imageInfo);
}

exports.decode = decode;
Expand Down
41 changes: 41 additions & 0 deletions packages/dicom-codec/test/dispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,47 @@ const SUPPORTED_UIDS = [
"1.2.840.10008.1.2.5",
]

describe("codecFactory instance cleanup", () => {
it("frees the decoder instance even when decode() throws", () => {
const codecFactory = require("../src/codecs/codecFactory")

let deleted = false

class FakeDecoder {
getEncodedBuffer() {
return { set: () => {} }
}

decode() {
throw new Error("boom")
}

delete() {
deleted = true
}
}

const codecConfig = { Decoder: FakeDecoder }
const context = {
timer: {
init: () => {},
end: () => {},
getDuration: () => 0,
},
logger: {
log: () => {},
},
}
const imageFrame = new Uint8Array([1, 2, 3])
const imageInfo = {}

expect(() =>
codecFactory.decode(context, codecConfig, imageFrame, imageInfo)
).toThrow("boom")
expect(deleted).toBe(true)
})
})

describe.skipIf(!ALL_BUILT)("dicom-codec dispatcher", () => {
let dicomCodec

Expand Down
40 changes: 39 additions & 1 deletion packages/dicom-codec/test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const packagesRoot = resolve(__dirname, "../..")
const LIBJPEG_8BIT_BUILT = existsSync(
resolve(packagesRoot, "libjpeg-turbo-8bit/dist/libjpegturbojs.js")
)
const LIBJPEG_12BIT_BUILT = existsSync(
resolve(packagesRoot, "libjpeg-turbo-12bit/dist/libjpegturbo12js.js")
)
const CHARLS_BUILT = existsSync(
resolve(packagesRoot, "charls/dist/charlsjs.js")
)
Expand All @@ -20,7 +23,11 @@ const OPENJPH_BUILT = existsSync(
)

const ALL_BUILT =
LIBJPEG_8BIT_BUILT && CHARLS_BUILT && OPENJPEG_BUILT && OPENJPH_BUILT
LIBJPEG_8BIT_BUILT &&
LIBJPEG_12BIT_BUILT &&
CHARLS_BUILT &&
OPENJPEG_BUILT &&
OPENJPH_BUILT

describe.skipIf(!ALL_BUILT)("dicom-codec integration", () => {
let dicomCodec
Expand Down Expand Up @@ -61,6 +68,37 @@ describe.skipIf(!ALL_BUILT)("dicom-codec integration", () => {
})
})

describe("JPEG Baseline 12-bit (1.2.840.10008.1.2.4.51)", () => {
const jpeg12BitBytes = readFileSync(
resolve(
packagesRoot,
"libjpeg-turbo-12bit/test/fixtures/jpeg/CT-512x512-12bit.jpg"
)
)

it("decodes through the dispatcher", async () => {
const imageInfo = {
rows: 512,
columns: 512,
bitsAllocated: 16,
samplesPerPixel: 1,
pixelRepresentation: 0,
signed: false,
}

const result = await dicomCodec.decode(
jpeg12BitBytes,
imageInfo,
"1.2.840.10008.1.2.4.51"
)

expect(result.imageFrame.byteLength).toBe(512 * 512 * 2)
expect(result.imageInfo.width).toBe(512)
expect(result.imageInfo.height).toBe(512)
expect(typeof result.processInfo.duration).toBe("number")
})
})

describe("JPEG-LS Lossless (1.2.840.10008.1.2.4.80)", () => {
const jlsBytes = readFileSync(
resolve(packagesRoot, "charls/test/fixtures/CT1.JLS")
Expand Down
9 changes: 7 additions & 2 deletions packages/libjpeg-turbo-12bit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"name": "@cornerstonejs/codec-libjpeg-turbo-12bit",
"version": "0.4.1",
"description": "JS/WASM Build of [libjpeg-turbo](https://github.com/libjpeg-turbo) WITH12BIT=ON",
"main": "dist/libjpeg-turbojs.js",
"main": "dist/libjpegturbo12js.js",
"exports": {
".": "./dist/libjpegturbo12js.js",
"./wasmjs": "./dist/libjpegturbo12wasm.js"
},
"publishConfig": {
"access": "public"
},
Expand All @@ -20,8 +24,9 @@
"scripts": {
"build": "bash build.sh",
"build:ci": "yarn run build",
"test": "echo 'libjpeg-turbo-12bit tests skipped (.51 transfer syntax disabled)'",
"test": "vitest run",
"test:ci": "yarn run test",
"test:watch": "vitest",
"prepublishOnly": "yarn run build"
},
"author": "",
Expand Down
Loading
Loading