Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by SolarRanger022

How can I crop an image to remove white space using Python and OpenCV?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm trying to remove extraneous white spaces from an image by detecting empty regions and cropping them out, similar to the examples shown below.

Image with spaces --> Image after cropping

I attempted a solution using OpenCV and numpy, but the current code isn't working as expected. I want to identify and isolate the non-white areas so that only meaningful parts remain.

Below is the code I've been working on:

PYTHON
import cv2

import numpy as np

def crop_empty_spaces_refined(image_path, threshold_percentage=0.01):
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)

PYTHON
if image is None: print(f"Error: Could not read image at {image_path}") return None if image.shape[2] == 4: # RGBA image gray = image[:, :, 3] # Use alpha channel else: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV) kernel = np.ones((3, 3), np.uint8) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: image_area = image.shape[0] * image.shape[1] min_contour_area = image_area * threshold_percentage valid_contours = [cnt for cnt in contours if cv2.contourArea(cnt) >= min_contour_area] if valid_contours: # ***Corrected Bounding Box Calculation*** x_coords = [] y_coords = [] for cnt in valid_contours: x, y, w, h = cv2.boundingRect(cnt) x_coords.extend([x, x + w]) # Add both start and end x y_coords.extend([y, y + h]) # Add both start and end y x_min = min(x_coords) y_min = min(y_coords) x_max = max(x_coords) y_max = max(y_coords) cropped_image = image[y_min:y_max, x_min:x_max] return cropped_image else: print("No valid contours found after filtering. Returning original image.") return image else: return image

image_path = '/mnt/data/Untitled.png' # file path
cropped_image = crop_empty_spaces_refined(image_path, threshold_percentage=0.0001)

if cropped_image is not None:
cv2.imwrite('/mnt/data/cropped_output.png', cropped_image)
print("Image Cropped and saved")
else:
print("Could not crop image")

2

Comments 0

Answers 4

Views 96

4 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below is an updated version of your code with some improvements and a few extra comments. The updates include:

  • Adding a check if the image is grayscale (i.e. only a 2D array) to avoid errors when accessing image.shape[2].
  • Optionally using the alpha channel (if available) to detect empty regions.
  • More descriptive comments for each step.
PYTHON
import cv2 import numpy as np def crop_empty_spaces_refined(image_path, threshold_percentage=0.01): # Read the image (including alpha channel if it exists) image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) if image is None: print(f"Error: Could not read image at {image_path}") return None # Determine how to get a grayscale image if len(image.shape) == 2: # Already grayscale gray = image elif image.shape[2] == 4: # RGBA image; using the alpha channel can help if empty spaces are transparent. gray = image[:, :, 3] else: # BGR image; convert to grayscale gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Invert threshold to select non-white pixels _, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV) # Apply a morphological opening to remove small noises (adjust kernel size if needed) kernel = np.ones((3, 3), np.uint8) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1) # Find contours of the non-white areas contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: image_area = image.shape[0] * image.shape[1] min_contour_area = image_area * threshold_percentage # Filter out very small contours based on the given threshold percentage valid_contours = [cnt for cnt in contours if cv2.contourArea(cnt) >= min_contour_area] if valid_contours: # Prepare to combine bounding boxes from each valid contour. x_coords = [] y_coords = [] for cnt in valid_contours: x, y, w, h = cv2.boundingRect(cnt) # Save the start and end coordinates x_coords.extend([x, x + w]) y_coords.extend([y, y + h]) x_min = min(x_coords) y_min = min(y_coords) x_max = max(x_coords) y_max = max(y_coords) cropped_image = image[y_min:y_max, x_min:x_max] return cropped_image else: print("No valid contours found after filtering. Returning original image.") return image else: print("No contours found. The image might be completely empty.") return image # Example usage: image_path = '/mnt/data/Untitled.png' # file path cropped_image = crop_empty_spaces_refined(image_path, threshold_percentage=0.0001) if cropped_image is not None: cv2.imwrite('/mnt/data/cropped_output.png', cropped_image) print("Image cropped and saved.") else: print("Could not crop image.")

Explanation

  1. Image Reading & Grayscale Conversion:
    The code first reads the image in an unchanged mode so that the alpha channel is not discarded. Then, it checks the number of dimensions and channels to decide whether to:

    • Use the alpha channel (if RGBA)
    • Convert BGR to grayscale
    • Use the image directly if it is already in grayscale.
  2. Thresholding:
    A binary inverse threshold is applied to create a mask where non-white pixels become white. You can adjust the 240 value if your background color differs.

  3. Morphology & Contour Detection:
    A morphological opening removes small artifacts and noise. Then, contours are detected. Any contour whose area is smaller than a percentage of the image area is filtered out.

  4. Bounding Box Calculation:
    The code computes the overall bounding box that contains all valid contours and crops the image accordingly.

