Search code examples
python-3.ximageimage-processingpython-imaging-librarypython-imageio

Image not showing properly when reading the image using PIL instead of imagio library


I have this code that converts an image from RGB space to HSI space:

import numpy as np
import imageio
from matplotlib import pyplot as plt


def RGB_TO_HSI(img):
    with np.errstate(divide='ignore', invalid='ignore'):

        rgb = np.float32(img) / 255

        # Separate color channels
        red = rgb[:, :, 0]
        green = rgb[:, :, 1]
        blue = rgb[:, :, 2]

        # Calculate Intensity
        def calc_intensity(red, green, blue):
            intensity = (red + green + blue + 0.001) / 3
            return intensity

        # Calculate Saturation
        def calc_saturation(red, green, blue):
            minimum = np.minimum(np.minimum(red, green), blue)
            saturation = 1 - (minimum / calc_intensity(red, green, blue))
            return saturation

        # Calculate Hue
        def calc_hue(red, green, blue):
            hue = np.copy(red)  # Basically have our hue = red for now; we only need its size/dimensions

            for i in range(0, blue.shape[0]):
                for j in range(0, blue.shape[1]):

                    if blue[i][j] <= green[i][j]:
                        hue[i][j] = np.arccos(0.5 * ((red[i][j] - green[i][j]) + (red[i][j] - blue[i][j])) / (np.sqrt((red[i][j] - green[i][j]) ** 2 + (red[i][j] - blue[i][j]) * (green[i][j] - blue[i][j]))))
                    else:
                        hue[i][j] = 2 * np.pi - np.arccos(0.5 * ((red[i][j] - green[i][j]) + (red[i][j] - blue[i][j])) / (np.sqrt((red[i][j] - green[i][j]) ** 2 + (red[i][j] - blue[i][j]) * (green[i][j] - blue[i][j]))))
            return hue

    # Merge channels into picture and return image
    hsi = np.zeros(img.shape)  # instead of having 3 channels, one for each color (RGB), here we have a channel; for "hue", another for "saturation" and another for "intensity"
    hsi[:, :, 0], hsi[:, :, 1], hsi[:, :, 2] = calc_hue(red, green, blue), calc_saturation(red, green, blue), calc_intensity(red, green, blue)
    return hsi

cat = imageio.imread("Path...")
hsi_cat = RGB_TO_HSI(cat)
plt.imshow(hsi_cat)
plt.show()

However, adjusting the line in the code to: hsi = np.zeros(rgb.shape, dtype=np.uint8), then loading the image using PIL, and passing it into the function as an argument:

cat_p = Image.open("Path...")  # Using PIL now
his_cat_p = RGB_TO_HSI(cat_p)  # Passing the JPEG image into the function
his_cat_p = Image.fromarray(his_cat_p)  # Converting the result back from an array (return hsi), into JPEG format
his_cat_p.show()  # Black image appears

This results is a black image, and I'm not quite sure why!


Solution

  • The function Image.fromarray only support limited modes with typical ranges. If you just want to render the data correctly as an image, maybe it is better to transform from HSI back to RGB values, then output it. Something like,

    cat_hsi = RGB_TO_HSI(cat)
    result = modify(cat_hsi)  # I guess you want to operate on hsi instead of rgb
    result_rgb = HSI_TO_RGB(result)
    img = Image.fromarray(result_rgb, mode='RGB')
    img.show()
    

    Additionally I made some modifications to the code so that it runs on my machine.

    • Making sure the RGB array with shape (X, Y, 3) is passed to the function
    • Explicitly handling the possible zero-division case in the calc_hue function.
    import numpy as np
    from numba import njit  # code gets significantly faster
    from matplotlib import pyplot as plt
    from PIL import Image
    
    
    def RGB_TO_HSI(img):
        rgb = np.float32(img) / 255
    
        red = rgb[:, :, 0]
        green = rgb[:, :, 1]
        blue = rgb[:, :, 2]
    
        def calc_intensity(red, green, blue):
            intensity = (red + green + blue + 0.001) / 3
            return intensity
    
        def calc_saturation(red, green, blue):
            minimum = np.minimum(np.minimum(red, green), blue)
            saturation = 1 - (minimum / calc_intensity(red, green, blue))
            return saturation
    
        @njit  # use numba to accelerate
        def calc_hue(red, green, blue):
            hue = np.copy(red)
            for i in range(0, blue.shape[0]):
                for j in range(0, blue.shape[1]):
                    denominator = np.sqrt(
                        (red[i][j] - green[i][j]) ** 2 + (red[i][j] - blue[i][j]) * (green[i][j] - blue[i][j])
                    )
                    if abs(denominator) < 1e-10:  # exliciply handle the possible zero-division cases
                        hue[i][j] = np.nan
                    else:
                        if blue[i][j] <= green[i][j]:
                            hue[i][j] = np.arccos(0.5 * ((red[i][j] - green[i][j]) + (red[i][j] - blue[i][j])) / denominator)
                        else:
                            hue[i][j] = 2 * np.pi - np.arccos(0.5 * ((red[i][j] - green[i][j]) + (red[i][j] - blue[i][j])) / denominator)
            return hue
    
        # Merge channels into picture and return image
        hsi = np.zeros(img.shape)  # instead of having 3 channels, one for each color (RGB), here we have a channel; for "hue", another for "saturation" and another for "intensity"
        hsi[:, :, 0], hsi[:, :, 1], hsi[:, :, 2] = calc_hue(red, green, blue), calc_saturation(red, green, blue), calc_intensity(red, green, blue)
        return hsi
    
    cat = np.array(Image.open("test.png"))
    hsi_cat = RGB_TO_HSI(cat[:, :, :3])  # sometims images are in RGBA format
    plt.imshow(hsi_cat)
    plt.show()
    

    (numba is magical isn't it?)