Morphological operations reshape the white foreground regions in a binary image mask. In OpenCV, they are useful after thresholding or color masking when small specks, thin gaps, or tiny holes would confuse contour detection, measurement, or later segmentation work.
OpenCV exposes cv2.erode() and cv2.dilate() for the primitive operations, while cv2.morphologyEx() applies combined operations such as opening and closing. Opening erodes and then dilates, which removes small white foreground noise. Closing dilates and then erodes, which fills small black gaps inside the foreground.
The script treats every nonzero input pixel as white foreground, applies one selected operation, writes a new mask, and prints foreground counts before and after the change. Start from a mask where the object or region to keep is white and the background is black; invert the threshold first when the source mask has the opposite polarity.
Related: How to threshold an image with OpenCV
Related: How to blur an image with OpenCV
Related: How to find contours with OpenCV
Steps to apply morphology to an image with OpenCV:
- Place the binary mask at input/mask.png.
The mask should use 0 for background and 255 for foreground. Use cv2.THRESH_BINARY_INV or invert the mask first when the object is black on a white background.
- Create apply_morphology.py.
- apply_morphology.py
#!/usr/bin/env python3 import argparse from pathlib import Path import cv2 import numpy as np def odd_positive(value: str) -> int: size = int(value) if size < 1 or size % 2 == 0: raise argparse.ArgumentTypeError("kernel size must be a positive odd integer") return size parser = argparse.ArgumentParser(description="Apply an OpenCV morphology operation to a binary mask.") parser.add_argument("input_mask", type=Path) parser.add_argument("output_mask", type=Path) parser.add_argument("--operation", choices=("erode", "dilate", "open", "close"), default="open") parser.add_argument("--kernel-size", type=odd_positive, default=5) parser.add_argument("--kernel-shape", choices=("rect", "ellipse", "cross"), default="ellipse") parser.add_argument("--iterations", type=int, default=1) args = parser.parse_args() gray = cv2.imread(str(args.input_mask), cv2.IMREAD_GRAYSCALE) if gray is None: raise SystemExit(f"could not read mask: {args.input_mask}") binary = np.where(gray > 0, 255, 0).astype(np.uint8) shape_map = { "rect": cv2.MORPH_RECT, "ellipse": cv2.MORPH_ELLIPSE, "cross": cv2.MORPH_CROSS, } kernel = cv2.getStructuringElement( shape_map[args.kernel_shape], (args.kernel_size, args.kernel_size), ) if args.operation == "erode": result = cv2.erode(binary, kernel, iterations=args.iterations) elif args.operation == "dilate": result = cv2.dilate(binary, kernel, iterations=args.iterations) else: operation_map = { "open": cv2.MORPH_OPEN, "close": cv2.MORPH_CLOSE, } result = cv2.morphologyEx( binary, operation_map[args.operation], kernel, iterations=args.iterations, ) args.output_mask.parent.mkdir(parents=True, exist_ok=True) if not cv2.imwrite(str(args.output_mask), result): raise SystemExit(f"could not write mask: {args.output_mask}") foreground_before = int(np.count_nonzero(binary == 255)) foreground_after = int(np.count_nonzero(result == 255)) changed_pixels = int(np.count_nonzero(binary != result)) total_pixels = int(binary.size) unique_values = " ".join(str(int(value)) for value in np.unique(result)) print(f"operation: {args.operation}") print(f"kernel: {args.kernel_shape} {args.kernel_size}x{args.kernel_size}") print(f"iterations: {args.iterations}") print(f"foreground before: {foreground_before} / {total_pixels} ({foreground_before / total_pixels * 100:.2f}%)") print(f"foreground after: {foreground_after} / {total_pixels} ({foreground_after / total_pixels * 100:.2f}%)") print(f"changed pixels: {changed_pixels}") print(f"unique values: {unique_values}") print(f"output: {args.output_mask}")
- Run an opening operation to remove small foreground specks.
$ python3 apply_morphology.py input/mask.png output/mask-open.png --operation open --kernel-size 7 operation: open kernel: ellipse 7x7 iterations: 1 foreground before: 69375 / 345600 (20.07%) foreground after: 69115 / 345600 (20.00%) changed pixels: 260 unique values: 0 255 output: output/mask-open.png
Opening removes foreground components that are smaller than the structuring element. Increase --kernel-size only when the noise is larger than the details that should remain.
- Run a closing operation and verify that the saved mask remains binary.
$ python3 apply_morphology.py input/mask.png output/mask-close.png --operation close --kernel-size 11 operation: close kernel: ellipse 11x11 iterations: 1 foreground before: 69375 / 345600 (20.07%) foreground after: 69637 / 345600 (20.15%) changed pixels: 262 unique values: 0 255 output: output/mask-close.png
Closing adds foreground pixels where dilation can bridge a gap and erosion can restore the outer boundary. Use a smaller kernel when separate objects start merging. Use --operation erode or --operation dilate only when shrinking or expanding the foreground is the intended result.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.