Crop black edges with OpenCV

Image ProcessingOpencv

Image Processing Problem Overview


I think it should be a very simple problem, but I cannot find a solution or an effective keyword for search.

I just have this image.

the original image

The black edges are useless so that I want to cut them, only leaving the Windows icon (and the blue background).

I do not want to calculate the coordinate and the size of the Windows icon. GIMP and Photoshop have sort of autocrop function. OpenCV does not have one?

Image Processing Solutions


Solution 1 - Image Processing

I am not sure whether all your images are like this. But for this image, below is a simple python-opencv code to crop it.

first import libraries :

import cv2
import numpy as np

Read the image, convert it into grayscale, and make in binary image for threshold value of 1.

img = cv2.imread('sofwin.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
_,thresh = cv2.threshold(gray,1,255,cv2.THRESH_BINARY)

Now find contours in it. There will be only one object, so find bounding rectangle for it.

contours,hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
x,y,w,h = cv2.boundingRect(cnt)

Now crop the image, and save it into another file.

crop = img[y:y+h,x:x+w]
cv2.imwrite('sofwinres.png',crop)

Below is the result :

enter image description here

Solution 2 - Image Processing

I thought this answer is much more succinct:

def crop(image):
    y_nonzero, x_nonzero, _ = np.nonzero(image)
    return image[np.min(y_nonzero):np.max(y_nonzero), np.min(x_nonzero):np.max(x_nonzero)]

Solution 3 - Image Processing

import numpy as np

def autocrop(image, threshold=0):
    """Crops any edges below or equal to threshold
    
    Crops blank image to 1x1.
    
    Returns cropped image.
    
    """
    if len(image.shape) == 3:
        flatImage = np.max(image, 2)
    else:
        flatImage = image
    assert len(flatImage.shape) == 2
    
    rows = np.where(np.max(flatImage, 0) > threshold)[0]
    if rows.size:
        cols = np.where(np.max(flatImage, 1) > threshold)[0]
        image = image[cols[0]: cols[-1] + 1, rows[0]: rows[-1] + 1]
    else:
        image = image[:1, :1]
        
    return image

Solution 4 - Image Processing

OK, so for completeness, I implemented each of the recommendations above, added an iterative version of the recursive algorithm (once corrected) and did a set of performance tests.

TLDR: Recursive is probably the best for the average case (but use the one below--the OP has a couple bugs), and the autocrop is the best for images you expect to be almost empty.

General findings:

  1. The recursive algorithm above has a couple of off-by-1 bugs in it. Corrected version is below.

  2. The cv2.findContours function has problems with non-rectangular images, and actually even trims some of the image off in various scenarios. I added a version which uses cv2.CHAIN_APPROX_NONE to see if it helps (it doesn't help).

  3. The autocrop implementation is great for sparse images, but poor for dense ones, the inverse of the recursive/iterative algorithm.

    import numpy as np import cv2

    def trim_recursive(frame): if frame.shape[0] == 0: return np.zeros((0,0,3))

    crop top

    if not np.sum(frame[0]): return trim_recursive(frame[1:])

    crop bottom

    elif not np.sum(frame[-1]): return trim_recursive(frame[:-1])

    crop left

    elif not np.sum(frame[:, 0]): return trim_recursive(frame[:, 1:]) # crop right elif not np.sum(frame[:, -1]): return trim_recursive(frame[:, :-1]) return frame

    def trim_contours(frame): gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) _,thresh = cv2.threshold(gray,1,255,cv2.THRESH_BINARY) _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) == 0: return np.zeros((0,0,3)) cnt = contours[0] x, y, w, h = cv2.boundingRect(cnt) crop = frame[y:y + h, x:x + w] return crop

    def trim_contours_exact(frame): gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) _,thresh = cv2.threshold(gray,1,255,cv2.THRESH_BINARY) _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if len(contours) == 0: return np.zeros((0,0,3)) cnt = contours[0] x, y, w, h = cv2.boundingRect(cnt) crop = frame[y:y + h, x:x + w] return crop

    def trim_iterative(frame): for start_y in range(1, frame.shape[0]): if np.sum(frame[:start_y]) > 0: start_y -= 1 break if start_y == frame.shape[0]: if len(frame.shape) == 2: return np.zeros((0,0)) else: return np.zeros((0,0,0)) for trim_bottom in range(1, frame.shape[0]): if np.sum(frame[-trim_bottom:]) > 0: break

    for start_x in range(1, frame.shape[1]): if np.sum(frame[:, :start_x]) > 0: start_x -= 1 break for trim_right in range(1, frame.shape[1]): if np.sum(frame[:, -trim_right:]) > 0: break

    end_y = frame.shape[0] - trim_bottom + 1 end_x = frame.shape[1] - trim_right + 1

    print('iterative cropping x:{}, w:{}, y:{}, h:{}'.format(start_x, end_x - start_x, start_y, end_y - start_y))

    return frame[start_y:end_y, start_x:end_x]

    def autocrop(image, threshold=0): """Crops any edges below or equal to threshold

    Crops blank image to 1x1.

    Returns cropped image.

    """ if len(image.shape) == 3: flatImage = np.max(image, 2) else: flatImage = image assert len(flatImage.shape) == 2

    rows = np.where(np.max(flatImage, 0) > threshold)[0] if rows.size: cols = np.where(np.max(flatImage, 1) > threshold)[0] image = image[cols[0]: cols[-1] + 1, rows[0]: rows[-1] + 1] else: image = image[:1, :1]

    return image

