Skip to content

Commit b062cc7

Browse files
committed
Add .NET-specific OpenCvSharp guides
1 parent bf50d0e commit b062cc7

9 files changed

Lines changed: 716 additions & 0 deletions
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
try
49+
{
50+
using var source = Cv2.ImDecode(
51+
encoded.GetBuffer().AsSpan(
52+
start: 0,
53+
length: checked((int)encoded.Length)),
54+
ImreadModes.Color);
55+
56+
if (source.Empty())
57+
{
58+
return BadRequest("The upload is not a supported image.");
59+
}
60+
61+
if (source.Total() > MaxDecodedPixels)
62+
{
63+
return BadRequest("The decoded image is too large.");
64+
}
65+
66+
cancellationToken.ThrowIfCancellationRequested();
67+
68+
using var grayscale = new Mat();
69+
Cv2.CvtColor(
70+
source,
71+
grayscale,
72+
ColorConversionCodes.BGR2GRAY);
73+
74+
if (!Cv2.ImEncode(".png", grayscale, out byte[] png))
75+
{
76+
return StatusCode(
77+
StatusCodes.Status500InternalServerError,
78+
"Could not encode the result.");
79+
}
80+
81+
return File(png, "image/png");
82+
}
83+
catch (OpenCVException)
84+
{
85+
return BadRequest("The upload could not be decoded.");
86+
}
87+
}
88+
}
89+
```
90+
91+
`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.
92+
93+
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.
94+
95+
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.
96+
97+
## Decode an arbitrary Stream with a byte limit
98+
99+
For `HttpRequest.Body`, blob storage, or another stream source, enforce a byte limit while buffering. This helper returns a caller-owned `Mat`:
100+
101+
```csharp
102+
using OpenCvSharp;
103+
104+
static async Task<Mat> DecodeImageAsync(
105+
Stream source,
106+
int maxEncodedBytes,
107+
ImreadModes mode,
108+
CancellationToken cancellationToken)
109+
{
110+
ArgumentNullException.ThrowIfNull(source);
111+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxEncodedBytes);
112+
113+
using var encoded = new MemoryStream(
114+
capacity: Math.Min(maxEncodedBytes, 64 * 1024));
115+
byte[] chunk = new byte[64 * 1024];
116+
117+
while (true)
118+
{
119+
int remaining = maxEncodedBytes - checked((int)encoded.Length);
120+
int readSize = remaining >= chunk.Length
121+
? chunk.Length
122+
: remaining + 1;
123+
int read = await source.ReadAsync(
124+
chunk.AsMemory(0, readSize),
125+
cancellationToken);
126+
127+
if (read == 0)
128+
{
129+
break;
130+
}
131+
132+
if (read > remaining)
133+
{
134+
throw new InvalidDataException(
135+
"The encoded image exceeds the configured limit.");
136+
}
137+
138+
await encoded.WriteAsync(
139+
chunk.AsMemory(0, read),
140+
cancellationToken);
141+
}
142+
143+
if (encoded.Length == 0)
144+
{
145+
throw new InvalidDataException("The encoded image is empty.");
146+
}
147+
148+
cancellationToken.ThrowIfCancellationRequested();
149+
150+
Mat image = Cv2.ImDecode(
151+
encoded.GetBuffer().AsSpan(
152+
start: 0,
153+
length: checked((int)encoded.Length)),
154+
mode);
155+
156+
if (!image.Empty())
157+
{
158+
return image;
159+
}
160+
161+
image.Dispose();
162+
throw new InvalidDataException(
163+
"The stream does not contain a supported image.");
164+
}
165+
```
166+
167+
The caller owns the returned matrix:
168+
169+
```csharp
170+
using Mat image = await DecodeImageAsync(
171+
Request.Body,
172+
maxEncodedBytes: 10 * 1024 * 1024,
173+
ImreadModes.Color,
174+
HttpContext.RequestAborted);
175+
```
176+
177+
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.
178+
179+
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.
180+
181+
## Validate encoded and decoded sizes
182+
183+
An encoded file can expand to a much larger pixel buffer. Validate at multiple layers:
184+
185+
- Request-body size at Kestrel, IIS, or the reverse proxy.
186+
- Multipart and per-file size before copying an `IFormFile`.
187+
- A byte limit while reading an arbitrary stream.
188+
- `Empty()`, dimensions, channel count, and decoded pixel count after `ImDecode`.
189+
- Application-specific limits before expensive processing or encoding.
190+
191+
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.
192+
193+
## Manage cancellation and CPU work
194+
195+
`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.
196+
197+
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.
198+
199+
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.
200+
201+
Each request should own its mutable matrices. Do not share a writable `Mat` between concurrent requests without external synchronization and a clear ownership model.
202+
203+
## Return encoded data, not a Mat
204+
205+
`Mat` is a native resource and is not an HTTP response representation. Encode it before leaving the request scope:
206+
207+
```csharp
208+
if (!Cv2.ImEncode(".jpg", result, out byte[] jpeg, [
209+
new ImageEncodingParam(
210+
ImwriteFlags.JpegQuality,
211+
90),
212+
]))
213+
{
214+
throw new InvalidOperationException(
215+
"Could not encode the JPEG response.");
216+
}
217+
218+
return Results.File(jpeg, "image/jpeg");
219+
```
220+
221+
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`.
222+
223+
## Deploy without a desktop GUI
224+
225+
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.
226+
227+
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.
228+
229+
## Related guides
230+
231+
- [Image Encoding and Conversion](image-conversion.md)
232+
- [Copies, Native Memory, and Performance](memory-copy-and-performance.md)
233+
- [Common Errors and Diagnostics](../troubleshooting/common-errors.md)
234+
- [Native Library Loading](../troubleshooting/native-library-loading.md)
235+
236+
## Official references
237+
238+
- [OpenCV image file reading and writing](https://docs.opencv.org/5.0/main_modules/imgcodecs.html)
239+
- [Upload files in ASP.NET Core](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads)
240+
- [Configure Kestrel server limits](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/options)
241+
242+
## Related OpenCvSharp API
243+
244+
- [Cv2](xref:OpenCvSharp.Cv2)
245+
- [Mat](xref:OpenCvSharp.Mat)
246+
- [ImreadModes](xref:OpenCvSharp.ImreadModes)
247+
- [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)