How to scale numpy array (image) values to a certain range quickly, without changing the image in any way?

Question:

I have a numpy array H_arr representing the following image:

H_arr image
(IMG 1)

and I wish to convert its values in the range [0,1], let’s call this new array as New_arr, in such a way that the original image remains intact. What I mean is that the exact same image (IMG 1) should be displayed when I use plt.imshow(New_arr).

The data type of H_arr is float32, with H_arr.min() giving -24.198463 and H_arr.max() giving 26.153196. H_arr.shape gives (960, 1280, 3).

I had earlier thought that I would use the following formula to convert it to the 0-1 range:

newvalue= (new_max-new_min)/(max-min)*(value-max)+new_max

and implement it as:

New = np.zeros((H_arr.shape[0],H_arr.shape[1],H_arr.shape[2]),dtype = float)
for i in range(H_arr.shape[0]):
  for j in range(H_arr.shape[1]):
    for k in range(H_arr.shape[2]):
      New[i][j][k]= (1-0)/(H_arr.max()-H_arr.min())*(H_arr[i][j][k]-H_arr.max())+1

But this is computationally quite expensive. Any input on how I should go about converting the original array is appreciated.

Edit: After incorporating the answers below, I can do it quite quickly within the [0,1] range, but the image drastically changes to

Scaled Image

How do I make sure that my image remains the same as before?

Asked By: Key76

||

Answers:

You can apply your formular to the entire array at once. No loop required, just remove the indices:

New= (1-0)/(H_arr.max()-H_arr.min())*(H_arr-H_arr.max())+1
Answered By: Nyps

Here is the function to scale your array into whatever interval you want:

def minmax_scaler(arr, *, vmin=0, vmax=1):
    arr_min, arr_max = arr.min(), arr.max()
    return ((arr - arr_min) / (arr_max - arr_min)) * (vmax - vmin) + vmin

Here is how you can use it:

import PIL
import numpy as np
import urllib

def read_image_from_url(ur):
    return np.array(PIL.Image.open(urllib.request.urlopen(url)))

# Original
url = "https://i.stack.imgur.com/75y6Q.png"
arr = read_image_from_url(url).astype(np.float32)

print(f"{arr.min() = }")
print(f"{arr.max() = }")

# Scaling in [0, 1]
arr_scaled_01 = minmax_scaler(arr, vmin=0, vmax=1)

print(f"{arr_scaled_01.min() = }")
print(f"{arr_scaled_01.max() = }")

# Scaling into new [vmin, vmax]
vmin_new, vmax_new = -34, 34
arr_scaled = minmax_scaler(arr, vmin=vmin_new, vmax=vmax_new)

print(f"{arr_scaled.min() = }")
print(f"{arr_scaled.max() = }")
arr.min() = 0.0
arr.max() = 255.0
arr_scaled_01.min() = 0.0
arr_scaled_01.max() = 1.0
arr_scaled.min() = -34.0
arr_scaled.max() = 34.0

Normally, when using imshow you can set vmin and vmax, so whatever the range of the values of your array, the plot will be the same.
However they will be ignored if the array is an RGB image, in which case you have to scale the array yourself in [0, 1] for floats or [0, 255] for ints.

import matplotlib.pyplot as plt

def imshow_minmax(arr, ax):
    # vmin, vmax and cmap will be ignored if RGBA image
    return ax.imshow(arr, vmin=arr.min(), vmax=arr.max(), cmap="gray")

fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(8, 4))

axes[0, 0].set_ylabel("One band")
imshow_minmax(arr[..., 0], ax=axes[0, 0])
imshow_minmax(arr_scaled_01[..., 0], ax=axes[0, 1])
imshow_minmax(arr_scaled[..., 0], ax=axes[0, 2])

axes[1, 0].set_ylabel("RGB")
imshow_minmax(arr, ax=axes[1, 0])
imshow_minmax(arr_scaled_01, ax=axes[1, 1])
imshow_minmax(arr_scaled, ax=axes[1, 2])

axes[1, 0].set_xlabel(f"Original in {[arr.min(), arr.max()]}")
axes[1, 1].set_xlabel("Scaled in [0, 1]")
axes[1, 2].set_xlabel(f"Scaled in {[vmin_new, vmax_new]}")

fig.tight_layout()
plt.show()

enter image description here

Answered By: paime