4

H as my name suggests I am new to code, am working with Python and would really appreciate some help with using the Python Imaging Library to complete a task.

I have 2 bitmap images. The first image is of a color painting and the second contains just black and white pixels... I would like to develop a function that accepts two images as parameters to create a new image that is essentially the first image (the painting), with only the black pixels from the second image, overwritten on top of it. The second image is smaller than the first and the overwritten pixels need to be positioned in the center of the new image.

I have installed the PIL and have been able to use the following code to successfully display the first image using the following code:

import PIL
from PIL import Image

painting = Image.open("painting.bmp")
painting.show()

I have ruled out using .blend and .composite functions or the .multiply function from the ImageChops module, as the images are not the same size.

I believe I'll need to use either .getpixel / .putpixel or .getdata / .putdata to find the black pixels which are identified by tuple (0,0,0). Also PIL's crop and paste functions I'm thinking should help work out the 'region' from the painting to overwrite with the black pixels, and could help center the black pixels over the painting?

So i'm looking at something like this maybe...

def overwrite_black(image1, image2): 

image_1 = Image.open('painting.bmp') 
image_2 = Image.open('black_white.bmp') 
pixels = list(image_2.getdata())

for y in xrange(image_2.size[1]):
    for x in xrange(image_2.size[0]):
        if pixels ==(o,o,o):
          image_2.putdata(pixels((x,y),(0,0,0)))

image_2.save('painted.bmp')

Again, i'm new so go easy on me and any help would be greatly appreciated.

Cheers.

NewBee
  • 43
  • 1
  • 4

2 Answers2

4

So I assume you just want to multiply two images on top of each other, the catch being that they're different sizes? Forgive me if I misunderstood, but here is some code that puts the black pixels of one image onto another, even if the black and white image is smaller than the other one.

Sourse Images (painting.bmp and mask.bmp)

paintingmask

Output:

result

"...a new image that is essentially the first image (the painting), with only the black pixels from the second image, overwritten on top of it." — is this what you want?


Essentially, the code just makes the smaller image as big as the first one, by putting it in the center of a blank image the same size as the first one.

import PIL
from PIL import Image
from PIL import ImageChops # used for multiplying images

# open images
painting = Image.open("painting.bmp")
mask     = Image.open("mask.bmp")


def black_onto(img1, img2):  
    # create blank white canvas to put img2 onto
    resized = Image.new("RGB", img1.size, "white")

    # define where to paste mask onto canvas
    img1_w, img1_h = img1.size
    img2_w, img2_h = img2.size
    box = (img1_w/2-img2_w/2, img1_h/2-img2_h/2, img1_w/2-img2_w/2+img2_w, img1_h/2-img2_h/2+img2_h)

    # multiply new mask onto image
    resized.paste(img2, box)
    return ImageChops.multiply(img1, resized)


out = black_onto(painting, mask)
out.show() # this gives the output image shown above

Explanation for:(img1_w/2-img2_w/2, img1_h/2-img2_h/2, img1_w/2-img2_w/2+img2_w, img1_h/2-img2_h/2+img2_h)

Ok, so this is the ugly bit, but it's really quite simple: box defines the region on the canvas that we want to place the mask onto, which is the center. box is a 4-value tuple that defines the x and y of the top-left and bottom-right corners of the region, like this: (x1, y1, x2, y2). Not the x, y and width, height, which would be more handy. Anyway, to define the region so that the image is centered, that code is what we get.

It goes like this: The x-value of the top-left corner is equal to half the width of the large image minus half the width of the mask image. (I find pen and paper helpful here.) The same goes for the y-value of the top-left corner. That's the first two values.

Now, if the tuple accepted (x, y, width, height), then the second two values would just be the dimensions of the mask image. But they're not, they're more x and y positions. So we have to calculate them manually, by taking same code as first two values (top-left position) and adding the width and height of the image to it, which we know from some variables (img2_w and img2_h). Hence the second two values of the tuple are the same as the first and second, but with the width or height of the mask (img2) added on.

Well, I hope that makes enough sense, and good luck with you're projects!

