System designHow-to

ROI pixel coordinate extraction: list every point inside the region

Convert an ROI to a binary mask, scan its bounding box once and record every pixel coordinate. Extraction loop, raster order and timing for a 12 MP frame.

LumaSpec Vision10 min read Share on LinkedIn

On this page 11 sections
  1. 01Raster order and the definition of inside
  2. 02What you need
  3. 03The method in three steps
  4. 04Step 1: convert the ROI to a binary mask
  5. 05Step 2: walk the bounding box, not the whole image
  6. 06Step 3: record coordinates and keep the types right
  7. 07What the pass costs
  8. 08How to verify the list
  9. 09Common mistakes and their symptoms
  10. 10When you should not build the list
  11. 11Questions

You have a region of interest on the image — a rectangle from a locator, an ellipse you drew in the setup tool, a polygon around a gasket — and the next stage of your code needs the pixels inside it: the actual points, not the bounding box. No mainstream vision library ships one call that returns that list. The technique is the same everywhere: convert the ROI to a binary mask, walk the image buffer once, and record the coordinate of every pixel whose mask value is non-zero. This guide gives you the loop, the scan order, the cost in milliseconds and the tests that prove the list is right.

Raster order and the definition of inside

Top-left to bottom-right means raster order, the order you read text: every point of row y0 from left to right, then row y0 + 1, and so on to the bottom row. The sort key is row, then column. Two properties follow for free: the list is already sorted, so downstream searches need no sort step, and the first point is the top-most-then-left-most pixel of the shape while the last is the bottom-most-then-right-most.

A pixel counts as inside when the mask says so. The mask is the library's rasterization of your shape — an ellipse reduced to per-pixel decisions — and there is no second, truer definition hiding behind it. Libraries stop at the mask because a mask is one byte per pixel while a list is eight bytes per point, and most consumers — blob tools, fitters, statistics — take the mask directly anyway.

What you need

  • The image width, height and row stride, read from the buffer header as integers.
  • The ROI descriptor, or the ROI-to-mask call your library provides; every mainstream library has one, and it returns an 8-bit buffer of zeros and non-zeros — some write 1 inside the shape, some 255.
  • An output vector of integer (x, y) pairs, reserved up front.

Size the memory before you write the loop. At a 4000 × 3000 frame the mask is 12 MB as one byte per pixel, or 1.5 MB packed at one bit per pixel; the worst-case list for the whole frame is 12,000,000 points × 8 bytes ≈ 96 MB. That last number is one more reason the method walks only the ROI.

The method in three steps

  1. Convert the ROI to a binary mask with the ROI-to-mask call, in the same pixel frame as the image.
  2. Walk the ROI bounding box only, testing the mask at offset y × stride + x for each pixel.
  3. Append (x, y) for every non-zero mask byte — rows as the outer loop, columns inner — and the list lands in raster order.

The three sections below fill in the checks each step needs.

Step 1: convert the ROI to a binary mask

The conversion is one call and the mask it returns is zero outside the shape. Test against zero, never against 1 or 255: libraries differ in what they write inside the shape, and an equality test against one value silently returns an empty list on the libraries that write the other.

Check the mask before you trust it. Very small shapes rasterize to nothing: an oval two pixels across can set zero pixels, and your loop will faithfully return an empty list. Assert that the set-pixel count is greater than zero and raise with the ROI's identifier in the message, so the failure names its cause.

Step 2: walk the bounding box, not the whole image

Take the box from the ROI descriptor; if you hold only the mask, find the minimum and maximum row and column of set pixels in one pass. From then on cost scales with box area, not frame area, which is the whole performance story of this technique.

offset = y × stride + x points_in_box = (x1 - x0 + 1) × (y1 - y0 + 1)

If your frames arrive as raw camera buffers rather than image objects, convert them first — the raw-buffer to processing-ready image conversion settles pixel format and stride questions before you index anything.

A rotated shape makes the box a loose bound: a square of side s turned 45° fills a box of area (s√2)² = 2s² yet still contains only s² set pixels. Because you keep testing the mask inside the box, the loose bound costs scan time, never correctness — at 45° it doubles the scan.

Step 3: record coordinates and keep the types right

x0, y0, x1, y1 = 100, 200, 899, 799    # int, ROI bounding box, inclusive edges
stride = 4000                           # int, bytes per row, from the buffer header
mask = roi_to_mask(roi, width, height)  # int8 per pixel: 0 outside, non-zero inside
points = []                             # list of (x, y) integer tuples, raster order
for y in range(y0, y1 + 1):
    base = y * stride                   # hold this product in 64-bit in compiled code
    for x in range(x0, x1 + 1):
        if mask[base + x] != 0:
            points.append((x, y))       # y outer, x inner: raster order for free

roi_to_mask stands in for whatever your library names its ROI-to-mask call; the rest is plain code. Three conventions keep the list usable downstream:

  • Coordinates are (x, y) = (column, row), origin at the top-left, y increasing downward.
  • Do the offset arithmetic in 64-bit integers in compiled code; a signed 32-bit sum overflows above 2,147,483,647 elements, and a 46,341 × 46,341 frame — 2,147,488,281 elements — crosses that line mid-index.
  • Keep the list in pixels until the hand-off; turning pixels into millimetres is a calibration step done on the points you keep, and mapping pixel coordinates into a warped image with a homography covers the transform side.

In a dataflow environment the identical walk applies: the guide on locating image coordinates from a 2D pixel array shows the array-index form of the same scan.

