Search code examples
pythonopencvtransparencycrop

Feather cropped edges


I'm trying to crop an object from an image, and paste it on another image. Examining the method in this answer, I've successfully managed to do that. For example:

crop steps

The code (show_mask_applied.py):

import sys
from pathlib import Path
from helpers_cv2 import *
import cv2
import numpy

img_path = Path(sys.argv[1])

img      = cmyk_to_bgr(str(img_path))
threshed = threshold(img, 240, type=cv2.THRESH_BINARY_INV)
contours = find_contours(threshed)
mask     = mask_from_contours(img, contours)
mask     = dilate_mask(mask, 50)
crop     = cv2.bitwise_or(img, img, mask=mask)

bg      = cv2.imread("bg.jpg")
bg_mask = cv2.bitwise_not(mask)
bg_crop = cv2.bitwise_or(bg, bg, mask=bg_mask)

final   = cv2.bitwise_or(crop, bg_crop)

cv2.imshow("debug", final)

cv2.waitKey(0)
cv2.destroyAllWindows()

helpers_cv2.py:

from pathlib import Path
import cv2
import numpy
from PIL import Image
from PIL import ImageCms
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

def cmyk_to_bgr(cmyk_img):
    img = Image.open(cmyk_img)
    if img.mode == "CMYK":
        img = ImageCms.profileToProfile(img, "Color Profiles\\USWebCoatedSWOP.icc", "Color Profiles\\sRGB_Color_Space_Profile.icm", outputMode="RGB")
    return cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR)

def threshold(img, thresh=128, maxval=255, type=cv2.THRESH_BINARY):
    if len(img.shape) == 3:
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    threshed = cv2.threshold(img, thresh, maxval, type)[1]
    return threshed

def find_contours(img):
    kernel   = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11,11))
    morphed  = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
    contours = cv2.findContours(morphed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    return contours[-2]

def mask_from_contours(ref_img, contours):
    mask = numpy.zeros(ref_img.shape, numpy.uint8)
    mask = cv2.drawContours(mask, contours, -1, (255,255,255), -1)
    return cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)

def dilate_mask(mask, kernel_size=11):
    kernel  = numpy.ones((kernel_size,kernel_size), numpy.uint8)
    dilated = cv2.dilate(mask, kernel, iterations=1)
    return dilated

Now, instead of sharp edges, I want to crop with feathered/smooth edges. For example (the right one; created in Photoshop):

sharp vs feathered

How can I do that?


All images and codes can be found that at this repository.


Solution

  • You are using a mask to select parts of the overlay image. The mask currently looks like this:

    enter image description here

    Let's first add a Gaussian blur to this mask.

    mask_blurred  = cv2.GaussianBlur(mask,(99,99),0)
    

    We get to this:

    enter image description here

    Now, the remaining task it to blend the images using the alpha value in the mask, rather than using it as a logical operator like you do currently.

    mask_blurred_3chan = cv2.cvtColor(mask_blurred, cv2.COLOR_GRAY2BGR).astype('float') / 255.
    img = img.astype('float') / 255.
    bg = bg.astype('float') / 255.
    out  = bg * (1 - mask_blurred_3chan) + img * mask_blurred_3chan
    

    The above snippet is quite simple. First, transform the mask into a 3 channel image (since we want to mask all the channels). Then transform the images to float, since the masking is done in floating point. The last line does the actual work: for each pixel, blends the bg and img images according to the value in the mask. The result looks like this:

    enter image description here

    The amount of feathering is controlled by the size of the kernel in the Gaussian blur. Note that it has to be an odd number.

    After this, out (the final image) is still in floating point. It can be converted back to int using:

    out = (out * 255).astype('uint8')