Jollywatt
  • 1,382
  • 2
  • 12
  • 31
  • That's pretty cool Joseph...works great! I had tried the .multiply function but didn't think to resize the second image so that it didn't crop the final image. I don't really understand what you did there with creating the box variable...it's a 4 number tuple, yeah?, but what is happening here: (img1_w/2-img2_w/2, etc) ... Why is it divided by 2 etc? – NewBee Oct 11 '13 at 12:01
  • Thanks! It was a pleasure to hack together! So, the 4-valued tuple thing basically defines the x and y values of the _top-left_ and _bottom-right_ corners of a "region". (Not x, y and width, height) The `box` is the region on the blank image that we want to paste the mask image onto. I'll put an proper explanation in my answer... – Jollywatt Oct 11 '13 at 21:42
  • Thanks once again Joseph, this detailed explanation cleared this up for me...you're right, it's quite simple really. I can see now that it's dived by two because the remaining difference of the width and height of the two images needs to be split equally, either side of the smaller image to center it. Also, I now understand that the 4 number tuple is the x and y coords for the top left and bottom right corners of the region. – NewBee Oct 12 '13 at 13:16
  • 1
    Despite this very effecient and clever solution, it was suggested that for the task we extract the pixels from the imageinto a list using: pixels = list(image_2.getdata()) ...make the changes to the list as needed and then to write the black pixels onto the second image using: image.putdata(pixels) ...and you can probably tell from the "maybe something like this code" that I posted above (which does not work), that I'm not sure how to do this...any pointers with regards to this? – NewBee Oct 12 '13 at 13:30
  • Amazing! For anyone copying this and getting an error on line 20 with float objects that cannot be interpreted as integers, as obvious as it may seem, a quick solution is to convert each of the four values to integers, as in ´´´box = (int(img1_w/2-img2_w/2), int(img1_h/2-img2_h/2), int(img1_w/2-img2_w/2+img2_w), int(img1_h/2-img2_h/2+img2_h))´´´ instead of ´´´box = (img1_w/2-img2_w/2, img1_h/2-img2_h/2, img1_w/2-img2_w/2+img2_w, img1_h/2-img2_h/2+img2_h)´´´ – WildWilyWilly Jun 21 '23 at 05:01
2

Ok, here's an alternate solution that's far more general and extendable. There's two parts to it; firstly there's resizing (but not scaling) the smaller image to the dimensions of the larger one, and then there's taking that image and making it into an image that only consists of black or transparent pixels, which can be simply pasted onto the larger one, which then leaves us with what you want.

Resizing:

1: painting 2: original mask 3: resized

  1. Reference image
  2. Input mask
  3. Resized mask

That can be done with this center_resize() function:

def center_resize(smaller, (width, height)):
    resized = Image.new("RGB", (width, height), "white")
    smaller_w, smaller_h = smaller.size
    box = (width/2-smaller_w/2,
           height/2-smaller_h/2,
           width/2-smaller_w/2+smaller_w,
           height/2-smaller_h/2+smaller_h)
    resized.paste(smaller, box)
    return resized

Filtering:

This function is made to iterate through all the pixels in an image and set them to a specified colour, if they are a specified colour. (Otherwise they get set to a third specified color.) In this case we want to leave all black pixels black, but set all other to a fully transparent black. To do that you would input this:

pixel_filter(image, condition=(0, 0, 0), true_colour=(0, 0, 0, 255), false_colour=(0, 0, 0, 0))

... and it would return a filtered image.

def pixel_filter(image, condition, true_colour, false_colour):
    filtered = Image.new("RGBA", image.size)
    pixels = list(image.getdata())
    for index, colour in enumerate(pixels):
        if colour == condition:
            filtered.putpixel((index%image.size[1],index/image.size[1]), true_colour)
        else:
            filtered.putpixel((index%image.size[1],index/image.size[1]), false_colour)
    return filtered

Now in your case, we can apply these to the B&W image, and simply paste it onto the main image.

mask = center_resize(mask, painting.size)
mask = pixel_filter(mask, (0, 0, 0), (0, 0, 0, 255), (0, 0, 0, 0))

painting.paste(mask, (0, 0), mask)
painting.show()

Output:

output

See if this code is useful—pull it apart and use what you want!

Jollywatt
  • 1,382
  • 2
  • 12
  • 31
  • It seems there is a slight bug in the pixel_filter function here... see code fix here: http://stackoverflow.com/questions/33850149/pil-filter-pixels-and-paste/33850493#33850493 – Gil Nov 22 '15 at 01:05