skip to Main Content

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.

2

Answers


  1. Chosen as BEST ANSWER

    While Paul92's answer is more than enough, I wanted to post my code anyway for any future visitor.

    I'm doing this cropping to get rid of white background in some product photos. So, the main goal is to get rid of the whites while keeping the product intact. Most of the product photos have shadows on the ground. They are either the ground itself (faded), or the product's shadow, or both.

    While the object detection works fine, these shadows also count as part of the object. Differentiating the shadows from the objects is not really necessary, but it results in some images that are not so desired. For example, examine the left and bottom sides of the image (shadow). The cut/crop is obviously visible, and doesn't look all that nice.

    shadow cut

    To get around this problem, I wanted to do non-rectangular crops. Using masks seems to do the job just fine. The next problem was to do the cropping with feathered/blurred edges so that I can get rid of these visible shadow cuts. With the help of Paul92, I've managed to do that. Example output (notice the missing shadow cuts, the edges are softer):

    softer edges

    Operations on the image(s):

    image operations

    The code (show_mask_feathered.py, helpers_cv2.py)

    import sys
    from pathlib import Path
    import cv2
    import numpy
    from helpers_cv2 import *
    
    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)
    
    dilation_length = 51
    blur_length     = 51
    
    mask         = mask_from_contours(img, contours)
    mask_dilated = dilate_mask(mask, dilation_length)
    mask_smooth  = smooth_mask(mask_dilated, odd(dilation_length * 1.5))
    mask_blurred = cv2.GaussianBlur(mask_smooth, (blur_length, blur_length), 0)
    mask_blurred = cv2.cvtColor(mask_blurred, cv2.COLOR_GRAY2BGR)
    
    mask_threshed = threshold(mask_blurred, 1)
    mask_contours = find_contours(mask_threshed)
    mask_contour  = max_contour(mask_contours)
    
    x, y, w, h   = cv2.boundingRect(mask_contour)
    
    img_cropped  = img[y:y+h, x:x+w]
    mask_cropped = mask_blurred[y:y+h, x:x+w]
    background   = numpy.full(img_cropped.shape, (200,240,200), dtype=numpy.uint8)
    output       = alpha_blend(background, img_cropped, mask_cropped)
    

  2. 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')
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search