System designHow-to

OpenCV polygon mask: crop with a transparent background

Build a polygon mask with OpenCV, composite camera frames onto a background without black borders, and keep everything outside the polygon transparent.

LumaSpec Vision9 min read Share on LinkedIn

On this page 11 sections
  1. 01What you need
  2. 02Build the polygon mask
  3. 03Composite without a black border
  4. 04Soft edges between two views
  5. 05Verify the composite
  6. 06The numbers: a trapezoid on a full-HD frame
  7. 07What four cameras cost
  8. 08Size the cameras to the canvas
  9. 09Common mistakes
  10. 10Where to go next
  11. 11Questions

A surround-view monitor or any multi-camera composite needs more than rectangular crops: each camera's usable region is a trapezoid, and everything outside it must show the canvas behind it, not black. This guide shows you how to draw a polygon mask with OpenCV in Python and composite each camera frame onto a background, so the area outside the polygon is genuinely transparent on screen and in a saved PNG. The worked example runs a four-point trapezoid on a 1920 × 1080 frame with the arithmetic shown, and the closing sections size the cameras and the links behind the canvas.

What you need

Everything below runs on plain Python with OpenCV; numpy comes with it. You need four things:

  • Each camera frame as an 8-bit, three-channel image — the CV_8UC3 layout — held in a numpy array of shape (1080, 1920, 3).
  • A background canvas of exactly the same height and width, which the views sit on.
  • The polygon vertices as integer pixel coordinates, one (x, y) pair per corner, taken in order around the shape.
  • Two drawing calls: cv2.fillPoly paints the filled polygon into the mask, and cv2.polylines draws its outline when you want to see the region on a live image.

If the region is a plain rectangle, skip the polygon and use the axis-aligned crop in crop an image ROI when the corner coordinates are unknown. Polygons earn their keep for trapezoid and keystone views, cut-outs around instruments, and curved display panels.

Build the polygon mask

Work in this order:

  1. Declare the frame and the background as uint8 arrays of shape (1080, 1920, 3). In the code below they start as zeros and stand in for real images.
  2. Declare the mask as a single-channel uint8 array of the same height and width, zero everywhere.
  3. Fill the polygon with 255. The vertices go in as an int32 array of shape (1, 4, 2): one polygon, four points, x before y.
import cv2
import numpy as np

src  = np.zeros((1080, 1920, 3), dtype=np.uint8)   # camera frame, 3 channels
bg   = np.zeros((1080, 1920, 3), dtype=np.uint8)   # background canvas, same shape

pts  = np.array([[[380, 1079], [760, 300], [1160, 300], [1540, 1079]]], dtype=np.int32)
mask = np.zeros((1080, 1920), dtype=np.uint8)
cv2.fillPoly(mask, pts, 255)     # 255 inside the trapezoid, 0 everywhere else

Keep the mask exactly the size of the frame. A smaller mask shifts every vertex by its offset, and the polygon lands in the wrong place.

Composite without a black border

The black ring appears when the destination starts as zeros: an AND of frame and mask, or a cropped polygon pasted onto a new black image, leaves every outside pixel at 0. Make the background the base instead, and let the mask decide which pixels the camera replaces.

  1. Copy the background into the output image.
  2. Copy the frame into that copy, through the mask, so only polygon pixels move.
dst = bg.copy()
src.copyTo(dst, mask)            # copyTo works from Python: only masked pixels land in dst

Written per pixel, the same operation is a blend, and it shows why no border can appear:

dst = src × m + bg × (1 − m)

The two weights sum to 1 at every pixel. Where m = 255 the camera pixel wins; where m = 0 the background shows; the background is always present, so nothing turns black. In code, do the sum in float and cast back once:

a   = mask.astype(np.float32) / 255.0            # 1.0 inside, 0.0 outside
dst = (src.astype(np.float32) * a[:, :, None]
       + bg.astype(np.float32) * (1.0 - a[:, :, None])).astype(np.uint8)