What the pass costs

The worked frame is a live 12 MP image from the area scan camera below:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

The camera in the card streams 4000 × 3000 frames at 30 fps over USB3. At 30 fps the frame period is 1/30 s ≈ 33 ms, and the arithmetic below spends that budget.

Budget the scan at 1-2 ns per pixel for a compiled scalar loop: three to six machine instructions per mask byte at a 3 GHz clock, with rows already in cache. Auto-vectorization cuts that several-fold, so treat 2 ns per pixel as the safe planning number.

  • Full-image pass of 12,000,000 pixels: 12,000,000 × 1 ns = 12 ms; at 2 ns, 24 ms. Against the 33 ms frame period that is 36-72 % of the budget, every frame.
  • ROI box of 800 × 600: 480,000 pixels → 0.48 ms at 1 ns and 0.96 ms at 2 ns, which is 2.9 % of the frame period at the pessimistic rate.
  • Output vector, 800 × 600 box fully filled: 480,000 × 8 bytes = 3.8 MB — reserve it once and reuse it each frame.

So the decision writes itself: walk the box inline every frame, and keep the full-image pass for set-up, offline analysis, or frames where the ROI genuinely covers the sensor. The scan is single-core host work that repeats next to your inspection logic, so it belongs on the machine that owns the cameras. The controller below pairs an Intel Core i3 processor with 8 GB of memory and four 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
Light outputs
4 built-in channels
Memory
8 GB
$2,419.00View partBuild a system with it

The rest of the range sits in the vision controllers category.

How to verify the list

Prove the walk on shapes whose answers you know before pointing it at parts:

  1. Rectangle ROI of 200 × 100 pixels: expect exactly 200 × 100 = 20,000 points, first point (x0, y0), last point (x0 + 199, y0 + 99). Assert all three.
  2. Ellipse ROI of 200 × 100: the mask holds roughly π/4 × 200 × 100 ≈ 15,708 points, within a few hundred of that either way — assert a band of ±250 around the estimate.
  3. Sortedness: for every consecutive pair, assert that (y, x) never decreases.
  4. Round-trip: paint the recorded points into a blank buffer and compare it byte for byte with the mask.
  5. Watch it live: running a binary image preview during inspection puts the mask on the live stream, where a rasterization surprise is visible before it costs a shift.

Common mistakes and their symptoms

Mistake Symptom Fix
Testing the mask against 1 or 255 Empty list while the ROI draws fine Test != 0; inside-values differ per library
Taking stride = width Points drift right row by row, diagonal shear Read the stride from the buffer header; padded rows are normal — a 4001-pixel row often occupies 4004 bytes
Walking the whole frame each cycle 12-24 ms per frame gone at 12 MP Walk the ROI box instead: 0.5-1 ms at the sizes above
Mask built in a bottom-left-origin frame List mirrored vertically against the image Build the mask in the image's frame, or flip y once at creation
32-bit offset arithmetic Negative indices or access violations on large frames Hold offsets in 64-bit integers
Growing the output vector per append Frame-time spikes from reallocation Reserve box area × 8 bytes once and reuse per frame

If the points land on the wrong part rather than in the wrong place, the ROI is usually bound to a different image source than the one inspected; the ROI source-binding fix walks that diagnosis.

When you should not build the list

A coordinate list is the most expensive representation of a region; build it only when the next stage truly iterates points.

If you are sizing hardware around this loop, the camera finder filters area scan cameras by resolution and interface, the camera configurator walks the chain from sensor to controller, and the contact page gets a proposed build checked against your frame rate. More buffer-level techniques sit on the systems topic page.

Questions

Do I need a mask for a plain axis-aligned rectangle?

No. For a rectangle the inside test is arithmetic: loop rows y0 to y1 and columns x0 to x1 and emit every point, with no mask call at all. You only need the ROI-to-mask step for shapes a single comparison cannot express: ellipses, rotated rectangles, polygons, annuli. Even then, a rotated rectangle is often worth handling as a rotated coordinate test instead of a mask, which skips the extra buffer.

What changes for 16-bit or color images?

The coordinates do not change, because you walk the mask, not the image, so pixel format is irrelevant to the point list. It matters only for the stride: a 4000-column row is 4000 bytes in 8-bit mono and 8000 bytes in 16-bit mono, while a 4001-column RGB row needs 12003 bytes and often gets 12004. Take the stride from the buffer header and the loop is unchanged.

How do I convert the pixel coordinates into millimetres?

The scan stays in pixels; convert at the boundary. For a camera looking straight down at a known working distance, one scale factor from a calibration target converts both axes. When the part can tilt or the camera sits at an angle, a homography maps pixel coordinates into world or warped-image coordinates per point. Do this after the walk, on the points you actually keep, not inside the loop.

How much memory does the output list need?

Eight bytes per point for a pair of 32-bit integers. An 800 × 600 ROI fully filled is 480,000 points, about 3.8 MB, and a full 4000 × 3000 frame is 96 MB. Reserve the vector once at the ROI bounding-box area so the append path never reallocates, and reuse the same buffer frame to frame instead of freeing it.

Can I split the scan across threads?

Yes, cleanly: give each thread a disjoint block of rows, build a per-thread list in raster order, then concatenate the lists in row order — the merged list is identical to the single-threaded one. Expect near-linear speedup until memory bandwidth saturates. At the sizes in this guide a single pass already finishes in about a millisecond, so add threads only when ROIs cover most of a high-resolution frame.