A WinForms camera preview whose process memory climbs by gigabytes per minute, but only once a resize step is added, points at one orphaned object per frame. The disposal you wrote is not the bug and neither is the PictureBox. This guide is for developers wiring a DirectShow-style USB camera into a C# form. You will trace which Bitmap loses its last reference on every frame, size the leak in megabytes per second from resolution, pixel format and frame rate, apply the one-line dispose that stops it, and decide when setting the camera's own resolution beats resizing in code.
Why does memory fill up only when you resize the frame?
The disposal you wrote frees the resized copy, but the resize filter also builds a brand-new Bitmap from the full-resolution clone, and the out assignment drops the last reference to that clone before anyone disposes it — one orphaned frame per frame.
Follow one frame through the handler. currentFrame = (Bitmap)eventArgs.Frame.Clone() allocates the full-resolution clone, 6.2 MB at 1920×1080. Inside ResizeFrame, filterResize.Apply(image) allocates a second Bitmap, the 640×480 resized copy. The out write then replaces currentFrame with that copy, dropping the last reference to the clone. Nobody disposes it. The only Dispose in the method frees web_cam_box.Image, which still holds the previous frame's resized copy.
Remove ResizeFrame and the pairing heals by accident: the clone itself lands in web_cam_box.Image, so the next frame's Dispose frees it. Every allocation then dies one frame later and commit stays flat, which is exactly the behaviour you saw.
The in/out call hides the handoff: ResizeFrame(in currentFrame, out currentFrame) is legal C#, and in protects the parameter slot from reassignment, not the object from losing references. The variable survives the call; the clone does not.
How much memory does one orphaned frame cost?
A frame costs width × height × bytes per pixel, so each orphaned 1920×1080, 24-bit clone is 6,220,800 bytes, about 6.2 MB, and at 30 fps the leak runs at about 187 MB/s.
Worked example, 24 bits per pixel (3 bytes): 1920 px × 1080 px = 2,073,600 pixels. At 3 B/px that is 2,073,600 × 3 = 6,220,800 B ≈ 6.2 MB per clone. At 30 fps the orphan rate is 6,220,800 × 30 = 186,624,000 B/s ≈ 187 MB/s, and one minute orphans 186,624,000 × 60 = 11,197,440,000 B ≈ 11.2 GB. The resized copy the code does dispose is only 640 × 480 × 3 = 921,600 B ≈ 0.9 MB, which is why the disposal line looks like it works.
| Camera mode | Bytes per frame, 24-bit | Orphaned at 30 fps |
|---|---|---|
| 640 × 480 | 921,600 B (0.9 MB) | 27.6 MB/s |
| 1280 × 720 | 2,764,800 B (2.8 MB) | 82.9 MB/s |
| 1920 × 1080 | 6,220,800 B (6.2 MB) | 186.6 MB/s |
Why doesn't the garbage collector free the orphaned Bitmaps?
Almost none of a Bitmap's memory is managed: the .NET object is a wrapper tens of bytes wide around native GDI+ pixel data, so the garbage collector sees almost nothing and the finalizer thread is the only automatic cleanup, and it runs too late to keep up with 187 MB/s of churn.
A finalizable object needs at least two collections: one to queue it for the finalizer thread, a second to reclaim it after that thread runs. The finalizer thread is single and serves the whole process, so 187 MB/s of native churn outruns it immediately. Heap snapshots come back clean throughout, because each orphan costs the managed heap a few dozen bytes while its pixels sit in native memory.
Calling GC.Collect() every frame forces the backlog through and flattens the curve, but a full blocking collection typically costs tens of milliseconds against the 33.3 ms budget at 30 fps, and it still relies on finalizers instead of fixing ownership. Diagnose with it; never ship it. If growth survives correct disposal, the leak sits elsewhere in the loop — the vision software memory leak inspection-loop guide walks that path.
What is the minimal fix in the resize method?
Dispose the input frame inside the resize method, right after Apply returns — the clone's last usable reference is the in parameter, so that one line restores the one-allocated, one-disposed pairing the no-resize version already had.
private void ResizeFrame(in Bitmap image, out Bitmap resized)
{
ResizeBicubic filterResize = new ResizeBicubic(640, 480);
resized = filterResize.Apply(image);
image.Dispose(); // the fix: the clone's last reference ends here, deliberately
}
Order matters: Apply must run before Dispose, because the filter reads the source pixels. With that line, each frame allocates one clone and one resized copy and frees both on the same path. The rewritten handler keeps the clone inside a using block so nothing escapes:
private void FinalVideoDevice_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
using (Bitmap source = (Bitmap)eventArgs.Frame.Clone())
{
ResizeBicubic filterResize = new ResizeBicubic(640, 480);
Bitmap resized = filterResize.Apply(source);
if (web_cam_box.Image != null)
{
web_cam_box.Image.Dispose();
}
web_cam_box.Image = resized;
}
}
The clone stays, because the event's frame belongs to the capture side: never keep or dispose that object yourself.
Can you skip the extra Bitmap entirely?
You can skip the clone entirely: a resize filter such as ResizeBicubic reads its source and returns a new image without touching the original, so passing eventArgs.Frame straight to Apply leaves one allocation per frame instead of two.
// resizeFilter: ResizeBicubic(640, 480), built once as a form field
private void FinalVideoDevice_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
Bitmap resized = resizeFilter.Apply(eventArgs.Frame);
if (web_cam_box.Image != null)
{
web_cam_box.Image.Dispose();
}
web_cam_box.Image = resized;
}
Build the filter once in the form's constructor instead of paying construction garbage every frame. One allocation per frame remains, and the PictureBox swap disposes it one frame later. If you later switch to an in-place filter variant that mutates its input, return to clone-then-dispose, because mutating the capture side's buffer corrupts the frames behind it.
When should you set the camera resolution instead of resizing?
If the camera already offers a built-in mode near 640×480, set VideoResolution to it before Start and delete the resize filter altogether — zero per-frame copies, one Bitmap per frame, and the CPU cost of the bicubic resample disappears too.
// caps: the device's VideoCapabilities array; set before Start(), never after
capabilities = FinalVideoDevice.VideoCapabilities;
FinalVideoDevice.VideoResolution = capabilities
.OrderBy(c => Math.Abs(c.FrameSize.Width - 640)
+ Math.Abs(c.FrameSize.Height - 480))
.First();
Score the capabilities against the display target rather than taking the largest mode, which maximizes bytes per frame and then spends CPU shrinking them again. VideoResolution only takes effect before capture starts, so set it where the device is constructed. If the sensor offers no mode near the target — a fixed 4K model, say — keep the filter with the dispose fix: there it does real work instead of hiding a leak.
How do you prove the fix worked?
Run the fixed loop for ten minutes and watch the process commit: before the fix it climbs about 11.2 GB per minute (187 MB/s), after it the number stays flat, oscillating a few MB as each frame's bitmaps are disposed.
- Reproduce with the resize on: record Commit size for 60 seconds. Roughly 11.2 GB of growth per minute in the 1080p case confirms the clone is the leak.
- Comment out the resize call: growth stops. This separates the pairing break from the PictureBox and the capture library.
- Restore the resize with the dispose fix in place: commit stays flat and the frame rate holds.
- Finish with one GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(), off the frame path. Commit should already be flat — a large drop after this trio means a bitmap is still escaping disposal.
Compare frame rates before and after as well: a bicubic 1920×1080 → 640×480 resample is real CPU work, and the VideoResolution route removes it entirely.
What changes with a real machine-vision camera?
An industrial camera changes the numbers, not the rule: bytes per pixel follow the pixel format, so the same 1920×1080 frame is about 2.1 MB as Mono8 and 6.2 MB as 24-bit RGB, and SDK buffer objects carry the same dispose-or-return ownership as your clone.
Mono8 stores 1 byte per pixel, so a 1920×1080 frame is 2,073,600 B ≈ 2.1 MB and the orphan rate would be about 62 MB/s at 30 fps. The pixel format itself is a fixed camera property, covered in why the camera pixel format does not change and how to set it. SDK buffers replace Clone() with explicit ownership — a buffer you pull is yours until you dispose or return it. Two deeper managed-code cases are C# camera memory leak: manage image buffer ownership and camera frame memory lifetime in managed code: avoid access violations.
A GigE camera also needs interface hardware in the PC: the MV-GE2002P is a dual-port PoE NIC, full height V2.0, and the MV-GE1104 is a 4-port GigE frame grabber for multi-camera lines.
- Interface
- GigE
- Ports
- 2 ports
- Interface
- GigE
- Ports
- 4 ports
If the live view ends up in an HMI or SCADA client rather than your own form, fitting and layering move into that platform's scripting — an HMI scripting example for mouseover resize and z-order of IP camera views solves the same display problem there.
What is the checklist that keeps frame display leak-free?
The prevention rule is one sentence: every line that allocates a Bitmap must have a matching Dispose on the same path, and anything you keep past the current frame needs exactly one owner responsible for freeing it.
- Dispose in the method that allocates. A using block or a same-path Dispose keeps the clone from outliving its statement block.
- Audit every assignment to a Bitmap field or control property. Each one either takes ownership and disposes the previous value, or orphans it.
- Keep exactly one owner per retained frame. Two fields holding the same Bitmap means neither can safely free it.
- Never park GC.Collect in the frame path. It masks undisposed objects and costs tens of milliseconds per call.
- Prefer camera-side resolution over code-side resizing. Bytes you never allocate never leak; a 640×480 stream makes the whole class of bug impossible.
- Carry the rule across runtimes. The Python frame-queue memory leak fix is the same unbounded frame list in another language.
Decision by case: webcam preview, display only — set VideoResolution to the capability nearest the display and delete the filter; where no such mode exists, resize and dispose the input in the same method. Full-resolution processing later in the pipeline — keep the clone short-lived in a using block and dispose it the moment the filter returns. Industrial hardware — the same ownership rule through SDK buffer objects; more acquisition-side fixes are collected on the systems topic page, and if you want a buffer plan checked against real hardware before you order, the contact page is the fastest way to reach us.
Interface hardware for the camera path lives in one place:
CoaXPress, Camera Link and GigE cards
58 models · from $189.00Questions
Does calling GC.Collect after every frame fix the leak?
It hides the symptom, not the cause. Forced collections eventually drain the finalizer backlog of undisposed Bitmaps, but memory still climbs between collections and every frame now pays a full blocking collection, typically tens of milliseconds against the 33 ms budget at 30 fps. Keep the GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect() trio for one-off diagnostics: if commit size drops sharply afterwards, something in the loop is still undisposed.
Should I dispose eventArgs.Frame itself?
No. Treat the frame handed to the handler as owned by the capture side: it may be recycled for the next frame once your handler returns, so keep it only by cloning, and dispose only what you created. Disposing a buffer the source still uses trades a leak for access violations or frames that arrive torn a few iterations later.
How many bytes per frame do other pixel formats need?
Multiply resolution by bytes per pixel. Mono8 and Bayer8 store 1 byte per pixel, so 1920×1080 is 2,073,600 B, about 2.1 MB and roughly 62 MB/s at 30 fps if orphaned; 24-bit RGB stores 3, giving 6.2 MB and 187 MB/s. A 10-bit pipeline unpacked to 16-bit planes doubles the per-pixel count again.
How do I confirm the growth is native and not the managed heap?
Watch two numbers side by side: the process commit size in Task Manager and the GC heap size from a profiler or dotnet-counters view. In this failure the GC heap stays nearly flat while commit climbs by hundreds of megabytes. That split is the signature of undisposed IDisposable objects whose payload lives in native memory, not of leaked managed objects.
Can I display frames without allocating a new Bitmap every time?
Yes, mostly. Keep one reusable 640×480 Bitmap, draw each incoming frame onto it through a Graphics object on the UI thread, and the display never swaps images at all. You still need a short-lived copy of the incoming frame if the capture thread keeps running during the draw, and that copy remains yours to dispose on the same path.

