YOLOv9 represents the latest advancement in the YOLO family of real-time object detection models, offering significant improvements in accuracy, speed, and efficiency. It introduces novel architectural components such as Generalized ELAN (GELAN) and Programmable Gradient Information (PGI) to mitigate information loss in deep networks and enhance feature learning. This article demonstrates how to deploy both YOLOv9 object detection and instance segmentation models using the OpenVINO™ C# API with OpenVINO™ 2024.0.
The OpenVINO™ C# API is a .NET wrapper around the OpenVINO™ Runtime C API, enabling C# developers to leverage OpenVINO’s cross-platform inference acceleration capabilities on Intel CPUs, GPUs, and NPUs. The full implementation and sample code are available at:
- OpenVINO™ C# API: GitHub Repository
- YOLOv9 Deployement Samples: YOLOv9 Samples
Model Preparation
To obtain YOLOv9 models in OpenVINO IR format:
- Clone the official repository: ```
git clone https://github.com/WongKinYiu/yolov9.git
cd yolov9
- Create a Python environment and install dependencies: ```
conda create -n yolov9 python=3.10
conda activate yolov9
pip install -r requirements.txt
pip install openvino==2024.0.0
- Export and convert the model: ```
wget https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-c.pt
python export.py --weights ./yolov9-c.pt --imgsz 640 --include onnx
ovc yolov9-c.onnx
For instance segmentation, use gelan-c-seg.pt instead.
C# Project Setup
Create a .NET 6 console application:
dotnet new console --framework net6.0 -o yolov9
Add required NuGet packages:
dotnet add package OpenVINO.CSharp.API
dotnet add package OpenVINO.runtime.win
dotnet add package OpenVINO.CSharp.API.Extensions
dotnet add package OpenVINO.CSharp.API.Extensions.OpenCvSharp
dotnet add package OpenCvSharp4
dotnet add package OpenCvSharp4.Extensions
dotnet add package OpenCvSharp4.runtime.win
The resulting .csproj file includes:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenCvSharp4" Version="4.9.0.20240103" />
<PackageReference Include="OpenVINO.CSharp.API" Version="2024.0.0.1" />
<PackageReference Include="OpenVINO.runtime.win" Version="2024.0.0.1" />
<PackageReference Include="OpenVINO.CSharp.API.Extensions.OpenCvSharp" Version="1.0.4" />
</ItemGroup>
</Project>
Model Inference Implementation
Object Detection
The inference pipeline follows standard OpenVINO steps: initialize Core, load model, compile for target device, create infer request, preprocess input, run inference, and post-process output.
static void RunYoloDetection(string modelPath, string imagePath, string device = "AUTO")
{
var core = new Core();
var model = core.read_model(modelPath);
var compiledModel = core.compile_model(model, device);
var inferRequest = compiledModel.create_infer_request();
using var image = new Mat(imagePath);
int maxSize = Math.Max(image.Cols, image.Rows);
using var squareImage = Mat.Zeros(maxSize, maxSize, MatType.CV_8UC3);
image.CopyTo(new Mat(squareImage, new Rect(0, 0, image.Cols, image.Rows)));
float scale = maxSize / 640.0f;
var inputTensor = inferRequest.get_input_tensor();
var shape = inputTensor.get_shape();
using var blob = CvDnn.BlobFromImage(squareImage, 1.0 / 255.0,
new Size(shape[2], shape[3]), 0, true, false);
float[] inputData = new float[blob.Total()];
Marshal.Copy(blob.Ptr(0), inputData, 0, inputData.Length);
inputTensor.set_data(inputData);
inferRequest.infer();
Tensor outputTensor = model.get_outputs_size() > 1
? inferRequest.get_output_tensor(1)
: inferRequest.get_output_tensor();
float[] output = outputTensor.get_data<float>((int)outputTensor.get_size());
using var resultMat = new Mat(84, 8400, MatType.CV_32F, output).T();
var boxes = new List<Rect>();
var classIds = new List<int>();
var confidences = new List<float>();
for (int i = 0; i < resultMat.Rows; i++)
{
using var scores = new Mat(resultMat, new Rect(4, i, 80, 1));
Cv2.MinMaxLoc(scores, out _, out double maxScore, out _, out Point maxLoc);
if (maxScore > 0.25)
{
float cx = resultMat.At<float>(i, 0);
float cy = resultMat.At<float>(i, 1);
float w = resultMat.At<float>(i, 2);
float h = resultMat.At<float>(i, 3);
int x = (int)((cx - 0.5 * w) * scale);
int y = (int)((cy - 0.5 * h) * scale);
int width = (int)(w * scale);
int height = (int)(h * scale);
boxes.Add(new Rect(x, y, width, height));
classIds.Add(maxLoc.X);
confidences.Add((float)maxScore);
}
}
CvDnn.NMSBoxes(boxes, confidences, 0.5f, 0.5f, out int[] indices);
foreach (int idx in indices)
{
Cv2.Rectangle(image, boxes[idx], Scalar.Red, 2);
Cv2.PutText(image, $"{classIds[idx]}-{confidences[idx]:F2}",
new Point(boxes[idx].X, boxes[idx].Y + 25),
HersheyFonts.HersheySimplex, 0.8, Scalar.Black, 2);
}
string outputPath = Path.ChangeExtension(imagePath, "_det_result.jpg");
Cv2.ImWrite(outputPath, image);
}
Instance Segmentation
Segmentation adds a second output tensor containing mask prototypes. The detection output includes mask coefficients used to reconstruct instance masks:
static void RunYoloSegmentation(string modelPath, string imagePath, string device = "AUTO")
{
// ... (same initialization and input processing as detection)
inferRequest.infer();
var detTensor = inferRequest.get_output_tensor(0);
var protoTensor = inferRequest.get_output_tensor(1);
float[] detData = detTensor.get_data<float>((int)detTensor.get_size());
float[] protoData = protoTensor.get_data<float>((int)protoTensor.get_size());
using var detections = new Mat(116, 8400, MatType.CV_32F, detData).T();
using var protos = new Mat(32, 25600, MatType.CV_32F, protoData);
var boxes = new List<Rect>();
var classIds = new List<int>();
var confidences = new List<float>();
var maskCoeffs = new List<Mat>();
// ... (parse detections similar to object detection)
CvDnn.NMSBoxes(boxes, confidences, 0.5f, 0.5f, out int[] indices);
using var overlay = Mat.Zeros(image.Size(), MatType.CV_8UC3);
var rand = new Random();
foreach (int idx in indices)
{
using var maskCoeff = maskCoeffs[idx];
using var maskFeatures = maskCoeff * protos; // (1x32) * (32x25600) = (1x25600)
ApplySigmoid(maskFeatures); // element-wise sigmoid
using var mask160 = maskFeatures.Reshape(1, 160); // 160x160
Rect box = boxes[idx];
int mx1 = Math.Max(0, (int)((box.X / scale) * 0.25));
int my1 = Math.Max(0, (int)((box.Y / scale) * 0.25));
int mx2 = Math.Min(160, (int)(((box.X + box.Width) / scale) * 0.25));
int my2 = Math.Min(160, (int)(((box.Y + box.Height) / scale) * 0.25));
using var croppedMask = new Mat(mask160, new Range(my1, my2), new Range(mx1, mx2));
using var resizedMask = new Mat();
Cv2.Resize(croppedMask, resizedMask, new Size(box.Width, box.Height));
using var binaryMask = new Mat();
Threshold(resizedMask, binaryMask, 0.5f, 1.0f); // binarize
using var fullMask = Mat.Zeros(image.Size(), MatType.CV_8UC1);
binaryMask.ConvertTo(binaryMask, MatType.CV_8U, 255);
binaryMask.CopyTo(new Mat(fullMask, new Rect(box.X, box.Y, box.Width, box.Height)));
Scalar color = new(rand.Next(256), rand.Next(256), rand.Next(256));
Cv2.Add(overlay, color, overlay, fullMask);
}
using var result = new Mat();
Cv2.AddWeighted(image, 0.6, overlay, 0.4, 0, result);
string outputPath = Path.ChangeExtension(imagePath, "_seg_result.jpg");
Cv2.ImWrite(outputPath, result);
}
static void ApplySigmoid(Mat mat)
{
for (int i = 0; i < mat.Rows; i++)
for (int j = 0; j < mat.Cols; j++)
mat.Set<float>(i, j, 1.0f / (1.0f + MathF.Exp(-mat.At<float>(i, j))));
}
static void Threshold(Mat src, Mat dst, float thresh, float maxVal)
{
dst.Create(src.Size(), src.Type());
for (int r = 0; r < src.Rows; r++)
for (int c = 0; c < src.Cols; c++)
dst.Set<float>(r, c, src.At<float>(r, c) > thresh ? maxVal : 0.0f);
}
Using OpenVINO Preprocessing
OpenVINO’s built-in preprocessing can be integrated during model loading to offload image resizing and normalization to the inference engine:
var processor = new PrePostProcessor(model);
processor.input(0)
.tensor()
.set_from(new Tensor(new OvType(ElementType.U8), new Shape(1, 640, 640, 3)))
.set_layout(new Layout("NHWC"))
.set_color_format(ColorFormat.BGR);
processor.input(0)
.preprocess()
.convert_color(ColorFormat.RGB)
.resize(ResizeAlgorithm.RESIZE_LINEAR)
.convert_element_type(new OvType(ElementType.F32))
.scale(255.0f)
.convert_layout(new Layout("NCHW"));
var optimizedModel = processor.build();
This allows direct feeding of raw BGR image data without manual normalization.
Execution
The main method supports automatic download of pre-converted models and test images:
static async Task Main(string[] args)
{
string modelPath, imagePath, device = "AUTO";
if (args.Length == 0)
{
// Download pre-converted model and sample image if not present
await EnsureModelAndImageAsync();
modelPath = "./model/yolov9-c-converted.xml";
imagePath = "./model/test_det_01.jpg";
}
else
{
(modelPath, imagePath, device) = (args[0], args[1], args.Length > 2 ? args[2] : "AUTO");
}
Console.WriteLine($"Running inference on {device}");
RunYoloDetection(modelPath, imagePath, device);
RunYoloSegmentation(modelPath.Replace("yolov9-c", "gelan-c-seg"), imagePath, device);
}
Build and run with:
dotnet build
dotnet run --no-build
Both OpenCvSharp and EmguCV variant are provided in the sample repository for developer flexibility.