System designHow-to

Draw YOLO detections in a WinForms PictureBox yourself

Grab frames, call the detector for coordinates, and draw YOLO detections yourself into a WinForms PictureBox with GDI+: boxes, centroids, pixel counts, in C++.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01What you need
  2. 02Why the demo display path cannot feed your form
  3. 03Wire it up: grab, detect, draw
  4. 04The coordinate math: centroids, pixel counts and scaling
  5. 05Verify it works
  6. 06Common mistakes
  7. 07When the hardware should do more
  8. 08Next steps
  9. 09Questions

You have trained the detector and the weights find your parts. The step that stalls most builds is the last one: putting live boxes, centroids and pixel counts inside your own Windows form. The framework's demo program draws detections in its own display loop, and on a capture setup it ends up writing frames out to files — neither output hands you a surface to draw on inside a PictureBox. The fix is to stop consuming the demo's video altogether: grab the frame yourself, call the detector library for box coordinates, and draw the input image plus your own overlays in the form's Paint handler. This guide walks through that pattern for a C++ WinForms application — the threading, the coordinate math for centroids, pixel counts and scaling, and the tests that prove each number. The same loop serves a webcam at 30 fps and an industrial GigE camera on the line.

What you need

Five pieces, and none of them is the demo program:

  • The trained weights and their network configuration from your Darknet run.
  • The detector compiled as a DLL that returns detections from one call — the yolo_cpp_dll wrapper is the usual route for this stack.
  • A WinForms C++ project (C++/CLI). Visual Studio Community 2015 with CUDA 10.0 is a known working combination; keep the wrapper build, the CUDA runtime and the Visual Studio toolchain on compatible versions.
  • A PictureBox for the live image and a label or status bar for the numeric readouts.
  • A frame source: a webcam through your capture library, or an industrial camera — area scan cameras on GigE Vision or USB3 Vision feed the same grab loop.

Everything below assumes the frame arrives as a bitmap in image pixels; the worked example uses 640 × 480.

Why the demo display path cannot feed your form

demo.c lives in the framework's own solution (darknet.sln) and drives its own display loop; adapted for saving, it writes frames out to files. Both routes dead-end in a form. A rendered frame has the boxes burned in, so the coordinates you need for centroids and pixel counts are gone, and a per-frame file write caps the loop at a few frames per second while loading the disk on a line-side machine.

The library route hands you the raw material instead. Its API exposes an image structure going in and returns the boxes as data: each detection carries x, y, w and h in pixels, a confidence value and a class index. Coordinates as data are the whole trick. Once the boxes arrive as numbers, drawing them, labelling them and turning them into centroids and areas is ordinary GDI+ work inside your own form.

Wire it up: grab, detect, draw

  1. Create the detector once. Load the weights and configuration at startup and keep the handle for the life of the form. Weight loading takes seconds; doing it per frame is a hidden stall that looks like a hang.
  2. Detect on a worker thread. One thread grabs a frame, converts it to the detector's image format and calls detect. Never call detect from a button handler or the Paint handler; running fetch off the main thread is the pattern in GUI freezes during image capture: run fetch off the main thread.
  3. Store results behind a lock. Copy the returned boxes into a member list guarded by a mutex, so the UI thread never reads a list that is being replaced.
  4. Marshal the refresh. Call BeginInvoke on the form with a short method that swaps in the new boxes and calls Invalidate(), so the control repaints only when results actually changed.
  5. Draw in the Paint handler. Build the Graphics object from the event arguments as Microsoft's GDI+ how-to on creating Graphics objects shows, draw the latest frame with DrawImage, then draw each box with DrawRectangle from a Pen you created once as a member.
  6. Label, do not bake. Print class index, confidence and centroid with DrawString on top of the frame. The overlay lives on screen only — the same principle as text overlay on a live camera image without altering the frame.

The coordinate math: centroids, pixel counts and scaling

The detector returns boxes in image pixels. Four lines of arithmetic turn each box into everything the application needs:

cx = x + w / 2 cy = y + h / 2 box_px = w × h s = control_width / image_width

Worked example: the frame is 640 × 480 and one detection returns x = 212, y = 96, w = 184, h = 152.

  • Centroid: cx = 212 + 184 / 2 = 212 + 92 = 304, and cy = 96 + 152 / 2 = 96 + 76 = 172, so the object centre sits at pixel (304, 172).
  • Box area: 184 × 152 = 27,968 px, which is 27,968 / 307,200 = 9.1% of the frame (307,200 = 640 × 480).
  • Scaling to a 320 × 240 PictureBox: s = 320 / 640 = 0.5, so the rectangle draws at (212 × 0.5, 96 × 0.5, 184 × 0.5, 152 × 0.5) = (106, 48, 92, 76). Multiply every coordinate by s — position and size alike — or the box drifts off the object.
// Illustrative values: frame 640 x 480, PictureBox 320 x 240, one detection
int bx = 212, by = 96, bw = 184, bh = 152;  // box from detect(), image pixels
float s = 320.0f / 640.0f;                  // 0.5
int cx = bx + bw / 2;                       // 304
int cy = by + bh / 2;                       // 172
int boxPx = bw * bh;                        // 27968

Drawing then combines your variables with documented calls: DrawRectangle(boxPen, (int)(bx * s), (int)(by * s), (int)(bw * s), (int)(bh * s)) places the rectangle, and DrawString prints the centroid and box pixel count beside it. With integer math an odd w truncates its half-pixel in the centroid — a sub-pixel offset no display can show.

