When the pixels you need to process live inside a GUI control — a legacy viewer pane, an HMI render, a TImage on an old C++ Builder panel — the obvious route is to save the bitmap to a file and read it back with cvLoadImage or imread. That works, and plenty of production panels still do it, but it is the slowest path available: file write, encode, file read, decode, on every frame. The in-memory route is one call. Grab the control's device-independent bitmap with GetDIBits into a buffer you own, then wrap that buffer with a cv::Mat header — or with cvCreateImageHeader plus cvSetData if your code still speaks IplImage.
This guide walks through the working sequence, the stride arithmetic, the two release traps, and a pixel-for-pixel test that proves the wrapped result matches the old file route before you delete it.
Why the file round trip is the slow path
A 224 × 168 control bitmap holds 224 × 168 × 3 = 112,896 bytes of pixels, about 110 KB once the BMP header is added. Saving and reloading pays for that payload twice, plus file-system overhead and decode, on every frame:
- On a spinning drive, each random access spends roughly 10 ms in seek plus rotational latency, so a write-flush-read cycle for this file lands in the 10–50 ms range.
- On an SSD the seeks disappear, but open, write, close, read and decode still cost 1–5 ms for a file this size.
- Encoded formats push it further: saving PNG adds compression on top of the same I/O.
GetDIBits into a buffer you already own is a memory transfer instead. At the multi-GB/s bandwidth of any current machine, 112,896 bytes moves in microseconds and the whole call stays under 1 ms. At 10 fps, the file route on a spinning drive consumes 10–50 percent of the entire 100 ms budget; the blit's share is under one percent. Those ranges follow from the payload size and standard drive behaviour — the timing snippet near the end puts your own machine's numbers next to them.
What you need
- The control's bitmap handle. Win32 calls it HBITMAP; in a C++ Builder TImage you get it as Picture->Bitmap->Handle. Any framework that draws through GDI can hand you the same handle under its own name.
- A device context for the fetch: GetDC(nullptr) is enough. Release it with ReleaseDC once the call returns.
- A BITMAPINFO whose header you fill yourself: 24 bits per pixel, biCompression = BI_RGB.
- One buffer of stride × height bytes, allocated once and reused across frames. A fresh allocation per frame spends the time you just saved.
- OpenCV, in whichever form your code already uses — the C++ interface or the legacy C API.
One condition the GetDIBits reference states outright: the bitmap must not be selected into a device context when you call the function. If you drew through a memory DC, unselect the bitmap first.
Fetch the pixels with GetDIBits
Two lines of arithmetic size everything:
For a 224 × 168 control: 224 × 3 = 672, (672 + 3) / 4 = 168 with integer division, 168 × 4 = 672 bytes per row, so the buffer is 672 × 168 = 112,896 bytes. The padding term vanishes here because 672 already divides by 4; a 225-pixel row would pad to 676.
- Fill the header: biSize = sizeof(BITMAPINFOHEADER), biWidth = 224, biHeight = -168, biPlanes = 1, biBitCount = 24, biCompression = BI_RGB, everything else zero. The negative height requests top-down rows, so buffer row 0 is the top of the picture; the BITMAPINFOHEADER reference defines the sign.
- Allocate the buffer once — 112,896 bytes in this example — and reuse it every frame.
- Call GetDIBits(hdc, hBmp, 0, 168, bits.data(), &bmi, DIB_RGB_COLORS).
- Check the return value. The function returns the number of scan lines copied, so it must read 168; a 0 means nothing was fetched.
- Release the device context: ReleaseDC(nullptr, hdc).
// example values for a 224 x 168 control bitmap
HBITMAP hBmp = Image1->Picture->Bitmap->Handle; // the control's bitmap handle
HDC hdc = GetDC(nullptr);
int width = 224, height = 168;
int stride = ((width * 3 + 3) / 4) * 4; // 672 bytes per row
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height; // negative: top-down rows
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
bmi.bmiHeader.biCompression = BI_RGB;
std::vector<uint8_t> bits(stride * height); // 112,896 bytes, reused per frame
int lines = GetDIBits(hdc, hBmp, 0, height, bits.data(), &bmi, DIB_RGB_COLORS);
ReleaseDC(nullptr, hdc);
// lines == 168 proves the fetch; any other value means it failed
Wrap the buffer in a cv::Mat
The Mat constructor that takes a data pointer wraps existing memory in place — no copy:
cv::Mat m(height, width, CV_8UC3, bits.data(), stride); // rows, cols, type, data, step in bytes
// m.rows == 168, m.cols == 224, m.step[0] == 672
Three details decide whether the wrap is correct:
- The argument order is rows, then cols — 168 then 224 here. Swap them and you get a transposed image with no error raised.
- The step argument is bytes per row, not pixels. When the stride carries padding it matters: a 225-wide control gives stride 676, and m.isContinuous() then reports false, which is the padding handled correctly, not a bug.
- No cvtColor is needed. GDI stores 24-bit pixels as blue, green, red bytes and CV_8UC3 is exactly that BGR order, so the wrapped Mat drops straight into a pipeline that already expects OpenCV's channel order.
Fetched bottom-up instead (positive biHeight), either flip once with cv::flip(m, m, 0) — still memory-speed — or wrap as-is and remember row 0 is the bottom row. The negative-height fetch is simpler and display-ready. A 32-bit control bitmap changes the numbers: biBitCount = 32, stride = width × 4, wrap as CV_8UC4, then convert with cvtColor's BGRA-to-BGR code if the pipeline wants three channels.
Legacy IplImage: cvCreateImageHeader plus cvSetData
Code that still uses IplImage needs two calls instead of one constructor:
IplImage* image2 = cvCreateImageHeader(cvSize(224, 168), IPL_DEPTH_8U, 3);
cvSetData(image2, bits.data(), 672); // attach the external buffer, no copy
image2->origin = 0; // top-down fetch, top-left origin
For a bottom-up fetch set image2->origin = 1, the bottom-left origin, and the classic calls behave exactly as they did on your file-loaded images — cvResize with CV_INTER_LINEAR, cvCvtColor with CV_BGR2GRAY, cvCloneImage, row order included.
Prove the wrap before you switch over
Run this once in a debug build while the old file path still exists. Let the control save itself to check.bmp the way it always has, then compare:
cv::Mat fromFile = cv::imread("check.bmp", cv::IMREAD_COLOR); // reference from the old route
cv::Mat diff;
cv::absdiff(m, fromFile, diff);
double worst = cv::norm(diff, cv::NORM_INF);
// worst == 0.0: every pixel matches the file route
// worst > 0 with correct rows and cols: biHeight sign or stride is wrong
absdiff needs identical rows, cols and type on both sides; if the control's bitmap is 32-bit, compare against a CV_8UC4 wrap of a 32-bit fetch. Assert the header fields as well: m.rows == 168, m.cols == 224, m.step[0] == 672 — or 676 in the padded 225-wide case.
Then time both routes on the machine that runs the panel:
auto t0 = std::chrono::steady_clock::now();
// GetDIBits plus wrap here
double ms = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - t0).count();
Expect the blit route under 1 ms for this 110 KB frame and the file route in the ranges from the first section. A visual check costs nothing: any image with a known top — text, a connector notch — exposes an upside-down fetch at a glance.
Common mistakes
- Swapping rows and cols in the Mat constructor — a transposed image, no exception thrown.
- Passing width in pixels as the step instead of stride in bytes — every row after the first shifts by the padding and the picture tears diagonally.
- Positive biHeight with no flip and no origin change — the frame arrives upside down.
- cvReleaseImage on a wrapped header — frees memory your buffer still owns.
- Skipping the return-value check — zero lines copied, and you wrap stale bytes left in the reused buffer.
- Allocating a fresh vector for every frame — allocation and first-touch page faults add up at frame rate. One reused allocation per control is enough; the acquisition-side version of the same rule is in reuse image buffers instead of per-frame allocation.
When you own the frames, skip the GUI
This detour earns its keep only when the pixels exist nowhere else: a viewer from an SDK that never exposes frames, an HMI you cannot re-plumb, a display-only panel you inherited. When the acquisition is yours, wrap the SDK's own buffer with the same header trick and leave GDI out entirely — wrap an SDK frame buffer in a cv::Mat without copying shows that variant. On a smart camera the question never arises, because acquisition and processing share the device's own memory and there is no GUI bitmap to copy.
If the loop does run on the station PC, put it on a vision controller rather than a desktop tower: fanless, several camera ports, light outputs on board, no moving parts next to the line. The MV-VC3301H-128G66, for example, is an Intel Core i3 controller with 8 GB of RAM and a 128 GB SSD — comfortable headroom for a blit-and-process loop.
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
To pick the box, compare the fanless industrial mini PCs our sister line stocks next to the tower you were going to use; to move the inspection onto the camera itself, start at the smart camera finder. Notes on buffers and pipelines beyond this one sit on the systems topic page. Whichever route you take, the design rule is the same: pixels stay in memory, headers wrap them, and files are for archiving — never for frames in transit.
Questions
Does a cv::Mat created this way copy the pixel data?
No. The header only stores rows, cols, type and a pointer, so the fetch stays a single blit. The price is ownership: the Mat is valid only while your buffer lives. If the vector is freed, or the next GetDIBits call overwrites it before you are done, the Mat reads dead or moving pixels. clone() when the header must outlive the loop; keep one reused allocation per control otherwise.
Why does my wrapped image come out upside down?
You fetched a bottom-up DIB: with a positive biHeight, GDI writes the last row first, so buffer row 0 is the bottom of the picture. Diagnose it by content — any text or notch reads flipped. Fix it at the source by passing biHeight negative, or keep the fetch and run cv::flip(m, m, 0). In IplImage code, setting origin to 1 tells the classic functions the rows run bottom-up, with no extra pass.
Do I need cvtColor after GetDIBits?
Not for a 24-bit fetch: GDI orders 24-bit pixels as blue, green, red and CV_8UC3 is the same BGR order, so the wrapped Mat is pipeline-ready. One conversion earns its place when the control's bitmap is 32-bit — wrap CV_8UC4, then convert BGRA to BGR — and another when the output side expects RGB rather than BGR, for example a display or file writer: one COLOR_BGR2RGB call at the end.
What stride do I pass when the width is not a multiple of 4?
Compute it, never guess: stride = ((width × 3 + 3) / 4) × 4 with integer division. A 640-pixel row is 640 × 3 = 1,920 bytes and needs no padding; a 641-pixel row is 1,923 bytes, which GDI pads to 1,924. Pass the padded figure as the Mat's step and expect isContinuous() to report false — that is the padding handled, not a bug. Skipping it shifts each row and tears the image diagonally.
How much faster is this than saving and reloading a file?
For a 224 × 168 frame the payload is 112,896 bytes. The file route costs 10–50 ms on a spinning drive — each random access spends about 10 ms seeking — and 1–5 ms on an SSD once open, write, flush, read and decode are counted. The GetDIBits route is one memory transfer, well under a millisecond. Measure both with std::chrono on your machine and keep the numbers in the panel's log.
