0

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!

Omar AlSuwaidi
  • 1,187
  • 2
  • 6
  • 26

1 Answers1

1

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?)

Yang Yushi
  • 725
  • 4
  • 20
  • Wow, numba is indeed magical, it made the code much faster! However, the case where the black image would appear was when opening the image using PIL instead of imageio; the code for that is in the last part of the question. – Omar AlSuwaidi Jan 27 '21 at 03:53
  • 1
    Hi I updated the answer using `PIL`. One possible issue is that when dealing with png files the data are in the format of RGBA instead of RGB so I have to ensure I only give RGB channels to the function. – Yang Yushi Jan 27 '21 at 08:53
  • I think the issue arrises when converting the hsi_cat from an array to an image using "Image.fromarray()". Even when ensuring RGB channels only, if you replace "plt.imshow(hsi_cat)" with "hsi_cat = Image.fromarray(hsi_cat)" then "hsi_cat.show()" you get the black image. – Omar AlSuwaidi Jan 27 '21 at 10:18
  • 1
    I don't think [Image.fromarray](https://pillow.readthedocs.io/en/stable/reference/Image.html?#PIL.Image.fromarray) support the data format of hue-satuation-intensity. I guess to display the image you will have to convert it to to a [supported data format](https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes). – Yang Yushi Jan 27 '21 at 10:36
  • 1
    If I want to show the image I would just transform everything back to RGB honestly – Yang Yushi Jan 27 '21 at 10:37
  • Also I did not check your equation for the conversion from RGB to HSI. It is a bit strange that you would get an zero division error. – Yang Yushi Jan 27 '21 at 10:49