A box count is a rectangle count, background included. When the specification asks for the object's own pixel count, threshold the pixels inside the box and count the matches; the box stays your region finder. For overlays that pair a shape with a numeric readout, showing a circular ROI overlay with a numeric radius follows the same draw-with-the-number pattern.

Verify it works

Run these four tests before pointing the camera at anything:

  1. Arithmetic test. Feed the values above through your centroid and area code and assert cx = 304, cy = 172, boxPx = 27968. Any other output means the math, not the detector, is wrong.
  2. Draw test. With s = 0.5 the rectangle's top-left must land at control coordinates (106, 48). Nothing on screen at that point means the Paint handler is redrawing the raw frame over the overlay, or the scaled coordinates never reach it.
  3. Click test. Click the control at (152, 86). Multiply by 1 / s = 2 and the click maps to image pixel (304, 172) — exactly the centroid — proving the inverse mapping your operators will rely on.
  4. Timing test. Log the detect call. At 15 fps the per-frame budget is 1000 ms / 15 = 66.7 ms, and the draw path should stay in the low single-digit milliseconds. Then run 60 seconds while dragging the window: the form must stay live while detection continues, which proves the loop is off the UI thread. If your grab loop reports 0 fps, reading the frame rate reliably covers why counters return zero.

Common mistakes

  • Consuming the demo's rendered output. Its boxes are burned in and its canvas is sized to the network input, so coordinates and aspect ratio are both gone. Take coordinates from the API and render your own frame.
  • Coordinate-space drift. If the pipeline letterboxes the frame before detect, boxes come back in network-input space; map them back before drawing. Bounding box offset on new images: the double letterbox fix works through that offset arithmetic.
  • Mixing image and control coordinates. Image-space boxes drawn into a scaled control land offset by exactly the scale factor. When boxes drift, do what debugging object detection output coordinates suggests: log one box in both spaces and compare.
  • Per-frame GDI churn. Creating Pens and Brushes every frame burns GDI handles over a long shift. Build them once and reuse them.
  • Drawing every detection. Filter on the returned confidence before drawing; 0.2 to 0.5 is the usual starting band, tuned per class.

When the hardware should do more

Everything above assumes you want to own the GUI. If the deliverable is only the inspection result, move detection onto the device: a smart camera runs the model on board and hands your form just the results over Ethernet, so the PC side carries no CUDA runtime, no GPU and no paint loop. For the choice between on-device rule-based tools and learned models, AI vs rule-based smart camera inspection: how to choose lays out the trade-offs.

When the detector stays PC-hosted — several cameras, GPU inference and your WinForms front end on the line — host it on a fanless vision controller built for camera I/O rather than an office desktop. Two controllers from the catalog cover opposite ends:

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
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
PoE
PoE camera ports
Light outputs
8 built-in channels
$4,479.00 · quotedView and quoteBuild a system with it

The first pairs an Intel Core i3 with 8 GB of memory, five or more LAN ports and PCIe expansion for an added inference card. The second moves to an Intel Core i5 with 32 GB of memory and PoE camera ports, plus eight built-in light channels, so a GigE camera and the illumination driver both connect directly.

ComputersVision controllers

Vision computers and fanless industrial PCs

190 models · from $1,119.00

Next steps

If you are sizing the box that runs the detector, open the vision system configurator and set the camera count, the interfaces and the GPU slot. For a quote on a full stack — camera, lens, light and controller together — use the contact page. More display and overlay patterns live in the systems topic guides.

Questions

Can the detector thread draw into the PictureBox directly?

No. A control's drawing surface belongs to the UI thread, so drawing from the worker causes cross-thread errors or flicker. Copy the returned boxes into a member list under a lock, then call BeginInvoke on the form to schedule a short refresh that ends in Invalidate(). The Paint handler then reads that list and draws. The copy stays small — a few dozen boxes — so the handoff costs microseconds and the loop keeps its real-time pace.

How do I map a click in the PictureBox back to image coordinates?

Divide the click position by the draw scale s and clamp to the image bounds. With a 640 × 480 frame in a 320 × 240 control, s = 0.5, so a click at (152, 86) maps to image coordinates (304, 172). If the control is larger than the scaled image, subtract the letterbox margin before dividing so the mapping stays exact — useful for placing ROIs where the operator points.

Does this work with YOLOv2 weights trained in Darknet?

Yes — YOLOv2 weights trained in Darknet are the classic case: load the weights and network configuration once through the wrapper DLL, then call detect per frame. This stack runs under Visual Studio Community 2015 with CUDA 10.0. Keep the wrapper build, the CUDA runtime and the toolchain on compatible versions; a mismatch surfaces as a DLL load failure at startup, not as a subtly wrong detection.

Is the box area the same as the object's pixel count?

No. w × h counts the whole rectangle, background included — 27,968 px in the worked example whether the part fills the box or not. For a true object pixel count, threshold the pixels inside the box by colour or contrast, count the matching ones and take their centroid. Run that on the frame you already hold, before Invalidate() schedules the redraw, so the numbers match what the overlay shows.

What frame rate should the overlay loop target?

Match the detector, not the camera. If detect returns every 66.7 ms — 15 fps — drawing more often only repaints the same boxes, so invalidate only when new results arrive. Keep the draw path in the low single-digit milliseconds so it never delays the next detect call. At 30 fps the budget halves to 33.3 ms per frame, which is where a Core i5-class controller starts to earn its keep over a Core i3.