66

Hi I am creating a program that replaces a face in a image with someone else's face. However, I am stuck on trying to insert the new face into the original, larger image. I have researched ROI and addWeight(needs the images to be the same size) but I haven't found a way to do this in python. Any advise is great. I am new to opencv.

I am using the following test images:

smaller_image:

enter image description here

larger_image:

enter image description here

Here is my Code so far... a mixer of other samples:

import cv2
import cv2.cv as cv
import sys
import numpy

def detect(img, cascade):
    rects = cascade.detectMultiScale(img, scaleFactor=1.1, minNeighbors=3, minSize=(10, 10), flags = cv.CV_HAAR_SCALE_IMAGE)
    if len(rects) == 0:
        return []
    rects[:,2:] += rects[:,:2]
    return rects

def draw_rects(img, rects, color):
    for x1, y1, x2, y2 in rects:
        cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)

if __name__ == '__main__':
    if len(sys.argv) != 2:                                         ## Check for error in usage syntax

    print "Usage : python faces.py <image_file>"

else:
    img = cv2.imread(sys.argv[1],cv2.CV_LOAD_IMAGE_COLOR)  ## Read image file

    if (img == None):                                     
        print "Could not open or find the image"
    else:
        cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
        gray = cv2.cvtColor(img, cv.CV_BGR2GRAY)
        gray = cv2.equalizeHist(gray)

        rects = detect(gray, cascade)

        ## Extract face coordinates         
        x1 = rects[0][3]
        y1 = rects[0][0]
        x2 = rects[0][4]
        y2 = rects[0][5]
        y=y2-y1
        x=x2-x1
        ## Extract face ROI
        faceROI = gray[x1:x2, y1:y2]

        ## Show face ROI
        cv2.imshow('Display face ROI', faceROI)
        small = cv2.imread("average_face.png",cv2.CV_LOAD_IMAGE_COLOR)  
        print "here"
        small=cv2.resize(small, (x, y))
        cv2.namedWindow('Display image')          ## create window for display
        cv2.imshow('Display image', small)          ## Show image in the window

        print "size of image: ", img.shape        ## print size of image
        cv2.waitKey(1000)              
Abid Rahman K
  • 48,289
  • 27
  • 140
  • 153
kaboomfox
  • 701
  • 1
  • 5
  • 7

8 Answers8

139

A simple way to achieve what you want:

import cv2
s_img = cv2.imread("smaller_image.png")
l_img = cv2.imread("larger_image.jpg")
x_offset=y_offset=50
l_img[y_offset:y_offset+s_img.shape[0], x_offset:x_offset+s_img.shape[1]] = s_img

the result image

Update

I suppose you want to take care of the alpha channel too. Here is a quick and dirty way of doing so:

s_img = cv2.imread("smaller_image.png", -1)

y1, y2 = y_offset, y_offset + s_img.shape[0]
x1, x2 = x_offset, x_offset + s_img.shape[1]

alpha_s = s_img[:, :, 3] / 255.0
alpha_l = 1.0 - alpha_s

for c in range(0, 3):
    l_img[y1:y2, x1:x2, c] = (alpha_s * s_img[:, :, c] +
                              alpha_l * l_img[y1:y2, x1:x2, c])

result image with alpha

Mateen Ulhaq
  • 18,406
  • 13
  • 75
  • 112
fireant
  • 12,032
  • 4
  • 34
  • 47
  • 8
    I know this is an ancient question but would you mind adding an explanation of what is going on in the alpha channel example? I'm getting into cv2 and python and this stuff is still a huge question mark for me – Jonathan Crowe Jun 25 '16 at 17:54
  • 2
    Seconding Jonathan's request. I want to know what the math is doing so I can better debug the problem – Adib Jun 30 '16 at 05:56
  • 1
    @JonathanCrowe to overlay image1 over imag2, [result-image::rgb channel] = [image1::rgb-channel] * [imag1::alpha-channel] + [image2::rgb-channel] * (1.0-[imag1::alpha-channel]). – fireant Jul 14 '16 at 18:58
  • 1
    @Adib see the above comment – fireant Jul 14 '16 at 18:58
  • 2
    Hey, in the update your line is cut: ``l_img[y_offset:y_offset+s_img.shape[0], x_offset:x_offset+s_img.shape[1], c] =`` What did you mean there? – GuySoft Aug 03 '16 at 14:22
  • It is not cut. The rest is on the next line. Works perfect. :) @GuySoft – Olezt Mar 29 '17 at 16:39
  • You saved my day man, thanks a lot – dlammy Feb 01 '21 at 11:49
