Skip to content

Commit 99e4b9b

Browse files
authored
Add .NET-specific OpenCvSharp guides (#2092)
* Add .NET-specific OpenCvSharp guides * Address documentation review feedback
1 parent bf50d0e commit 99e4b9b

9 files changed

Lines changed: 741 additions & 0 deletions
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# ASP.NET Core Image Uploads and Streams
2+
3+
An uploaded PNG or JPEG is compressed file data, while an OpenCvSharp `Mat` contains decoded pixels. A typical ASP.NET Core request therefore crosses two explicit conversion boundaries:
4+
5+
1. Read and limit the uploaded bytes.
6+
2. Decode the bytes with `Cv2.ImDecode`.
7+
3. Process caller-owned `Mat` objects.
8+
4. Encode the result with `Cv2.ImEncode`.
9+
5. Return the encoded managed byte array.
10+
11+
OpenCV does not decode an arbitrary .NET `Stream` incrementally. Even when ASP.NET Core receives the request as a stream, the encoded image must be collected into a contiguous buffer before calling `ImDecode`.
12+
13+
## Process an IFormFile in a controller
14+
15+
The following controller accepts one buffered multipart upload, converts it to grayscale, and returns a PNG:
16+
17+
```csharp
18+
using Microsoft.AspNetCore.Http;
19+
using Microsoft.AspNetCore.Mvc;
20+
using OpenCvSharp;
21+
22+
namespace ImageApi.Controllers;
23+
24+
[ApiController]
25+
[Route("images")]
26+
public sealed class ImagesController : ControllerBase
27+
{
28+
private const long MaxFileBytes = 10 * 1024 * 1024;
29+
private const long MaxRequestBytes = 11 * 1024 * 1024;
30+
private const long MaxDecodedPixels = 40_000_000;
31+
32+
[HttpPost("grayscale")]
33+
[RequestSizeLimit(MaxRequestBytes)]
34+
public async Task<IActionResult> Grayscale(
35+
IFormFile file,
36+
CancellationToken cancellationToken)
37+
{
38+
if (file.Length is <= 0 or > MaxFileBytes)
39+
{
40+
return BadRequest("Upload one non-empty image up to 10 MiB.");
41+
}
42+
43+
using var encoded = new MemoryStream(
44+
capacity: checked((int)file.Length));
45+
await file.CopyToAsync(encoded, cancellationToken);
46+
cancellationToken.ThrowIfCancellationRequested();
47+
48+
Mat source;
49+
try
50+
{
51+
source = Cv2.ImDecode(
52+
encoded.GetBuffer().AsSpan(
53+
start: 0,
54+
length: checked((int)encoded.Length)),
55+
ImreadModes.Color);
56+
}
57+
catch (OpenCVException)
58+
{
59+
return BadRequest("The upload could not be decoded.");
60+
}
61+
62+
using (source)
63+
{
64+
if (source.Empty())
65+
{
66+
return BadRequest("The upload is not a supported image.");
67+
}
68+
69+
if (source.Total() > MaxDecodedPixels)
70+
{
71+
return BadRequest("The decoded image is too large.");
72+
}
73+
74+
cancellationToken.ThrowIfCancellationRequested();
75+
76+
using var grayscale = new Mat();
77+
Cv2.CvtColor(
78+
source,
79+
grayscale,
80+
ColorConversionCodes.BGR2GRAY);
81+
82+
if (!Cv2.ImEncode(".png", grayscale, out byte[] png))
83+
{
84+
return StatusCode(
85+
StatusCodes.Status500InternalServerError,
86+
"Could not encode the result.");
87+
}
88+
89+
return File(png, "image/png");
90+
}
91+
}
92+
}
93+
```
94+
95+
`IFormFile` is already buffered by ASP.NET Core, potentially in memory or a temporary file. Copying it into a `MemoryStream` creates a contiguous buffer for `ImDecode`. `GetBuffer()` avoids the additional array copy that `ToArray()` would make.
96+
97+
The request limit is slightly larger than the file limit because a multipart request also contains boundaries and headers. Hosting layers such as IIS or a reverse proxy may impose their own limits.
98+
99+
Do not trust `IFormFile.FileName` or `ContentType` to select a decoder or response type. Decode the content, choose the output format on the server, and return the media type that matches that encoder.
100+
101+
## Decode an arbitrary Stream with a byte limit
102+
103+
For `HttpRequest.Body`, blob storage, or another stream source, enforce a byte limit while buffering. This helper returns a caller-owned `Mat`:
104+
105+
```csharp
106+
using OpenCvSharp;
107+
108+
static async Task<Mat> DecodeImageAsync(
109+
Stream source,
110+
int maxEncodedBytes,
111+
ImreadModes mode,
112+
CancellationToken cancellationToken)
113+
{
114+
ArgumentNullException.ThrowIfNull(source);
115+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxEncodedBytes);
116+
117+
using var encoded = new MemoryStream(
118+
capacity: Math.Min(maxEncodedBytes, 64 * 1024));
119+
byte[] chunk = new byte[64 * 1024];
120+
121+
while (true)
122+
{
123+
int remaining = maxEncodedBytes - checked((int)encoded.Length);
124+
int readSize = remaining >= chunk.Length
125+
? chunk.Length
126+
: remaining + 1;
127+
int read = await source.ReadAsync(
128+
chunk.AsMemory(0, readSize),
129+
cancellationToken);
130+
131+
if (read == 0)
132+
{
133+
break;
134+
}
135+
136+
if (read > remaining)
137+
{
138+
throw new InvalidDataException(
139+
"The encoded image exceeds the configured limit.");
140+
}
141+
142+
await encoded.WriteAsync(
143+
chunk.AsMemory(0, read),
144+
cancellationToken);
145+
}
146+
147+
if (encoded.Length == 0)
148+
{
149+
throw new InvalidDataException("The encoded image is empty.");
150+
}
151+
152+
cancellationToken.ThrowIfCancellationRequested();
153+
154+
Mat image = Cv2.ImDecode(
155+
encoded.GetBuffer().AsSpan(
156+
start: 0,
157+
length: checked((int)encoded.Length)),
158+
mode);
159+
160+
if (!image.Empty())
161+
{
162+
return image;
163+
}
164+
165+
image.Dispose();
166+
throw new InvalidDataException(
167+
"The stream does not contain a supported image.");
168+
}
169+
```
170+
171+
The caller owns the returned matrix:
172+
173+
```csharp
174+
using Mat image = await DecodeImageAsync(
175+
Request.Body,
176+
maxEncodedBytes: 10 * 1024 * 1024,
177+
ImreadModes.Color,
178+
HttpContext.RequestAborted);
179+
```
180+
181+
This helper applies an application limit even when the stream does not report a length. Configure the server or endpoint request-body limit as the first line of defense so the application does not need to receive an oversized body before rejecting it.
182+
183+
For very frequent requests, an `ArrayPool<byte>` or a recyclable stream can reduce managed buffer allocation. Return pooled storage only after `ImDecode` completes because the decoder reads the buffer synchronously during the call.
184+
185+
## Validate encoded and decoded sizes
186+
187+
An encoded file can expand to a much larger pixel buffer. Validate at multiple layers:
188+
189+
- Request-body size at Kestrel, IIS, or the reverse proxy.
190+
- Multipart and per-file size before copying an `IFormFile`.
191+
- A byte limit while reading an arbitrary stream.
192+
- `Empty()`, dimensions, channel count, and decoded pixel count after `ImDecode`.
193+
- Application-specific limits before expensive processing or encoding.
194+
195+
Checking decoded dimensions happens after the decoder has allocated the image. If hostile image formats or decompression bombs are in scope, use an image-header parser with strict dimension limits, isolate decoding in a constrained worker, or apply operating-system memory limits. A post-decode check alone cannot prevent the decoder's initial allocation.
196+
197+
## Manage cancellation and CPU work
198+
199+
`CopyToAsync` and `ReadAsync` observe a `CancellationToken`. Most `Cv2` calls are synchronous native operations and cannot be interrupted by an ASP.NET Core cancellation token after they start.
200+
201+
Check cancellation before expensive stages and avoid starting new work after `HttpContext.RequestAborted` is signaled. For long-running or untrusted workloads, move processing to a bounded queue or worker process that has an explicit timeout and resource limits.
202+
203+
Wrapping every `Cv2` call in `Task.Run` does not reduce its CPU or memory cost. Under load it can add ThreadPool contention. Use bounded concurrency, and benchmark it together with OpenCV's own native thread usage. `Cv2.SetNumThreads` changes process-wide OpenCV behavior, so configure it once only when measurements justify doing so.
204+
205+
Each request should own its mutable matrices. Do not share a writable `Mat` between concurrent requests without external synchronization and a clear ownership model.
206+
207+
## Return encoded data, not a Mat
208+
209+
`Mat` is a native resource and is not an HTTP response representation. Encode it before leaving the request scope:
210+
211+
```csharp
212+
if (!Cv2.ImEncode(".jpg", result, out byte[] jpeg, [
213+
new ImageEncodingParam(
214+
ImwriteFlags.JpegQuality,
215+
90),
216+
]))
217+
{
218+
throw new InvalidOperationException(
219+
"Could not encode the JPEG response.");
220+
}
221+
222+
return Results.File(jpeg, "image/jpeg");
223+
```
224+
225+
The returned `byte[]` is managed and remains valid after the source and result matrices are disposed. Encoding necessarily creates a compressed output buffer; it is not a zero-copy view of the `Mat`.
226+
227+
## Deploy without a desktop GUI
228+
229+
ASP.NET Core services normally do not use `Cv2.ImShow`, WPF, or another desktop UI. Choose the headless runtime package when the service needs the full non-GUI OpenCV module set, or the slim package only when its reduced modules are sufficient.
230+
231+
Native runtime assets must match the deployment runtime identifier and architecture. Containers may also require native dependencies for the selected image codecs. Log `Cv2.GetVersionString()` and `Cv2.GetBuildInformation()` when production codec behavior differs from development.
232+
233+
## Related guides
234+
235+
- [Image Encoding and Conversion](image-conversion.md)
236+
- [Copies, Native Memory, and Performance](memory-copy-and-performance.md)
237+
- [Common Errors and Diagnostics](../troubleshooting/common-errors.md)
238+
- [Native Library Loading](../troubleshooting/native-library-loading.md)
239+
240+
## Official references
241+
242+
- [OpenCV image file reading and writing](https://docs.opencv.org/5.0/main_modules/imgcodecs.html)
243+
- [Upload files in ASP.NET Core](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads)
244+
- [Configure Kestrel server limits](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/options)
245+
246+
## Related OpenCvSharp API
247+
248+
- [Cv2](xref:OpenCvSharp.Cv2)
249+
- [Mat](xref:OpenCvSharp.Mat)
250+
- [ImreadModes](xref:OpenCvSharp.ImreadModes)
251+
- [ImageEncodingParam](xref:OpenCvSharp.ImageEncodingParam)

docs/docfx/articles/guides/image-conversion.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ For file uploads, HTTP responses, and database blobs, encode directly with `Cv2.
7777

7878
See [Display Images in .NET Applications](displaying-images-dotnet.md) for WPF, Avalonia, GDI+, UI-thread, and live-frame guidance.
7979

80+
See [ASP.NET Core Image Uploads and Streams](aspnet-image-processing.md) for bounded upload buffering, `IFormFile`, request cancellation, and HTTP responses.
81+
8082
## Official OpenCV references
8183

8284
- [OpenCV image file reading and writing](https://docs.opencv.org/5.0/main_modules/imgcodecs.html)

0 commit comments

Comments
 (0)