System designHow-to

Zero pixels outside ROI without a per-pixel loop

An ROI bounds operations but clears nothing. Zero the pixels outside the ROI with bulk calls; inside are the arithmetic, a mask recipe and a test proving it.

LumaSpec Vision10 min read Share on LinkedIn

On this page 12 sections
  1. 01What an ROI actually does
  2. 02The pixel budget: a worked example
  3. 03The decision between the bulk moves
  4. 04What you need
  5. 05Zero outside one rectangular ROI
  6. 06Several ROIs or a shaped region
  7. 07Prove the border is zero
  8. 08Common mistakes
  9. 09When you do not need to mask at all
  10. 10Where the stage runs: picking the controller
  11. 11Checks before you ship
  12. 12Questions

You found the part, drew the rectangle, and the inspection runs inside it. The saved image, the viewer window and the video encoder still show everything around it: conveyor, glare, last frame's ghost. The instinctive fix is a nested loop that walks the frame byte by byte. Resist it. That loop is the slowest correct way to black a border. This guide gives the three bulk moves every vision library offers, the pixel arithmetic that picks between them, steps for one rectangle and for several ROIs, and a two-line test that proves the border is zero.

What an ROI actually does

Setting an ROI points the next operations at a sub-rectangle. It bounds where a filter, a threshold or a measurement acts, and it writes nothing. Release it and every pixel outside still holds exactly what it held before. On a live stream that means the border keeps the previous frame's content, and that stale band rides along into saved images, viewer windows and encoders.

That is also why the per-pixel loop feels heavy. On a 640 × 480 8-bit BGR frame a full scan performs 307,200 rectangle tests and up to 921,600 single-byte channel writes: work proportional to the whole frame, not to the 200 × 150 region you care about. The fix is not a faster loop but a different shape of operation. The same logic that lets an ROI bound your preprocessing operators applies here: choose moves that act on blocks, not on pixels.

The pixel budget: a worked example

Take the case you will meet most often: a 640 × 480 frame, 8 bits per channel, 3 channels, and an ROI at x = 100, y = 80, sized 200 × 150.

frame_px = W·H = 640 × 480 = 307,200 roi_px = w·h = 200 × 150 = 30,000 outside_px = 307,200 - 30,000 = 277,200 bytes = pixels × 3 channels

The outside pixels split into four rectangles, and the split always sums back to W·H − w·h:

Band Size Pixels Bytes (×3)
Top 640 × 80 51,200 153,600
Bottom 640 × 250 160,000 480,000
Left 100 × 150 15,000 45,000
Right 340 × 150 51,000 153,000
Outside total — 277,200 831,600

The naive loop writes all 921,600 channel bytes if it touches every pixel, or 831,600 if it tests the rectangle first, and it adds 307,200 branches and address computations either way. The four-band fill writes the same 831,600 border bytes in four vectorized calls with no test at all. On DDR4-class memory, which streams 10-25 GB/s per core, that fill runs in 33-83 µs. At a 60 fps camera the entire frame budget is 16.7 ms, so every route below fits; the point of the arithmetic is which route leaves the most margin for detection.

The decision between the bulk moves

Count bytes and pick the smaller job. Copying the inside moves w·h × 3 = 90,000 B; clearing the outside moves (W·H − w·h) × 3 = 831,600 B. Here the copy wins by 9.2×.

Move Traffic per frame Tests or branches Extra memory
Per-pixel loop (baseline) 921,600 B written, or 831,600 B with a test first 307,200 tests none
Copy ROI into a reused zero canvas 180,000 B steady state (90,000 copy + 90,000 re-clear) none 921,600 B canvas, allocated once
Fill the four outside bands 831,600 B none none
Masked copy through a 255/0 mask 90,000 B per pass, plus a one-off mask build of 337,200 B none 307,200 B mask + 921,600 B canvas

Use the canvas route when you control the pipeline's destination image, the band fill when you must black the border on a copy of the full frame, and the mask when the region is several rectangles or not rectangular at all.

What you need

  • The frame as an image object that reports its size and channel count: 640 × 480, 8-bit, 3 channels in the example.
  • The ROI as x, y, width and height in pixels, from a fixture or from your detection stage.
  • Three library primitives: a bulk zero fill, a rectangle copy or fill, and a masked copy or clear. The snippets below use OpenCV's spelling: cv::Mat, cv::Rect, copyTo, setTo and cv::countNonZero.
  • One reusable canvas, plus one mask if you take the mask route.

