22

I am trying to compare images using OpenCV and Python.

Consider these images:


Image  900 x 726


Image 900 x 675


Both feature an identical pair of shoes, set to a white background. The only difference being that the first has a taller background than the second.

I want to know how to programmatically crop the white backgrounds of both so that I'm left with only the pair of shoes.

I must add that it won't be possible for me to manually crop the backgrounds.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Tuhin Sah
  • 235
  • 1
  • 3
  • 8
  • 1
    Threshold the image to get a binary image, use findContours() to find and largest contour and crop it – ZdaR Jan 23 '18 at 06:27
  • @ZdaR , I don't think the detected contours will be that precise! – Ubdus Samad Jan 23 '18 at 06:30
  • The object seems to have a clear boundary, so *use findContours() to find and largest contour*, if you choose the largest contour it should suffice the required problem – ZdaR Jan 23 '18 at 06:35
  • @UbdusSamad Why not? The OP says that "Both feature an identical pair of shoes". The precision of the contour is irrelevant. – Costantino Grana Jan 23 '18 at 06:36
  • Try it yourselves, the `findcontours()` will mess up the boundaries, I am quite sure of it, though it's possible that for this particular image it'll do good, but it won't work for every image. (I could be wrong but I am telling this as per the best of my knowledge, cv2 may have been updated or something) – Ubdus Samad Jan 23 '18 at 06:44
  • @ZdaR If you try it and succeed please notify me that turned out to be wrong! – Ubdus Samad Jan 23 '18 at 06:47
  • The secret sauce is not in `cv2.findContours()`, instead the accuracy is highly dependent on how you threshold the image, `cv2.findContours()` takes a `cv2.RETR_EXTERNAL` flag which will precisely find the largest outer contour, provided the boundaries are preserved in thresholding. – ZdaR Jan 23 '18 at 06:53
  • This is the best result I can get: https://i.stack.imgur.com/N2MAT.jpg – Kinght 金 Jan 23 '18 at 06:54
  • The question says *I want to know how to programmatically crop the white backgrounds of both so that I'm left with only the pair of shoes*, OP just wants to know the bounding rect, there is no need of morphing etc. Your first step of thresholding is enough, just find the largest contour after it and get it's bounding rect. – ZdaR Jan 23 '18 at 06:56
  • @ZdaR Thank you for the answer! I'll test it out on my dataset, and report the results! – Tuhin Sah Jan 23 '18 at 07:22
  • @Silencer That looks like exactly what I need! Could you post your code in an answer? – Tuhin Sah Jan 23 '18 at 07:24
  • @ZdaR If don't do `morp-op`, there will be a lot of noise. Maybe the shoes will also be seperated to different parts. Then the max contour will not really contain the whole target region. At least in my solution, it is needed. – Kinght 金 Jan 23 '18 at 07:29
  • @TuhinSah Make it clear: shoes on which backgound? Black or white. Different background responds to different method. White is simpler. And do you want to deal with the blank border? Just crop the borders away? The method is posted, similar with others. – Kinght 金 Jan 23 '18 at 07:38
  • @Silencer The shoes are on a white background. I would like to completely get rid of the border; as in be left with a rectangular box with either a white or a transparent background, having the length and width of the shoes in the picture. – Tuhin Sah Jan 23 '18 at 09:22
  • @TuhinSah Ok, My new result, suitable? [croped shoes](https://i.stack.imgur.com/RLLnu.png) – Kinght 金 Jan 23 '18 at 09:36
  • @ZdaR Sorry, I didn't read the question very well, what you have suggested is indeed optimal for OP's problem. – Ubdus Samad Jan 23 '18 at 09:46
  • @Silencer can you elaborate how did you get those results, they look really precise! – Ubdus Samad Jan 23 '18 at 09:46
  • @Silencer Yes! It worked exactly as I wanted! Thank you! – Tuhin Sah Jan 23 '18 at 11:24

6 Answers6

37

You requirement in the comment: The shoes are on a white background. I would like to completely get rid of the border; as in be left with a rectangular box with either a white or a transparent background, having the length and width of the shoes in the picture.

Then my steps to crop the target regions:

  1. Convert to gray, and threshold
  2. Morph-op to remove noise
  3. Find the max-area contour
  4. Crop and save it
#!/usr/bin/python3
# Created by Silencer @ Stackoverflow 
# 2018.01.23 14:41:42 CST
# 2018.01.23 18:17:42 CST
import cv2
import numpy as np

## (1) Convert to gray, and threshold
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th, threshed = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)

## (2) Morph-op to remove noise
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11,11))
morphed = cv2.morphologyEx(threshed, cv2.MORPH_CLOSE, kernel)

