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:
- 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.
- Declare the mask as a single-channel uint8 array of the same height and width, zero everywhere.
- 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.
- Copy the background into the output image.
- 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:
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:
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.
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.
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
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.
- Sensor format
- 1.1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 12 MP
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:
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.
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.