Zero outside one rectangular ROI

  1. Create the destination once, the same size and type as the frame, all zeros. One call fills 921,600 B; keep it for the pipeline's lifetime.
  2. Copy just the rectangle into it. This moves 30,000 px × 3 B = 90,000 B and touches nothing else — no mask, no branches.
  3. When the ROI moves between frames, re-zero the previous rectangle first, another 90,000 B. Steady-state traffic is 180,000 B, 4.6× less than the band fill.
  4. Skip the mask for a plain rectangle: building one costs 307,200 B of memory and a 337,200 B build pass and buys nothing here.
// src: 8-bit 3-channel frame, 640 × 480; dst is created once and reused
cv::Mat dst = cv::Mat::zeros(src.size(), src.type()); // one bulk fill
cv::Rect roi(100, 80, 200, 150);                      // x, y, width, height
src(roi).copyTo(dst(roi));                            // moves only the inside bytes

Several ROIs or a shaped region

  1. Build one 8-bit single-channel mask, frame-sized, all zeros: 307,200 B here. Masks follow the 255-inside / 0-outside convention.
  2. Fill every ROI rectangle with 255. Draw a polygon or contour into the mask instead when the region is not rectangular.
  3. Zero the destination canvas once and reuse it, as above.
  4. Run one masked copy. A masked copy writes only pixels whose mask byte is non-zero, so all rectangles transfer in a single pass over their combined 30,000 inside pixels, whatever their count.
  5. When the regions together cover more than half the frame, invert the mask once and clear through it instead: that writes the 277,200 outside pixels and skips the inside.
// rois: std::vector<cv::Rect>, filled by your detection stage
cv::Mat mask = cv::Mat::zeros(src.size(), CV_8UC1);    // 0 everywhere
for (const cv::Rect& r : rois) { mask(r).setTo(255); } // 255 inside each ROI
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());  // zeroed once, reused
src.copyTo(dst, mask);                                 // one pass for every ROI

Band fills scale as four per rectangle and can wipe a neighbour's content when rectangles overlap; the mask does any number in one copy. In the classic C API the same move is a masked cvCopy onto an image zeroed with cvSetZero.

Prove the border is zero

Two lines settle it on live frames, not just on a still:

cv::Mat outside = dst.clone();           // test-only copy
outside(roi).setTo(0);                   // remove the ROI, keep only the border
assert(cv::countNonZero(outside) == 0);  // passes only if every border pixel is 0

Cheaper spot-checks for debug builds: pixel (0, 0) and pixel (639, 479) must read 0, and pixel (100, 80) must equal the source pixel there. Run the full count at commissioning and after every line change; leave the spot-checks in debug.

Common mistakes

  • Trusting the ROI call to clear. It bounds; it never writes zeros, so the stale border rides into saves and encoders.
  • A destination that was never zeroed. A masked copy writes only inside pixels, so whatever the destination held shows outside.
  • A mask of the wrong type. Masks are 8-bit single channel, 255 inside and 0 outside; a 3-channel image in that slot fails or masks garbage.
  • Loop bounds that stop at the ROI edges. A loop that descends from the frame edge to the ROI edge clears two of the four bands and leaves the top and left bands stale.
  • Budgeting pixels instead of bytes. Three channels triple the traffic: 307,200 px is 921,600 B. The same trap in other pixel formats is in why your 16-bit image buffer has half the expected pixels.

When you do not need to mask at all

If the inspection itself only needs the rectangle, crop once and pass the crop downstream: 30,000 px instead of 307,200, so every later stage runs on 9.8% of the frame. Masking serves outputs — displays, saved images, encoders — not processing speed.

When the region is fixed in the frame, push the crop upstream into the camera. A sensor ROI makes the camera transmit only that rectangle: at 60 fps the 640 × 480 BGR stream is 921,600 B × 60 ≈ 55.3 MB/s, about half of GigE's practical ceiling of roughly 118 MB/s, while a 200 × 150 ROI is 90,000 × 60 = 5.4 MB/s, a tenth of the traffic. Where the ROI belongs, camera or software, is worked through in 2D vs 3D camera ROI: where to set it and which one wins, and the frame-rate side of the trade in multi ROI camera capture: the frame rate arithmetic. Pixel format moves bytes per frame as much as resolution does; that arithmetic is in RGB camera frame rate limits: pixel format and exposure.

When the region moves with the part, detect on the full frame and mask in software with the steps above.

GigE and USB3 area scan cameras expose the sensor ROI among their acquisition settings, so the camera itself ships only the rectangle you set.