This approach should help you properly crop out the empty spaces. Adjust parameters (like threshold value or kernel size) as needed for your specific images.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by QuantumObserver208 1 month ago

0

Approach:

  1. threshold -> obtain mask
  2. use boundingRect() on the mask
  3. crop
PYTHON
im = cv.imread("Cb768fyr.jpg") gray = cv.cvtColor(im, cv.COLOR_BGR2GRAY) th = 240 # 255 won't do, the image's background isn't perfectly white (th, mask) = cv.threshold(gray, th, 255, cv.THRESH_BINARY_INV) (x, y, w, h) = cv.boundingRect(mask) pad = 0 # increase to give it some border cropped = im[y-pad:y+h+pad, x-pad:x+w+pad]

cropped

Why threshold at all? Because cv.boundingRect() would otherwise treat all non-zero pixels as "true", i.e. the background would be considered foreground.

Why threshold with something other than 255? The background isn't perfectly white, due to the source image having been compressed lossily. If you did, that would be the result:

bad mask, level 255


If you wanted to replace cv.boundingRect(), you can do it like this:

  1. max-reduce mask along each axis in turn
  2. find first and last index of positive values
PYTHON
xproj = np.max(mask, axis=1) # collapse X, have Y ymin = np.argmax(xproj) ymax = len(xproj) - np.argmax(xproj[::-1]) print(f"{ymin=}, {ymax=}") yproj = np.max(mask, axis=0) xmin = np.argmax(yproj) xmax = len(yproj) - np.argmax(yproj[::-1]) print(f"{xmin=}, {xmax=}") cropped = im[ymin-pad:ymax+pad, xmin-pad:xmax+pad]

This could also use np.argwhere(). I won't bother comparing these two approaches since cv.boundingRect() does the job already.


The findContours approach will pick any connected component, not all of them. This means it could sometimes pick the triad (bottom left) or text (top left), and entirely discard most of the image.

You could fix that by slapping a convex hull on all the contours, but you'd still have to call boundingRect() anyway. So, all the contour stuff is wasted effort.

No comments yet.

Answer by QuasarTraveler402 1 month ago

0

Assuming you want to also get rid of the small structures in the upper left and lower left corners, you can do the following:

  • Use cv2.connectedComponentsWithStats() to find all connected components, together with their statistics (area, bounding box, center; see this answer for and explanation of the returned results).
  • Get mask and rectangle of component with largest area (ignoring the background component at index 0).
  • Fill everything that does not belong to the component's mask with white.
  • Crop with component's rectangle.

Code:

PYTHON
import cv2 in_path = "stackoverflow/car.jpg" # TODO: Adjust as necessary out_path = "stackoverflow/cropped.png" # TODO: Adjust as necessary image = cv2.imread(in_path) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV) # Following https://stackoverflow.com/a/35854198/7395592 _, labels, stats, _ = cv2.connectedComponentsWithStats(mask) # Ignore label 0, which is the background stats = stats[1:] # Get mask and rectangle (columns 0–3) for region with largest area (column 4) row_idx_largest = stats[:, -1].argmax() mask_largest = (labels == row_idx_largest + 1) # +1: we cropped background row x, y, w, h, _ = stats[row_idx_largest] # Fill everything outside the largest region with white, then crop image[~mask_largest] = 255 cropped_image = image[y:y+h, x:x+w] cv2.imwrite(out_path, cropped_image)

Resulting image:

cropping result

No comments yet.

Answer by StarlitAstronaut282 1 month ago

0

You can solve the problem by doing sort of "raycasting". To find the top edge you would measure where the topmost non-white pixel is in the first column. Then repeat for the second column and third etc.

enter image description here

The smallest of these values (i.e. the topmost) defines the top edge. You then repeat the process for the other sides.

Below is sample code demonstrating the idea.

PYTHON
import cv2 import numpy as np def crop_empty_spaces(image_path): # Load the image image = cv2.imread(image_path) # Convert to grayscale gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Apply a binary threshold to identify non-empty (non-white) areas _, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV) # Find contours contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # Get the bounding box of the largest contour if contours: x, y, w, h = cv2.boundingRect(contours[0]) cropped_image = image[y:y+h, x:x+w] return cropped_image else: return image # Return original if no contours found # Example usage image_path = '/mnt/data/Untitled.png' # Update with your file path cropped_image = crop_empty_spaces(image_path) cv2.imwrite('/mnt/data/cropped_output.png', cropped_image)

No comments yet.

Discussion

No comments yet.