15

Using @fireant's idea, I wrote up a function to handle overlays. This works well for any position argument (including negative positions).

def overlay_image_alpha(img, img_overlay, x, y, alpha_mask):
    """Overlay `img_overlay` onto `img` at (x, y) and blend using `alpha_mask`.

    `alpha_mask` must have same HxW as `img_overlay` and values in range [0, 1].
    """
    # Image ranges
    y1, y2 = max(0, y), min(img.shape[0], y + img_overlay.shape[0])
    x1, x2 = max(0, x), min(img.shape[1], x + img_overlay.shape[1])

    # Overlay ranges
    y1o, y2o = max(0, -y), min(img_overlay.shape[0], img.shape[0] - y)
    x1o, x2o = max(0, -x), min(img_overlay.shape[1], img.shape[1] - x)

    # Exit if nothing to do
    if y1 >= y2 or x1 >= x2 or y1o >= y2o or x1o >= x2o:
        return

    # Blend overlay within the determined ranges
    img_crop = img[y1:y2, x1:x2]
    img_overlay_crop = img_overlay[y1o:y2o, x1o:x2o]
    alpha = alpha_mask[y1o:y2o, x1o:x2o, np.newaxis]
    alpha_inv = 1.0 - alpha

    img_crop[:] = alpha * img_overlay_crop + alpha_inv * img_crop

Example usage:

import numpy as np
from PIL import Image

# Prepare inputs
x, y = 50, 0
img = np.array(Image.open("img_large.jpg"))
img_overlay_rgba = np.array(Image.open("img_small.png"))

# Perform blending
alpha_mask = img_overlay_rgba[:, :, 3] / 255.0
img_result = img[:, :, :3].copy()
img_overlay = img_overlay_rgba[:, :, :3]
overlay_image_alpha(img_result, img_overlay, x, y, alpha_mask)

# Save result
Image.fromarray(img_result).save("img_result.jpg")

Result:

img_result.jpg

If you encounter errors or unusual outputs, please ensure:

  • img should not contain an alpha channel. (e.g. If it is RGBA, convert to RGB first.)
  • img_overlay has the same number of channels as img.
Mateen Ulhaq
  • 18,406
  • 13
  • 75
  • 112
  • 5
    `IndexError: index 3 is out of bounds for axis 2 with size 3` is the error one gets with this. – Schütze Aug 08 '18 at 01:44
  • 1
    how do i overlay the larger image's centroid with the smaller image's centroid? I have the centroids of both the images already. I used the function above, but the smaller image's leftmost pixel is automatically overlayed on the larger image. – Alok Subedi Aug 09 '18 at 08:37
  • 3
    @Schütze source image has to be converted to RGBA eg. `img = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA).copy()` – lcapra Apr 22 '19 at 16:43
8

