algorithmopencvimage-processingcomputer-visionsprite

How to detect and crop individual sprites when their bounding boxes overlap?


Please refer to this link first:

How to automatically detect and crop individual sprite bounds in sprite sheet?

In the example in the above link, the bounding boxes of the individual sprites do not overlap, so the code in the accepted answer works well, and all the sprites are extracted in individual files.

Non-overlapping bounding boxes sprite sheet

But supposed the sprites were packed differently such that their bounding boxes overlapped even thought the individual images did not overlap:

Overlapping bounding boxes sprite sheet

Then the above code would not work well at all, and just one file is output because the bounding boxes would intersect.

How would someone solve this problem using OpenCV?

Without using OpenCV, my approach would be to:

Instead of the approach I have described above, is there a method to use OpenCV to do this ? I can do it using my method, but I would like to learn more about OpenCV and what it is capable of, but I am just a beginner and still learning.


Solution

  • You can fill each contour with white and then generate an image with only that contour. After that, apply a regular cropping by getting the properties of the bounding rect. Here is the code:

    im = cv2.imread("trees.png") # read image
    imGray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) # convert to gray
    contours, _ = cv2.findContours(imGray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # contouring
    sortedContours = sorted(contours, key=cv2.contourArea, reverse=True) # sorting, not necessary...
    for contourIdx in range(0,len(sortedContours)-1): # loop with index for easier saving
        contouredImage = im.copy() # copy image
        contouredImage = cv2.drawContours(contouredImage, sortedContours, contourIdx, (255,255,255), -1) # fill contour with white
        extractedImage = cv2.inRange(contouredImage, (254,254,254), (255,255,255)) # extract white from image
        resultImage = cv2.bitwise_and(im, im, mask=extractedImage) # AND operator to get only one filled contour at a time
        x, y, w, h = cv2.boundingRect(sortedContours[contourIdx]) # get bounding box
        croppedImage = resultImage[y:y + h, x:x + w] # crop
        cv2.imwrite("contour_"+str(contourIdx)+".png", croppedImage) # save
    

    And the results look nice as well, I'll just post the biggest one and smallest one as an example:

    Biggest tree

    Smallest

    V2.0: make it work with both images

    The first image, for some reason, has 255 on the green channel for all the background. Which is why only one big contour is detected. I added a few lines of code to pre-process the image and also to take into consideration the alpha channel. Here is the code:

    im = cv2.imread("trees.png", cv2.IMREAD_UNCHANGED) # read image
    b, g, r, alpha = cv2.split(im.copy()) # split image
    g[g==255] = 0 # for the first image where the green channel has 255 on all background pixels
    imBGR = cv2.merge([b,g,r]) # recombine image in BGR format
    imGray = cv2.cvtColor(imBGR, cv2.COLOR_BGR2GRAY) # convert to gray
    contours, _ = cv2.findContours(imGray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # contouring
    sortedContours = sorted(contours, key=cv2.contourArea, reverse=True) # sorting, not necessary...
    for contourIdx in range(0,len(sortedContours)-1): # loop with index for easier saving
        contouredImage = imBGR.copy() # copy BGR image
        contouredImage = cv2.drawContours(contouredImage, sortedContours, contourIdx, (255,255,255), -1) # fill contour with white
        extractedImage = cv2.inRange(contouredImage, (254,254,254), (255,255,255)) # extract white from image
        resultImage = cv2.bitwise_and(imBGR, imBGR, mask=extractedImage) # AND operator to get only one filled contour at a time
        x, y, w, h = cv2.boundingRect(sortedContours[contourIdx]) # get bounding box
        croppedImage = resultImage[y:y + h, x:x + w] # crop
        cv2.imwrite("trees_2_contour_"+str(contourIdx)+".png", croppedImage) # save
    

    I won't bother with adding images of the results again, as they are the same as before. However, here is an image of the first example with the green background I am talking about:

    The code:

    im = cv2.imread("trees2.png", cv2.IMREAD_UNCHANGED) # read image
    b, g, r, alpha = cv2.split(im.copy()) # split image
    imBGR = cv2.merge([b,g,r]) # recombine image in BGR format
    plt.imshow(imBGR)
    

    The image:

    Example

    I went around this with g[g==255] = 0