1

def normalize_brightness(img: Image) -> Image: """

 Normalize the brightness of the given Image img by:
  1. computing the average brightness of the picture: - this can be done by calculating the average brightness of each pixel in img (the average brightness of each pixel is the sum of the values of red, blue and green of the pixel, divided by 3 as a float division) - the average brightness of the picture is then the sum of all the pixel averages, divided by the product of the width and height of img

  2. find the factor, let's call it x, which we can multiply the average brightness by to get the value of 128.

  3. multiply the colors in each pixel by this factor x """

    img_width, img_height = img.size
    pixels = img.load()  # create the pixel map
    h = 0
    for i in range(img_width):
        for j in range(img_height):
            r, g, b = pixels[i, j]
            avg = sum(pixels[i, j]) / 3
            h += avg
    total_avg = int(h / (img_width * img_height))
    x = 128 // total_avg
    r, g, b = pixels[i, j]
    pixels[i, j] = (r * x, g * x, b * x)
    return img
    

    I am a little lost as to what I am doing wrong can someone help?

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
Jack Jones
  • 37
  • 2
  • 8

3 Answers3

2

You really should avoid for loops when image processing with Python whenever possible because it is seriously slow, verbose, harder to read and more likely to contain errors. Try to use vectorised Numpy functions, or OpenCV or PIL built-in functions.

#!/usr/bin/env python3

from PIL import Image
import numpy as np

def normalize(im):
   """Normalise brightness of image"""

   # Convert to Numpy array
   na = np.array(im, dtype=np.float32)

   # Calculate average brightness
   avg = na.mean()

   # Calculate factor x
   x = 128 / avg

   # Scale whole array as float since likely fractional
   na *= x

   # Convert back to PIL Image and return
   return Image.fromarray(na.astype(np.uint8))

# Load image and normalize
im = Image.open('start.png').convert('RGB')
result = normalize(im)
result.save('result.png')

This code runs in around 800 microseconds on my machine whereas any version with a for loop requires around 70x longer.

Input image:

enter image description here

Result:

enter image description here

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
1

Your calculation code to get the factor seems okay, processing every pixel to get the average of sum of averages.

However, your modification code to adjust the brightness is not done within a similar loop so it will operate on one pixel, and I'm not even sure that pixel is even within the image. You should do that within a loop as well:

for i in range(img_width):
    for j in range(img_height):
        (r, g, b) = pixels[i, j]
        pixels[i, j] = (r * x, g * x, b * x)

This should replace the third-last and second-last lines of what you have at the moment (between x = ... and return ...). So what you would end up with is:

img_width, img_height = img.size
pixels = img.load()  # create the pixel map
h = 0
for i in range(img_width):
    for j in range(img_height):
        r, g, b = pixels[i, j]
        avg = sum(pixels[i, j]) / 3
        h += avg
total_avg = int(h / (img_width * img_height))
x = 128 // total_avg

# == New stuff below
for i in range(img_width):
    for j in range(img_height):
        (r, g, b) = pixels[i, j]
        pixels[i, j] = (r * x, g * x, b * x)
# == New stuff above

return img

A few other things to look in to:

First, I'm not sure if returning img is the right thing to do here, unless pixels is a reference to (not copy of) the pixels in the image. You may want to check up on that as well.

Further, it may be possible that the value for [rgb] * x gives you something more than 255 for certain input data sets. If that's the case, you probably want to clamp them into the range 0..255 to ensure this doesn't happen. Something like (replacing the "new stuff" in the code above):

for i in range(img_width):
    for j in range(img_height):
        # Get original pixel.

        (r, g, b) = pixels[i, j]

        # Scale with upper limit.

        r = min(255, r * x)
        g = min(255, g * x)
        b = min(255, b * x)

        # Replace pixel with scaled one.

        pixels[i, j] = (r, g, b)
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • yes because for some reason when I put the modification inside the nested for loop I get a zero division error – Jack Jones Mar 30 '20 at 07:22
  • @Jack, you can't put it inside the *current* loop set because you'll divide by `total_avg` which hasn't been set yet. You need *another* loop set. – paxdiablo Mar 30 '20 at 07:24
  • so basically when I do my modification I am only modifing (total_avg) the average of ONE pixel and not the total average of all the pixels? And if so how would the for loop be to fix this? – Jack Jones Mar 30 '20 at 07:30
  • @Jack, the `for` loop and whree to put it has been added to the answer. – paxdiablo Mar 30 '20 at 07:33
  • where? I still just see the version before it – Jack Jones Mar 30 '20 at 07:36
  • okay I have changed my code in the question to the format in which you said but I still get the division error and therefore need the for loop to fix. – Jack Jones Mar 30 '20 at 07:53
  • @Jack, if you're getting `/0` errors, you need to isolate where the zero is coming from. That has nothing to do with your mis-processing of pixels when adjusting the image. The most likely case is that `total_avg` is set to zero for some reason. – paxdiablo Mar 30 '20 at 07:57
  • yes that is the case once I isolated its the total_avg which is 0 for some reason. How can I fix this? I have also added my current code again in the question. – Jack Jones Mar 31 '20 at 05:01
  • @Jack, you should ask yourself before editing any question: is this going to invalidate any answers already posted? If so, you should either constrain the edits to adding *new* information (leaving everything else there) or just ask a new question with a link back to this one. Editing questions in such a way as they invalidate answers destroys the whole intent of SO as a Q&A site. In any case, your edits were still wrong in that you were adjusting the brightness within the first loop. You *cannot* do this at that point since the end of the first loop is the earliest time you know ... – paxdiablo Mar 31 '20 at 06:00
  • ... what factor to apply. It has to be done in two *distinct* loops, as per this answer. My suggestion is to use that second code block in this answer, it'll be the correct basis for what you're trying to do. – paxdiablo Mar 31 '20 at 06:01
  • that code block doesn't seem to work for some reason. I also added the min function to it which is min(255, r*x)...... but when I return it nothing happens and the image is just the same as before – Jack Jones Mar 31 '20 at 06:19
  • @Jack: okay, did you *read* the paragraph beginning "First, I'm not sure if returning img is the right thing to do here"? You may want to check on that since the pixels may be separate from the original image. – paxdiablo Mar 31 '20 at 06:23
  • Secondly, it should be fairly easy to put in some `print` statements to see what the brightness-changing factor is and how it affects each pixel. – paxdiablo Mar 31 '20 at 06:28
0

At first, thanks to paxdiablo for sharing his answer.

I would just like to improve on the answer.

The calculation of the average can be optimized using list comprehension like:

x = 128 // (sum([sum(pixels[i, j]) / 3 for i in range(img_width) for j in range(img_height)]) / (img_width * img_height))

So my complete answer will be:

Normalize the brightness of the given Image

img_width, img_height = img.size
pixels = img.load()  # create the pixel map

x = 128 // (sum([sum(pixels[i, j]) / 3 for i in range(img_width) for j in range(img_height)]) / (img_width * img_height))

for i in range(img_width):
    for j in range(img_height):
        r, g, b = pixels[i, j]
        pixels[i, j] = [min(255, r * x), min(255, g * x), min(255, b * x)]

return img

Rishab P
  • 1,593
  • 7
  • 18