Based on contributions by Glenn.
If the objects you need to detect are a distinct, consistent color, filtering by HSV (hue, saturation, value) is a fast and simple alternative to a full object-detection model. This how-to assumes you already know how to get an image or frame from a camera, and shows how to build a color mask and find the objects within it.
What you need
- Python with OpenCV. On Linux:
sudo apt-get install python3-opencv(orpip install opencv-python). - NumPy:
pip install numpy - An image or camera feed containing objects of a known, distinct color.
- A helper script to find the right HSV range for your object: HSV_Image_Testing.py (1.7 KB)
Steps
1. Load the image and work on a copy
import cv2
import numpy as np
image = cv2.imread("testfile.png")
image_copy = image.copy() # keep the original untouched
2. Crop to a region of interest (optional but recommended)
Restricting detection to a region of interest (ROI) speeds up processing and prevents anything outside that area from affecting your results.
# Define your ROI boundaries
min_x, max_x = 445, 1630
min_y, max_y = 0, 775
# Extract ROI
roi = image_copy[min_y:max_y, min_x:max_x]
3. Convert the ROI to HSV
hsv_image = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
4. Find the HSV range for your object’s color
Run the HSV_Image_Testing.py helper script against a sample image to interactively find the hue/saturation/value range that matches your object.
5. Build the mask
Fill in the range you found and create a binary mask that keeps only pixels inside it.
# Lower/upper bound for hue, e.g. for green-yellowish tones
hue = [0, 100]
# Lower/upper bound for saturation, to exclude very low-saturation colors
saturation = [0, 100]
# Lower/upper bound for brightness (value)
value = [100, 255]
lower_hsv = np.array([hue[0], saturation[0], value[0]], dtype=np.uint8)
upper_hsv = np.array([hue[1], saturation[1], value[1]], dtype=np.uint8)
# Create a mask that identifies the regions of the image within the HSV range
mask = cv2.inRange(hsv_image, lower_hsv, upper_hsv)
Optional: clean up small noise and holes in the mask. Test whether this actually helps your specific image before keeping it.
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
Example mask, used here to detect a plate:
6. Find contours in the mask
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
detected_blocks = []
7. Filter contours by shape and size
Loop over the contours and keep only the ones that match what you’re looking for. This example looks for roughly square blocks in a set size range, and normalizes each detection to a square centered on its bounding rectangle.
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# Convert the bounding rect to a square, centered on the original rect
side = max(w, h)
center_x = x + w // 2
center_y = y + h // 2
square_x = center_x - side // 2
square_y = center_y - side // 2
w, h = side, side
# Filter by size
if 40 <= w <= 115 and 40 <= h <= 115:
detected_blocks.append([x, y, w, h, 0]) # angle = 0 for squares
# Draw the square back onto the full (non-cropped) image
top_left = (square_x + min_x, square_y + min_y)
bottom_right = (square_x + min_x + side, square_y + min_y + side)
cv2.rectangle(image_copy, top_left, bottom_right, (0, 255, 0), 2)
The objects that passed the filter are now in detected_blocks, each entry holding [x, y, w, h, angle] in full-image coordinates.
Troubleshooting
- No detections at all: double-check your HSV range with the testing script — lighting changes can shift hue/saturation more than expected.
- Too many false positives: narrow the saturation and value ranges first (low-saturation, near-white/near-black areas are the most common source of noise), before tightening the hue range.
- Detected boxes look noisy or fragmented: try the optional morphological open/close step in step 5.
Related
Rewritten and consolidated (Sept 2026) from the original student how-to’s: Detect objects using HSV.