For a fixed, image-wide opacity — one view floating over the canvas at 70 percent — cv2.addWeighted(src, 0.7, bg, 0.3, 0) is the one-call, saturating form. A per-pixel shape needs the masked version above.

Soft edges between two views

Adjacent trapezoids meet in a hard seam. Apply one of OpenCV's blur filters to the 0/255 mask with a 5 × 5 kernel before dividing by 255: the edge becomes a ramp from 0 to 1 instead of a step. Widen each polygon by about half the kernel so neighboring ramps overlap; across the overlap the weights still sum to 1 and the seam disappears on screen. Keep a separate, unblurred mask for any measurement: a 5-pixel ramp softens real edges by 5 pixels, and geometry inspection does not want that.

Verify the composite

Set one marker pixel on each side and prove the composite before you trust it on live frames:

src[540, 960] = (20, 200, 90)     # marker inside the trapezoid
bg[540, 100]  = (90, 90, 90)      # marker outside it

dst = bg.copy()
src.copyTo(dst, mask)             # composite again so the markers are included

assert tuple(dst[540, 960]) == (20, 200, 90)   # inside: camera pixel survived
assert tuple(dst[540, 100]) == (90, 90, 90)    # outside: background survived
print(np.count_nonzero(mask))     # about 610000: area plus the drawn boundary

At y = 540 the left edge of this trapezoid sits at x ≈ 643 and the right edge at x ≈ 1277, so x = 100 is safely outside and x = 960 safely inside; probes well clear of the boundary avoid edge-pixel ambiguity. While you tune the shape, draw the outline over the live image with cv2.polylines and choose the line width deliberately, as in machine vision overlay line thickness: draw it yourself.

The numbers: a trapezoid on a full-HD frame

The trapezoid in the code spans 1160 px along the bottom, 400 px along the top and 779 px in height:

A = (w_bottom + w_top) / 2 × h = (1160 + 400) / 2 × 779 = 607,620 px kept = 607,620 / (1920 × 1080) = 607,620 / 2,073,600 = 29.3 %

The composite keeps 29.3 percent of this frame and hands the other 70.7 percent to the canvas. Memory follows the same count: a 1920 × 1080 three-channel frame is 1920 × 1080 × 3 = 6,220,800 bytes ≈ 6.22 MB, the single-channel mask is 2,073,600 bytes ≈ 2.07 MB, and each float32 copy of the frame is 24.9 MB. The blend holds about 100 MB of float32 temporaries per view, so allocate them once and reuse them at frame rate.

What four cameras cost

Bandwidth, not CPU, is the constraint. One full-HD view at 30 fps is 6.22 MB × 30 = 186.6 MB/s, about 1.5 Gbit/s — more than one GigE link carries (about 118 MB/s of payload). Four views as full frames are 746 MB/s, which is why the pipeline wants USB3-class links or cameras that send only what you need.

Cropping to the polygon's bounding box on the camera cuts this hard. The trapezoid spans x 380 to 1540 and y 300 to 1079, a 1160 × 779 box of 903,640 pixels — 43.6 percent of the frame. At 3 bytes per pixel that is 2.71 MB per frame, or 81 MB/s at 30 fps, and one GigE link carries it with room to spare. Set the camera ROI to that box, transfer, then apply the polygon mask on the PC. The acquisition options are compared in ROI vs binning vs subsampling: choosing faster acquisition, the frame-rate accounting is in Multi ROI camera capture: the frame rate arithmetic, and the long-run link choices in 5GBASE-T camera interface: bandwidth, cables and distance.

CPU load stays small. The blend is about three operations on each of 6.22 million values, roughly 19 MFLOP per view, so four views at 30 fps ask for about 2.2 GFLOP/s — a desktop CPU does that alongside the rest of the pipeline.

Size the cameras to the canvas

Work backwards from the canvas. A camera that must fill a 1920 × 1080 tile one-to-one needs 1920 × 1080 = 2,073,600 pixels, about 2.1 MP. If you dewarp each view — perspective-correcting a trapezoid costs sharpness, so budget 2× in each axis — the requirement rises fourfold to about 8.3 MP. That one decision separates the 2 MP class from the 12 MP class, and the calculator below runs it for your own tile size and pixel density.

