singrium
singrium

Reputation: 3016

Same math and numpy functions don't give the same result when applied to pixels values

I want to calculate the Perceived brightness ,based on the formula from this link, of this image:

enter image description here

The idea is to loop over each pixel and calculate its Perceived brightness according to this formula:

Pb = sqrt(0.241 R² + 0.691 G² + 0.068 B²)

then sum all the values and calculate the mean.
Here is the code I wrote:

import cv2
from math import sqrt

img = cv2.imread('e.png')
H, W = img.shape[:2]
pr = 0.241
pg = 0.691
pb = 0.068

p = []
for h in range(0, H):
    for w in range(0, W):
        p.append(sqrt(pr * pow(img[h][w][2], 2) + pg * pow(img[h][w][1], 2) + pb * pow(img[h][w][0], 2)))
arr = np.reshape(p, (H, W))
cv2.imwrite('loop_img.jpg', arr)
print(np.mean(arr))

The image I got at the end is this:

enter image description here
And the mean is 82.04557421656007

However when I repeated the same process using numpy (to avoid looping over each pixel), I got different values!
Here is the code I used:

import cv2
import numpy as np

img = cv2.imread('e.png')
b, g, r = cv2.split(img)
pr = 0.241
pg = 0.691
pb = 0.068
P = np.sqrt(pr * pow(r, 2) + pg * pow(g, 2) + pb * pow(b, 2))
cv2.imwrite('np_img.jpg', P)
print(np.mean(P))

The image I got is this:

enter image description here
And the mean is 1.6438602314083277

The most weird is that when I applied the same methods on a random numpy array, I got similar results!

import numpy as np
import cv2

from math import sqrt

pr = 0.241
pg = 0.691
pb = 0.068

arr = np.array([[[255, 127,   0],
                 [255, 127,   0]],

                [[255, 133,   0],
                [255, 133,   0]],

                [[255, 138,   0],
                [255, 138,   0]]])

b, g, r = cv2.split(arr)

p = []
for h in range(0, 3):
    for w in range(0, 2):
        print(arr[h][w])
        p.append(sqrt(pr * pow(arr[h][w][2], 2) + pg * pow(arr[h][w][1], 2) + pb * pow(arr[h][w][0], 2)))
arr_p = np.reshape(p, (3, 2))
print('arr_p:', arr_p)
np_p = np.sqrt(pr * pow(r, 2) + pg * pow(g, 2) + pb * pow(b, 2))
print('np_ap:', np_p)
print('loop_mean:', np.mean(arr_p))
print('numpy_mean:', np.mean(np_p))

The results I got:

arr_p: [[124.7671391  124.7671391 ]
 [129.01472397 129.01472397]
 [132.59375551 132.59375551]]  

np_ap: [[124.7671391  124.7671391 ]
 [129.01472397 129.01472397]
 [132.59375551 132.59375551]]  

loop_mean: 128.79187285939827  

numpy_mean: 128.79187285939827

Is there any explanation why I got different results with the image and similar results with the second array? (could it be related to the array elements type?)
N.B: I use

python==3.6
numpy==1.16.1  
opencv-contrib-python==4.0.0.21  
opencv-python==4.0.0.21  

Upvotes: 3

Views: 252

Answers (2)

sgarizvi
sgarizvi

Reputation: 16796

The problem is due to the difference of data type conversion rules between numpy array and raw data types.

In the case of numpy array, the calculation is being done as follows:

P = np.sqrt(pr * pow(r, 2) + pg * pow(g, 2) + pb * pow(b, 2))

The culprit operation here is pow. Since the default data type of image read using cv2.imread is np.uint8 so consequently, r, g and b also have the same type. Now, when pow function is applied on the numpy array, the resultant array tends to have the same integer type. The values in the result are truncated to the range of uint8 type thus causing invalid results. Since the results are truncated, the mean value becomes very small as being observed.

Possible solutions:

1. Convert input image to floating point type:

img = cv2.imread('e.png')
img = img.astype(np.float)

2. Use floating point operands in pow:

P = np.sqrt(pr * pow(r, 2.0) + pg * pow(g, 2.0) + pb * pow(b, 2.0))

Why are the results correct in the loop case?

p.append(sqrt(pr * pow(img[h][w][2], 2) + pg * pow(img[h][w][1], 2) + pb * pow(img[h][w][0], 2)))

Apparently, applying pow on a single integer instead of numpy array results in a value of larger integer type (int64) thus avoiding the issue of truncation.

Upvotes: 4

api55
api55

Reputation: 11420

The problem is the pow function to a np.uint8 array. First, lets have a simple example:

>> a = np.arange(20, dtype=np.uint8).reshape(4,5)

which gives:

array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]], dtype=uint8)

It is important to test with np.uint8 which is the type of a loaded image. Then we do pow or np.power (they behave exactly the same) and the result is the following:

>> np.power(a,2)
array([[  0,   1,   4,   9,  16],
       [ 25,  36,  49,  64,  81],
       [100, 121, 144, 169, 196],
       [225,   0,  33,  68, 105]], dtype=uint8)

>> pow(a,2)
array([[  0,   1,   4,   9,  16],
       [ 25,  36,  49,  64,  81],
       [100, 121, 144, 169, 196],
       [225,   0,  33,  68, 105]], dtype=uint8)

As you can see, the power function did not change the type... and this leads to overflow...

You have 2 options to solve it:

Cast the type and then cast it back like

b = np.float32(b) #same for g and r or to the whole image
# or this
b, g, r = cv2.split(np.float32(img))

and then before saving use np.uint8(), opencv saving functions usually work only with uint8... maybe the newer versions doesn't.

The other thing is to use np.float_power which will return float32 type and the correct numbers.

Upvotes: 3

Related Questions