Then to test it, I made this simple function:

import datetime
import numpy as np
import random

ITERATIONS = 10000

def test_image(img):
  orig_shape = img.shape
  print ('original shape: {}'.format(orig_shape))
  start_time = datetime.datetime.now()
  for i in range(ITERATIONS):
    recursive_img = trim_recursive(img)
  print ('recursive shape: {}, took {} seconds'.format(recursive_img.shape, (datetime.datetime.now()-start_time).total_seconds()))
  start_time = datetime.datetime.now()
  for i in range(ITERATIONS):
    contour_img = trim_contours(img)
  print ('contour shape: {}, took {} seconds'.format(contour_img.shape, (datetime.datetime.now()-start_time).total_seconds()))
  start_time = datetime.datetime.now()
  for i in range(ITERATIONS):
    exact_contour_img = trim_contours(img)
  print ('exact contour shape: {}, took {} seconds'.format(exact_contour_img.shape, (datetime.datetime.now()-start_time).total_seconds()))
  start_time = datetime.datetime.now()
  for i in range(ITERATIONS):
    iterative_img = trim_iterative(img)
  print ('iterative shape: {}, took {} seconds'.format(iterative_img.shape, (datetime.datetime.now()-start_time).total_seconds()))
  start_time = datetime.datetime.now()
  for i in range(ITERATIONS):
    auto_img = autocrop(img)
  print ('autocrop shape: {}, took {} seconds'.format(auto_img.shape, (datetime.datetime.now()-start_time).total_seconds()))


def main():
  orig_shape = (10,10,3)

  print('Empty image--should be 0x0x3')
  zero_img = np.zeros(orig_shape, dtype='uint8')
  test_image(zero_img)

  print('Small image--should be 1x1x3')
  small_img = np.zeros(orig_shape, dtype='uint8')
  small_img[3,3] = 1
  test_image(small_img)

  print('Medium image--should be 3x7x3')
  med_img = np.zeros(orig_shape, dtype='uint8')
  med_img[5:8, 2:9] = 1
  test_image(med_img)

  print('Random image--should be full image: 100x100')
  lg_img = np.zeros((100,100,3), dtype='uint8')
  for y in range (100):
    for x in range(100):
      lg_img[y,x, 0] = random.randint(0,255)
      lg_img[y, x, 1] = random.randint(0, 255)
      lg_img[y, x, 2] = random.randint(0, 255)
  test_image(lg_img)

main()

...AND THE RESULTS...

