4

I am working with a dataset that contains .mha files. I want to convert these files to either png/tiff for some work. I am using the Medpy library for converting.

image_data, image_header = load('image_path/c0001.mha')

from medpy.io import  save
save(image_data, 'image_save_path/new_image.png', image_header)

I can actually convert the image into png/tiff format, but the converted image turns dark after the conversion. I am attaching the screenshot below. How can I convert the images successfully?

enter image description here enter image description here

Walter Tross
  • 12,237
  • 2
  • 40
  • 64
Tareq Mahmud
  • 85
  • 1
  • 7
  • If you could make available a sample `.mha` image online, it would be best. Of course, it would have to trigger the same problem – Walter Tross Jan 27 '22 at 19:44
  • 1
    Hi, here is the file. https://drive.google.com/file/d/1agp_ncmfFwwMAEQiAoxVT1B07Ac-iI_s/view?usp=sharing – Tareq Mahmud Jan 27 '22 at 20:04

2 Answers2

4

Your data is clearly limited to 12 bits (white is 2**12-1, i.e., 4095), while a PNG image in this context is 16 bits (white is 2**16-1, i.e., 65535). For this reason your PNG image is so dark that it appears almost black (but if you look closely it isn't).

The most precise transformation you can apply is the following:

import numpy as np
from medpy.io import load, save

def convert_to_uint16(data, source_max):
    target_max = 65535  # 2 ** 16 - 1
    # build a linear lookup table (LUT) indexed from 0 to source_max
    source_range = np.arange(source_max + 1)
    lut = np.round(source_range * target_max / source_max).astype(np.uint16)
    # apply it
    return lut[data]

image_data, image_header = load('c0001.mha')
new_image_data = convert_to_uint16(image_data, 4095)  # 2 ** 12 - 1
save(new_image_data, 'new_image.png', image_header)

Output: sample image converted to PNG

N.B.: new_image_data = image_data * 16 corresponds to replacing 65535 with 65520 (4095 * 16) in convert_to_uint16

Walter Tross
  • 12,237
  • 2
  • 40
  • 64
3

You may apply "contrast stretching".

The dynamic range of image_data is about [0, 4095] - the minimum value is about 0, and the maximum value is about 4095 (2^12-1).
You are saving the image as 16 bits PNG.
When you display the PNG file, the viewer, assumes the maximum value is 2^16-1 (the dynamic range of 16 bits is [0, 65535]).
The viewer assumes 0 is black, 2^16-1 is white, and values in between scales linearly.

In your case the white pixels value is about 4095, so it translated to be a very dark gray in the [0, 65535] range.

The simplest solution is to multiply image_data by 16:

from medpy.io import load, save

image_data, image_header = load('image_path/c0001.mha')

save(image_data*16, 'image_save_path/new_image.png', image_header)

A more complicated solution is applying linear "contrast stretching".
We may transform the lower 1% of all pixel to 0, the upper 1% of the pixels to 2^16-1, and scale the pixels in between linearly.

import numpy as np
from medpy.io import load, save

image_data, image_header = load('image_path/c0001.mha')

tmp = image_data.copy()
tmp[tmp == 0] = np.median(tmp)  # Ignore zero values by replacing them with median value (there are a lot of zeros in the margins).
tmp = tmp.astype(np.float32) # Convert to float32

# Get the value of lower and upper 1% of all pixels
lo_val, up_val = np.percentile(tmp, (1, 99))  # (for current sample: lo_val = 796, up_val = 3607)

# Linear stretching: Lower 1% goes to 0, upper 1% goes to 2^16-1, other values are scaled linearly
# Clipt to range [0, 2^16-1], round and convert to uint16
# https://stackoverflow.com/questions/49656244/fast-imadjust-in-opencv-and-python
img = np.round(((tmp - lo_val)*(65535/(up_val - lo_val))).clip(0, 65535)).astype(np.uint16)  # (for current sample: subtract 796 and scale by 23.31)

img[image_data == 0] = 0  # Restore the original zeros.

save(img, 'image_save_path/new_image.png', image_header)

The above method enhance the contrast, but looses some of the original information.
In case you want higher contrast, you may use non-linear methods, improving the visibility, but loosing some "integrity".


Here is the "linear stretching" result (downscaled):
enter image description here

Rotem
  • 30,366
  • 4
  • 32
  • 65
  • Thank you. I couldn't figure out the limit issue. – Tareq Mahmud Jan 28 '22 at 09:48
  • Take a look at this [post](https://stackoverflow.com/questions/49656244/fast-imadjust-in-opencv-and-python). Take a look at MATLAB [imadjust](https://www.mathworks.com/help/images/ref/imadjust.html) and MATLAB [stretchlim](https://www.mathworks.com/help/images/ref/stretchlim.html) methods. And read about [Percentile](https://en.wikipedia.org/wiki/Percentile). Basically it's just a linear transformation (with scale and offset). For computing the scale and offset adaptively, we are using the lower and upper percentiles. The exact math exceeds the scope of your question. – Rotem Jan 28 '22 at 11:44