Based on fireant's excellent answer above, here is the alpha blending but a bit more human legible. You may need to swap 1.0-alpha and alpha depending on which direction you're merging (mine is swapped from fireant's answer).

o* == s_img.* b* == b_img.*

for c in range(0,3):
    alpha = s_img[oy:oy+height, ox:ox+width, 3] / 255.0
    color = s_img[oy:oy+height, ox:ox+width, c] * (1.0-alpha)
    beta  = l_img[by:by+height, bx:bx+width, c] * (alpha)

    l_img[by:by+height, bx:bx+width, c] = color + beta
Kurt
  • 1,948
  • 2
  • 23
  • 22
5

Here it is:

def put4ChannelImageOn4ChannelImage(back, fore, x, y):
    rows, cols, channels = fore.shape    
    trans_indices = fore[...,3] != 0 # Where not transparent
    overlay_copy = back[y:y+rows, x:x+cols] 
    overlay_copy[trans_indices] = fore[trans_indices]
    back[y:y+rows, x:x+cols] = overlay_copy

#test
background = np.zeros((1000, 1000, 4), np.uint8)
background[:] = (127, 127, 127, 1)
overlay = cv2.imread('imagee.png', cv2.IMREAD_UNCHANGED)
put4ChannelImageOn4ChannelImage(background, overlay, 5, 5)
Nathan B
  • 1,154
  • 1
  • 14
  • 12
0

When attempting to write to the destination image using any of these answers above and you get the following error:

ValueError: assignment destination is read-only

A quick potential fix is to set the WRITEABLE flag to true.

img.setflags(write=1)
Richard Lalancette
  • 2,302
  • 21
  • 29
Torantula
  • 75
  • 1
  • 9
0

For just add an alpha channel to s_img I just use cv2.addWeighted before the line l_img[y_offset:y_offset+s_img.shape[0], x_offset:x_offset+s_img.shape[1]] = s_img

as following:
s_img=cv2.addWeighted(l_img[y_offset:y_offset+s_img.shape[0], x_offset:x_offset+s_img.shape[1]],0.5,s_img,0.5,0)

0

A simple 4on4 pasting function that works-

def paste(background,foreground,pos=(0,0)):
    #get position and crop pasting area if needed
    x = pos[0]
    y = pos[1]
    bgWidth = background.shape[0]
    bgHeight = background.shape[1]
    frWidth = foreground.shape[0]
    frHeight = foreground.shape[1]
    width = bgWidth-x
    height = bgHeight-y
    if frWidth<width:
        width = frWidth
    if frHeight<height:
        height = frHeight
    # normalize alpha channels from 0-255 to 0-1
    alpha_background = background[x:x+width,y:y+height,3] / 255.0
    alpha_foreground = foreground[:width,:height,3] / 255.0
    # set adjusted colors
    for color in range(0, 3):
        fr = alpha_foreground * foreground[:width,:height,color]
        bg = alpha_background * background[x:x+width,y:y+height,color] * (1 - alpha_foreground)
        background[x:x+width,y:y+height,color] = fr+bg
    # set adjusted alpha and denormalize back to 0-255
    background[x:x+width,y:y+height,3] = (1 - (1 - alpha_foreground) * (1 - alpha_background)) * 255
    return background
dan
  • 1
  • 1
0

A simple function that blits an image front onto an image back and returns the result. It works with both 3 and 4-channel images and deals with the alpha channel. Overlaps are handled as well.

The output image has the same size as back, but always 4 channels.
The output alpha channel is given by (u+v)/(1+uv) where u,v are the alpha channels of the front and back image and -1 <= u,v <= 1. Where there is no overlap with front, the alpha value from back is taken.

import cv2

def merge_image(back, front, x,y):
    # convert to rgba
    if back.shape[2] == 3:
        back = cv2.cvtColor(back, cv2.COLOR_BGR2BGRA)
    if front.shape[2] == 3:
        front = cv2.cvtColor(front, cv2.COLOR_BGR2BGRA)

    # crop the overlay from both images
    bh,bw = back.shape[:2]
    fh,fw = front.shape[:2]
    x1, x2 = max(x, 0), min(x+fw, bw)
    y1, y2 = max(y, 0), min(y+fh, bh)
    front_cropped = front[y1-y:y2-y, x1-x:x2-x]
    back_cropped = back[y1:y2, x1:x2]

    alpha_front = front_cropped[:,:,3:4] / 255
    alpha_back = back_cropped[:,:,3:4] / 255
    
    # replace an area in result with overlay
    result = back.copy()
    print(f'af: {alpha_front.shape}\nab: {alpha_back.shape}\nfront_cropped: {front_cropped.shape}\nback_cropped: {back_cropped.shape}')
    result[y1:y2, x1:x2, :3] = alpha_front * front_cropped[:,:,:3] + (1-alpha_front) * back_cropped[:,:,:3]
    result[y1:y2, x1:x2, 3:4] = (alpha_front + alpha_back) / (1 + alpha_front*alpha_back) * 255

    return result
  • This is the only thing on here that I can get to execute but for some reason it blends horribly, the front image colours manages to sort of mix with the background or something. – ch4rl1e97 Mar 12 '21 at 00:54
  • wait a second, I changed something afterward in my own code, I’ll take a look at it – Jonas De Schouwer Mar 12 '21 at 08:10
  • it is kind of the point to mix though when alpha_front<255, then the front image is a bit transparent – Jonas De Schouwer Mar 12 '21 at 08:10
  • This is the issue I had: [click here](https://i.imgur.com/cjambIZ.png) In the end I merged like 12 different approaches and used addWeighted() for this problem and just modified my background image to be black in an editor where the top image is to be placed. In my case the front/top image doesn't have any transparency (or rather, I don't care if it does) so that worked for me. [see here for that result](https://i.imgur.com/Xi7n6f4.png) – ch4rl1e97 Mar 12 '21 at 15:02
  • I edited this answer to include my changes. The important thing is that the `alpha_back * back_cropped[:,:,:3]` on the second-to-last line changed to `(1-alpha_front) * back_cropped[:,:,:3]`. Because the alpha back channel is already taken into account in the alpha channel of the result image. – Jonas De Schouwer Mar 12 '21 at 15:56