## (3) Find the max-area contour
cnts = cv2.findContours(morphed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cnt = sorted(cnts, key=cv2.contourArea)[-1]

## (4) Crop and save it
x,y,w,h = cv2.boundingRect(cnt)
dst = img[y:y+h, x:x+w]
cv2.imwrite("001.png", dst)

Result:

Kinght 金
  • 17,681
  • 4
  • 60
  • 74
1

Kinght's solution works well. In my case, I also have CMYK images. When I crop them, I get incorrect (vivid colors) output. And it seems OpenCV doesn't support CMYK. So I needed a way to convert CMYK images to RGB, and then open it with OpenCV. This is how I handled it:

import cv2
import numpy

from PIL import Image
from PIL import ImageCms

# force opening truncated/corrupt image files
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

img = "shoes.jpg"

img = Image.open(img)
if img.mode == "CMYK":
    # color profiles can be found at C:\Program Files (x86)\Common Files\Adobe\Color\Profiles\Recommended
    img = ImageCms.profileToProfile(img, "USWebCoatedSWOP.icc", "sRGB_Color_Space_Profile.icm", outputMode="RGB")
# PIL image -> OpenCV image; see https://stackoverflow.com/q/14134892/2202732
img = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR)

## (1) Convert to gray, and threshold
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th, threshed = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)

## (2) Morph-op to remove noise
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11,11))
morphed = cv2.morphologyEx(threshed, cv2.MORPH_CLOSE, kernel)

## (3) Find the max-area contour
cnts = cv2.findContours(morphed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cnt = sorted(cnts, key=cv2.contourArea)[-1]

## (4) Crop and save it
x,y,w,h = cv2.boundingRect(cnt)
dst = img[y:y+h, x:x+w]

# add border/padding around the cropped image
# dst = cv2.copyMakeBorder(dst, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=[255,255,255])

cv2.imshow("image", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

# create/write to file
# cv2.imwrite("001.png", dst)
akinuri
  • 10,690
  • 10
  • 65
  • 102
1

by PIL you can convert white background to transparent:

from PIL import Image
  
def convertImage():
    img = Image.open("hi.png")
    img = img.convert("RGBA")
  
    datas = img.getdata()
  
    newData = []
  
    for item in datas:
        if item[0] == 255 and item[1] == 255 and item[2] == 255:
            newData.append((255, 255, 255, 0))
        else:
            newData.append(item)
  
    img.putdata(newData)
    img.save("./New.png", "PNG")
    print("Successful")
  
convertImage()

and here is the output sample: enter image description here

Ali Ganjbakhsh
  • 541
  • 7
  • 13
0

This link worked perfectly for me for a similar problem, although it uses PIL. Note that it will result in a rectangular image, bounded by the top/right/bottom/left-most pixels that are not white. In your case, it should give identical images with the same size.

I am guessing the code could be easily adapted to work with OpenCV functions only.

0

I found this on github.

https://imagemagick.org/script/download.php

import pgmagick

def remove_background(image, background=None):
    """Returns a copy of `image` that only contains the parts that is distinct
       from the background. If background is None, returns parts that are
       distinct from white."""
    if background is None:
        background = pgmagick.Image(image.size(), 'white')
    elif isinstance(background, pgmagick.Image):
        blob = pgmagick.Blob()
        background.write(blob)
        background = pgmagick.Image(blob, image.size())
    else:
        background = pgmagick.Image(image.size(), background)
    background.composite(image, 0, 0, pgmagick.CompositeOperator.DifferenceCompositeOp)
    background.threshold(25)
    blob = pgmagick.Blob()
    image.write(blob)
    image = pgmagick.Image(blob, image.size())
    image.composite(background, 0, 0, pgmagick.CompositeOperator.CopyOpacityCompositeOp)
    return image
Oscar Rangel
  • 848
  • 1
  • 10
  • 18
-1

Although the question has been answered thoroughly already, I would like to share a simple version that relies on numpy only:

import numpy as np

def remove_background(image, bg_color=255):
    # assumes rgb image (w, h, c)
    intensity_img = np.mean(image, axis=2)

    # identify indices of non-background rows and columns, then look for min/max indices
    non_bg_rows = np.nonzero(np.mean(intensity_img, axis=1) != bg_color)
    non_bg_cols = np.nonzero(np.mean(intensity_img, axis=0) != bg_color)
    r1, r2 = np.min(non_bg_rows), np.max(non_bg_rows)
    c1, c2 = np.min(non_bg_cols), np.max(non_bg_cols)

    # return cropped image
    return image[r1:r2+1, c1:c2+1, :]
ketza
  • 127
  • 1
  • 12