Empty image--should be 0x0x3
original shape: (10, 10, 3)
recursive shape: (0, 0, 3), took 0.295851 seconds
contour shape: (0, 0, 3), took 0.048656 seconds
exact contour shape: (0, 0, 3), took 0.046273 seconds
iterative shape: (0, 0, 3), took 1.742498 seconds
autocrop shape: (1, 1, 3), took 0.093347 seconds
Small image--should be 1x1x3
original shape: (10, 10, 3)
recursive shape: (1, 1, 3), took 1.342977 seconds
contour shape: (0, 0, 3), took 0.048919 seconds
exact contour shape: (0, 0, 3), took 0.04683 seconds
iterative shape: (1, 1, 3), took 1.084258 seconds
autocrop shape: (1, 1, 3), took 0.140886 seconds
Medium image--should be 3x7x3
original shape: (10, 10, 3)
recursive shape: (3, 7, 3), took 0.610821 seconds
contour shape: (0, 0, 3), took 0.047263 seconds
exact contour shape: (0, 0, 3), took 0.046342 seconds
iterative shape: (3, 7, 3), took 0.696778 seconds
autocrop shape: (3, 7, 3), took 0.14493 seconds
Random image--should be full image: 100x100
original shape: (100, 100, 3)
recursive shape: (100, 100, 3), took 0.131619 seconds
contour shape: (98, 98, 3), took 0.285515 seconds
exact contour shape: (98, 98, 3), took 0.288365 seconds
iterative shape: (100, 100, 3), took 0.251708 seconds
autocrop shape: (100, 100, 3), took 1.280476 seconds

Solution 5 - Image Processing

How about a slick little recursive function?

import cv2
import numpy as np
def trim(frame):
    #crop top
    if not np.sum(frame[0]):
        return trim(frame[1:])
    #crop bottom
    elif not np.sum(frame[-1]):
        return trim(frame[:-2])
    #crop left
    elif not np.sum(frame[:,0]):
        return trim(frame[:,1:]) 
    #crop right
    elif not np.sum(frame[:,-1]):
        return trim(frame[:,:-2])    
    return frame

Load and threshold the image to ensure the dark areas are black:

img = cv2.imread("path_to_image.png")   
thold = (img>120)*img

Then call the recursive function

trimmedImage = trim(thold)

Solution 6 - Image Processing

additional information on Abid Rahman K's answer:

cv2.boundingRect can do the job without finding outer contour like below

_,thresh = cv2.threshold(gray,1,255,cv2.THRESH_BINARY)
x,y,w,h = cv2.boundingRect(thresh)

(this feature probably added after Abid's answer and runs FASTER)

Solution 7 - Image Processing

In case it helps anyone, I went with this tweak of @wordsforthewise's replacement for a PIL-based solution:

bw = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
rows, cols = bw.shape

non_empty_columns = np.where(bw.max(axis=0) > 0)[0]
non_empty_rows = np.where(bw.max(axis=1) > 0)[0]
cropBox = (min(non_empty_rows) * (1 - padding),
            min(max(non_empty_rows) * (1 + padding), rows),
            min(non_empty_columns) * (1 - padding),
            min(max(non_empty_columns) * (1 + padding), cols))

return img[cropBox[0]:cropBox[1]+1, cropBox[2]:cropBox[3]+1 , :]

(It's a tweak in that the original code expects to crop away a white background rather than a black one.)

Solution 8 - Image Processing

Python Version 3.6


Crop images and insert into a 'CropedImages' folder

import cv2
import os

arr = os.listdir('./OriginalImages')

for itr in arr:
    img = cv2.imread(itr)
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    _,thresh = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    _, contours, _ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    cnt = contours[0]
    x,y,w,h = cv2.boundingRect(cnt)
    crop = img[y:y+h,x:x+w]
    cv2.imwrite('CropedImages/'+itr,crop)

Change the number 120 to other in 9th line and try for your images, It will work

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGqqnbigView Question on Stackoverflow
Solution 1 - Image ProcessingAbid Rahman KView Answer on Stackoverflow
Solution 2 - Image ProcessingNick AllenView Answer on Stackoverflow
Solution 3 - Image ProcessingfviktorView Answer on Stackoverflow
Solution 4 - Image ProcessingSellyView Answer on Stackoverflow
Solution 5 - Image ProcessingDanGoodrickView Answer on Stackoverflow
Solution 6 - Image ProcessingsturkmenView Answer on Stackoverflow
Solution 7 - Image ProcessingssokolowView Answer on Stackoverflow
Solution 8 - Image ProcessingSunku Vamsi Tharun KumarView Answer on Stackoverflow