Camera resolution for the smallest feature
Units
Pixels needed
–
Sensor size
– MP
Smallest sensor that fits
–
Pixel on the part
– mm
Pixels across the feature, that sensor
–

Pixels needed = field / feature × pixels per feature on each axis (field incl. margin). The lens must resolve the pixels too: Nyquist = 1000 / (2 × pixel µm) lp/mm.

Find cameras with enough pixels Open the full calculator

The 1:1 tile needs 2.1 MP. The MV-CU020-90GC is a 2.5 MP color camera with a 1/1.7" sensor, global shutter and C-mount on GigE; global shutter matters because a rolling shutter skews the frame whenever the scene moves.

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

Dewarping with 2× headroom needs 8.3 MP per view. The MV-CH120-11UC is a 12 MP color USB3 camera with a 1.1" sensor, global shutter and C-mount, built as the high-speed version in its series.

Area scan camerasHikrobot MV-CH120-11UC 12 MP Area Scan Camera, USB3, IMX304, High-speed version, ColorMV-CH120-11UC
Sensor format
1.1"
Lens mount
C-mount
Interface
USB3
Resolution
12 MP
$2,339.00 · quotedView and quoteBuild a system with it

Shortlist by resolution, shutter and mount in the camera finder. The PC-connected models sit in the area scan cameras category; when each polygon is an inspection region rather than a display tile, the smart cameras run the decision on the device without a PC. Fit camera, lens and link together in the configurator:

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

Common mistakes

  • Black ring around the polygon: the output started as zeros. Start from the background, as in step 4, or blend with the (1 − m) term.
  • Polygon in the wrong place: the mask is smaller than the frame, or the vertices were built for a cropped image. Keep mask and frame the same size and the same origin.
  • Fill rejected or misplaced: the fill call takes integer pixel coordinates, so round calibration output to int32 before filling.
  • Probes on the boundary: the fill includes the border, so test pixels well inside and well outside, as in the verification section.
  • Transparency gone after saving: JPEG and BMP carry no alpha channel. Write PNG, reload, and confirm the image has four channels.
  • Dropped frames with four cameras: full frames exceed the link. Crop to each polygon's bounding box on the camera first, as computed above.

Where to go next

Build in this order: size the cameras to the canvas, cut each camera to its bounding box, then add the polygon composite — it is the last twenty lines of the project, not the first. The software-side recipes continue on the systems topic page, and if the canvas feeds a controller display, smart camera image in a PLC HMI: what actually displays live covers that display path. Contact us when you want help matching cameras and links to your canvas.

Questions

Why is the area outside my polygon black instead of transparent?

The mask worked; the canvas underneath did not. If the destination image starts as zeros — an AND of frame and mask, or a cropped polygon pasted onto a new black image — every pixel outside the polygon stays 0. Start from the background image and overwrite only the polygon, or blend with the (1 − mask) term so the background fills the rest.

Can one mask hold several polygons?

Yes. Fill each polygon with 255 into the same single-channel mask, one fill call per polygon or several vertex arrays in one call, and all filled regions composite together. Overlaps overwrite each other at 255, so the union of the regions shows camera pixels. Give each camera its own mask when the regions need different feathering.

How do I get smooth edges instead of stair-stepping?

The fill functions draw hard edges. Blur the 0/255 mask with a 3 to 5 pixel kernel before dividing by 255, or render the mask at four times the resolution and downsample it; both produce intermediate values that the blend turns into a gradual transition. The kernel width sets the transition width, so 3 pixels keeps the region tight.

Does the transparency survive saving the composite to a file?

Only in formats that store per-pixel alpha. Pack the mask as a fourth channel and write a PNG, which keeps 8-bit transparency; a viewer or HMI shows the canvas through the unmasked area. JPEG and BMP carry no alpha channel, so the file comes back opaque. Confirm by reloading and checking the image has four channels.