Skip to content

Commit

Permalink
Add DICOM format support
Browse files Browse the repository at this point in the history
Signed-off-by: Artem Senichev <[email protected]>
  • Loading branch information
artemsen committed Dec 1, 2024
1 parent c36a0bd commit 0dae133
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Fully customizable and lightweight image viewer for Wayland based display server
- PNM (built-in);
- TGA (built-in);
- QOI (built-in);
- DICOM (built-in);
- Farbfeld (built-in).
- Fully customizable keyboard bindings, colors, and [many other](https://github.com/artemsen/swayimg/blob/master/extra/swayimgrc) parameters;
- Loading images from files and pipes;
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ sources = [
'src/ui.c',
'src/viewer.c',
'src/formats/bmp.c',
'src/formats/dicom.c',
'src/formats/farbfeld.c',
'src/formats/pnm.c',
'src/formats/qoi.c',
Expand Down
253 changes: 253 additions & 0 deletions src/formats/dicom.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// SPDX-License-Identifier: MIT
// DICOM format decoder.
// Copyright (C) 2024 Artem Senichev <[email protected]>

#include "../loader.h"

#include <limits.h>
#include <string.h>

// DICOM signature
static const uint8_t signature[] = { 'D', 'I', 'C', 'M' };
#define DICOM_SIGNATURE_OFFSET 128

// DICOM tags
#define TAG_SAMPLES_PER_PIXEL 0x00280002
#define TAG_ROWS 0x00280010
#define TAG_COLUMNS 0x00280011
#define TAG_BIT_ALLOCATED 0x00280100
#define TAG_SMALL_PIXEL_VAL 0x00280106
#define TAG_BIG_PIXEL_VAL 0x00280107
#define TAG_PIXEL_DATA 0x7fe00010

// DICOM element value types
enum value_representation {
VR_AE = 'A' | ('E' << 8),
VR_AS = 'A' | ('S' << 8),
VR_AT = 'A' | ('T' << 8),
VR_CS = 'C' | ('S' << 8),
VR_DA = 'D' | ('A' << 8),
VR_DS = 'D' | ('S' << 8),
VR_DT = 'D' | ('T' << 8),
VR_FD = 'F' | ('D' << 8),
VR_FL = 'F' | ('L' << 8),
VR_IS = 'I' | ('S' << 8),
VR_LO = 'L' | ('O' << 8),
VR_LT = 'L' | ('T' << 8),
VR_PN = 'P' | ('N' << 8),
VR_SH = 'S' | ('H' << 8),
VR_SL = 'S' | ('L' << 8),
VR_SS = 'S' | ('S' << 8),
VR_ST = 'S' | ('T' << 8),
VR_TM = 'T' | ('M' << 8),
VR_UI = 'U' | ('I' << 8),
VR_UL = 'U' | ('L' << 8),
VR_US = 'U' | ('S' << 8),
VR_UT = 'U' | ('T' << 8),
VR_OB = 'O' | ('B' << 8),
VR_OW = 'O' | ('W' << 8),
VR_SQ = 'S' | ('Q' << 8),
VR_UN = 'U' | ('N' << 8),
VR_QQ = 'Q' | ('Q' << 8),
VR_RT = 'R' | ('T' << 8),
};

// DICOM image description
struct dicom_image {
uint16_t spp; ///< Samples per Pixel
uint16_t bpp; ///< Number of bits allocated for each pixel sample
uint16_t width; ///< Image width
uint16_t height; ///< Image height
int16_t px_min; ///< Min pixel value encountered in the image
int16_t px_max; ///< Max pixel value encountered in the image
const uint8_t* data; ///< Image data
size_t data_sz; ///< Size of data in bytes
};

// DICOM element description
struct element {
uint32_t tag;
uint16_t vr;
const void* data;
size_t size;
};

// Binary stream
struct stream {
const uint8_t* data;
size_t size;
size_t pos;
};

/**
* Consume data from the stream.
* @param stream binary stream
* @param bytes number of bytes to consume
* @return pointer to the data or NULL on End of stream
*/
static const uint8_t* consume(struct stream* stream, size_t bytes)
{
const uint8_t* ptr = NULL;
const size_t end = stream->pos + bytes;

if (end <= stream->size) {
ptr = stream->data + stream->pos;
stream->pos = end;
}

return ptr;
}

/**
* Read next data element from the stream.
* @param stream binary stream
* @param element output element description
* @return false if no more elements int the stream
*/
static bool next_element(struct stream* stream, struct element* element)
{
const uint8_t* data;

// read tag
if (!(data = consume(stream, sizeof(element->tag)))) {
return false;
}
element->tag = (*(const uint16_t*)data) << 16;
element->tag |= *(const uint16_t*)(data + sizeof(uint16_t));

// read value representation (type)
if (!(data = consume(stream, sizeof(element->vr)))) {
return false;
}
element->vr = *(const uint16_t*)data;

// get payload size
if (!(data = consume(stream, sizeof(uint16_t)))) {
return false;
}
element->size = *(const uint16_t*)data;
if (element->size == 0 &&
(element->vr == VR_OB || element->vr == VR_OW || element->vr == VR_SQ ||
element->vr == VR_UN || element->vr == VR_UT)) {
if (!(data = consume(stream, sizeof(uint32_t)))) {
return false;
}
element->size = *(const uint32_t*)data;
}

// get payload data
if (element->size == 0) {
element->data = NULL;
} else if (!(element->data = consume(stream, element->size))) {
return false;
}

return true;
}

/**
* Get image description from the stream.
* @param stream binary stream
* @param image output image description
* @return true if image description is valid
*/
static bool get_image(struct stream* stream, struct dicom_image* image)
{
struct element el;

memset(image, 0, sizeof(*image));

// collect info
while (next_element(stream, &el)) {
if (el.tag == TAG_SAMPLES_PER_PIXEL && el.vr == VR_US) {
image->spp = *(const uint16_t*)el.data;
} else if (el.tag == TAG_ROWS && el.vr == VR_US) {
image->height = *(const uint16_t*)el.data;
} else if (el.tag == TAG_COLUMNS && el.vr == VR_US) {
image->width = *(const uint16_t*)el.data;
} else if (el.tag == TAG_BIT_ALLOCATED && el.vr == VR_US) {
image->bpp = *(const uint16_t*)el.data;
} else if (el.tag == TAG_SMALL_PIXEL_VAL && el.vr == VR_SS) {
image->px_min = *(const int16_t*)el.data;
} else if (el.tag == TAG_BIG_PIXEL_VAL && el.vr == VR_SS) {
image->px_max = *(const int16_t*)el.data;
} else if (el.tag == TAG_PIXEL_DATA && el.vr == VR_OW) {
image->data = el.data;
image->data_sz = el.size;
}
}

// check
if (!image->data || image->height == 0 || image->width == 0 ||
image->data_sz != image->width * image->height * (image->bpp / 8)) {
return false;
}

return true;
}

// DICOM loader implementation
enum loader_status decode_dicom(struct image* ctx, const uint8_t* data,
size_t size)
{
struct dicom_image dicom;
struct stream stream;
struct pixmap* pm;
size_t total_pixels;
double pixel_coeff;

// check signature
if (size < DICOM_SIGNATURE_OFFSET + sizeof(signature) ||
memcmp(data + DICOM_SIGNATURE_OFFSET, signature, sizeof(signature))) {
return ldr_unsupported;
}

// get image description
stream.data = data;
stream.size = size;
stream.pos = DICOM_SIGNATURE_OFFSET + sizeof(signature);
if (!get_image(&stream, &dicom) || dicom.spp != 1 /* monochrome */ ||
dicom.bpp != 16 /* 2 bytes per pixel */) {
return ldr_fmterror;
}

// calculate min/max color value if not set yet
if (dicom.px_max == 0 || dicom.px_max <= dicom.px_min) {
dicom.px_min = INT16_MAX;
for (size_t i = 0; i < dicom.data_sz; i += sizeof(int16_t)) {
const int16_t color = *(const int16_t*)(dicom.data + i);
if (dicom.px_max < color) {
dicom.px_max = color;
}
if (dicom.px_min > color) {
dicom.px_min = color;
}
}
}

// Calculate coefficient for converting 16-bit color to 8-bit
if (dicom.px_max <= dicom.px_min) {
pixel_coeff = 1.0;
} else {
pixel_coeff = 256.0 / (dicom.px_max - dicom.px_min);
}

// allocate image buffer
pm = image_allocate_frame(ctx, dicom.width, dicom.height);
if (!pm) {
return ldr_fmterror;
}

// decode image
total_pixels = dicom.width * dicom.height;
for (size_t i = 0; i < total_pixels; ++i) {
int16_t color = *((const int16_t*)dicom.data + i);
color -= dicom.px_min;
color *= pixel_coeff;
pm->data[i] = ARGB(0xff, color, color, color);
}

image_set_format(ctx, "DICOM");

return ldr_success;
}
16 changes: 13 additions & 3 deletions src/loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,18 @@ const char* supported_formats = "bmp, pnm, farbfeld, tga"
#endif
#ifdef HAVE_LIBTIFF
", tiff"
#endif
#ifdef HAVE_LIBDICOM
", dicom"
#endif
;

// declaration of loaders
LOADER_DECLARE(bmp);
LOADER_DECLARE(dicom);
LOADER_DECLARE(farbfeld);
LOADER_DECLARE(pnm);
LOADER_DECLARE(qoi);
LOADER_DECLARE(farbfeld);
LOADER_DECLARE(tga);
#ifdef HAVE_LIBEXR
LOADER_DECLARE(exr);
Expand Down Expand Up @@ -99,6 +103,7 @@ LOADER_DECLARE(tiff);
LOADER_DECLARE(webp);
#endif

// clang-format off
// list of available decoders
static const image_decoder decoders[] = {
#ifdef HAVE_LIBJPEG
Expand All @@ -110,7 +115,9 @@ static const image_decoder decoders[] = {
#ifdef HAVE_LIBGIF
&LOADER_FUNCTION(gif),
#endif
&LOADER_FUNCTION(bmp), &LOADER_FUNCTION(pnm),
&LOADER_FUNCTION(bmp),
&LOADER_FUNCTION(pnm),
&LOADER_FUNCTION(dicom),
#ifdef HAVE_LIBWEBP
&LOADER_FUNCTION(webp),
#endif
Expand All @@ -132,8 +139,11 @@ static const image_decoder decoders[] = {
#ifdef HAVE_LIBTIFF
&LOADER_FUNCTION(tiff),
#endif
&LOADER_FUNCTION(qoi), &LOADER_FUNCTION(farbfeld), &LOADER_FUNCTION(tga)
&LOADER_FUNCTION(qoi),
&LOADER_FUNCTION(farbfeld),
&LOADER_FUNCTION(tga) // should be the last one
};
// clang-format on

/** Background thread loader queue. */
struct loader_queue {
Expand Down
Binary file added test/data/image.dcm
Binary file not shown.
3 changes: 2 additions & 1 deletion test/loader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ TEST_F(Loader, External)
}

TEST_LOADER(bmp);
TEST_LOADER(dcm);
TEST_LOADER(ff);
TEST_LOADER(pnm);
TEST_LOADER(qoi);
TEST_LOADER(tga);
TEST_LOADER(ff);
#ifdef HAVE_LIBEXR
// TEST_LOADER(exr);
#endif
Expand Down
1 change: 1 addition & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sources = [
'../src/memdata.c',
'../src/pixmap.c',
'../src/formats/bmp.c',
'../src/formats/dicom.c',
'../src/formats/farbfeld.c',
'../src/formats/pnm.c',
'../src/formats/qoi.c',
Expand Down

0 comments on commit 0dae133

Please sign in to comment.