Area scan camerasHikrobot MV-CT0MG0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., GigE, Mono, BASEMV-CT0MG0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
GigE
Resolution
6-10 MP
$909.00View partBuild a system with it

Where the stage runs: picking the controller

The masking stage lives on the vision controllers side of the build, and its class follows camera count and frame size, not the masking arithmetic, which is tens of microseconds on any current processor. One or two cameras at VGA-class frame rates leave the margin to detection and communication, and a fanless box fits.

Vision controllersHikrobot MV-VC2140-128G40-1T VC2100 Vision Controller, N97, 8GB+128G SSD+ 1THDDMV-VC2140-128G40-1T
LAN ports
3-4 LAN ports
USB 3 ports
2 USB 3 ports
Light outputs
4 built-in channels
Memory
8 GB
$1,679.00View partBuild a system with it

The MV-VC2140-128G40-1T pairs an Intel N-series processor with 8 GB of memory, 3-4 LAN ports, 2 USB 3 ports and 4 built-in light channels, so it also drives the light directly. Step up when frames grow to 12 MP or several cameras feed one pipeline. The MV-VC3301H-128G66 runs an Intel Core i3 with 8 GB of memory and 5+ LAN ports, and the MV-VC5525-256G70-NN runs an Intel Core i5 with 32 GB of memory, PoE camera ports and 8 built-in light channels.

Vision controllersHikrobot MV-VC3301H-128G66 I3-8100, 8GB+128G SSD, No PoE, PCIe expansion supported, Incl. VM6200Pro licenseMV-VC3301H-128G66
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
$2,419.00View part
Vision controllersHikrobot MV-VC5525-256G70-NN VC5000 Vision Controller, 12th-I5, 32GB+256G SSD, Supported PCIEMV-VC5525-256G70-NN
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
$4,479.00 · quotedView and quote

Budget rule: at 60 fps the frame is 16.7 ms, the bulk moves need tens of microseconds, so hold the masking stage under a tenth of a millisecond and give the rest to detection. Size the box with the system configurator or compare models in the vision controller category.

Checks before you ship

  • Run the full countNonZero border test on live frames at commissioning and after every line change; a still image says nothing about a moving ROI.
  • Spot-check pixels (0, 0), (639, 479) and (100, 80) in debug builds.
  • Recompute the budget for your frame: outside_px = W·H − w·h, then bytes = outside_px × channels.
  • Keep the canvas and mask allocated once and re-zero only the band you wrote last frame.
  • Region fixed? Set the sensor ROI and drop the masking stage entirely.

The buffer-level habits around this stage — allocation, overlays, thresholds, preview quirks — collect on the systems topic page. To pair cameras with the box that runs them, use the camera finder next to the vision controller category, and for a specific frame rate or camera count, the contact page reaches the applications team.

Questions

Does setting an ROI delete the pixels outside it?

No. An ROI only bounds where an operator acts; the border pixels keep their values. Anything written while the ROI is active lands inside the rectangle, and the rest of the buffer still holds the previous frame. To black the border you write zeros there yourself: a band fill, a masked clear, or a copy into a zeroed canvas.

Which pixels does a masked copy write?

Only pixels whose mask value is non-zero, and any non-zero value qualifies, not just 255. Build the mask 8-bit single-channel at frame size, 255 inside the region and 0 outside, and one masked copy transfers the rectangle alone. The untouched border keeps whatever the destination held, which is why the destination must be zeroed first.

Should I copy the ROI or clear the outside?

Whichever writes fewer bytes. Compare w·h × channels against (W·H − w·h) × channels and the rule falls out: copy the ROI while it covers less than half the frame, invert and clear when it covers more. In the worked example that is 90,000 B copied against 831,600 B cleared, so the copy wins by far.

Does blacking the background make my inspection faster?

No; it adds writes. Speed comes from giving later stages fewer pixels: crop to the ROI or set the sensor ROI. Black borders pay off in outputs instead: viewers, encoders, saved images, and the training sets you collect, where a clean border stops a model from learning conveyor texture as a feature.

Can the camera send only the region of interest?

Yes, when the region is fixed in the frame. The sensor ROI settings (width, height and x-y offset) sit among the camera's acquisition features, and the transmitted frame becomes the rectangle itself, so re-check lens coverage and calibration for the smaller field. At 60 fps a 640 × 480 BGR stream is about 55.3 MB/s; a 200 × 150 ROI is about 5.4 MB/s.