ITPub博客

首页 > 大数据 > 数据分析 > 下面是一个完整的 YOLO 目标检测示例,

下面是一个完整的 YOLO 目标检测示例,

投稿 数据分析 作者:ecclw459 时间:2026-01-29 18:18:36 0 删除 编辑

using JYPPX.TensorRtSharp.Cuda; using JYPPX.TensorRtSharp.Nvinfer; using OpenCvSharp; using OpenCvSharp.Dnn; using System.Diagnostics; using System.Runtime.InteropServices; namespace YoloDetInfer {     internal class Program    {         // ================= 配置参数 =================         // 模型输入尺寸 (宽=高)         private const int InputSize = 640 ;         // 建议根据实际模型动态获取或使用 Netron 查看         private const int OutputSize = 8400 ;         // 模型类别数 (根据您的具体数据集修改,此处假设为15类)         private const int CategoryNum = 80 ;         // 置信度阈值         private const float ConfThreshold = 0.25f ;         // NMS IOU 阈值         private const float NmsThreshold = 0.3f ;         static void Main( string[] args)        {             //  ============= 配置 TensorRT 日志回调 =============             // 定义一个委托,用于处理 TensorRT 内部产生的日志消息。             // 这允许我们将 C++ 层面的日志输出到 C# 的控制台。            LogCallbackFunction _callbackDelegate = (message) =>            {                Console.WriteLine(message);            };             // 将自定义的回调函数注册给 TensorRT 的全局 Logger 实例。            Logger.Instance.SetCallback(_callbackDelegate);             // 设置日志的严重性级别阈值。             // LoggerSeverity.kINFO: 打印信息、警告和错误。             // 开发调试阶段通常设为 kINFO 或 kVERBOSE;生产环境可设为 kWARNING 或 kERROR 以减少输出。            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);             string enginePath = "yolov8s.engine" ;             string imagePath = "bus.jpg" ;             // ================= 1. 加载 TensorRT Engine =================             // 使用 using 语句确保文件流正确关闭             byte [] engineData;             using (FileStream fs = new FileStream(enginePath, FileMode.Open, FileAccess.Read))             using (BinaryReader br = new BinaryReader(fs))            {                engineData = br.ReadBytes(( int )fs.Length);            }             // 反序列化 Engine             // Runtime 必须在 Engine 生命周期内保持存活,通常建议设为全局或静态,或者确保它最后释放            Runtime runtime = new Runtime();             // 创建 CudaEngine (此处使用 using 确保推理完成后引擎被销毁)             using (CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(engineData, ( ulong )engineData.Length))            {                 // ================= 2. 初始化推理上下文与显存 =================                 // 创建执行上下文                 using (JYPPX.TensorRtSharp.Nvinfer.ExecutionContext executionContext = cudaEngine.createExecutionContext(TrtExecutionContextAllocationStrategy.kSTATIC))                 using (CudaStream cudaStream = new CudaStream()) // 创建 CUDA 流用于异步执行                {                     // 获取输入维度信息 (用于校验)                    Dims inputDims = executionContext.getTensorShape( "images" );                    Logger.Instance.INFO( $"Input Shape: {inputDims.d[ 0]}x {inputDims.d[ 1]}x {inputDims.d[ 2]}x {inputDims.d[ 3]}" );                     // 计算所需显存大小                     // 输入: Batch=1, Channel=3, Height=640, Width=640                     ulong inputSizeInBytes = 1 * 3 * InputSize * InputSize;                     // 输出: Batch=1, Channels=CategoryNum+4(box)+1(angle), Num=8400                     int outputChannels = CategoryNum + 4 ; // 4坐标 + N类别                     ulong outputSizeInBytes = ( ulong )( 1 * outputChannels * OutputSize);                    Stopwatch sw = new Stopwatch();                     // 分配 GPU 显存                     using (Cuda1DMemory< float > inputGpuMemory = new Cuda1DMemory< float >(inputSizeInBytes))                     using (Cuda1DMemory< float > outputGpuMemory = new Cuda1DMemory< float >(outputSizeInBytes))                    {                         // 绑定显存地址到 TensorRT 上下文                        executionContext.setInputTensorAddress( "images" , inputGpuMemory. get ());                        executionContext.setOutputTensorAddress( "output0" , outputGpuMemory. get ());                         // 预热推理 (可选,但推荐,尤其是推理时)                        executionContext.executeV3(cudaStream);                        cudaStream.Synchronize();                         // ================= 3. 图像预处理 =================                        Mat img = Cv2.ImRead(imagePath);                         if (img.Empty())                        {                            Logger.Instance.INFO( "Image not found!" );                             return ;                        }                        sw.Start();                         float [] inputData = PreProcess(img, out float scale, out int xOffset, out int yOffset);                        sw.Stop();                        Logger.Instance.INFO( $"Pre-processing time: {sw.ElapsedMilliseconds} ms" );                         // ================= 4. 推理 =================                         // 准备主机内存接收结果                         float [] outputData = new float [outputChannels * OutputSize];                        sw.Restart();                         // 将数据从主机 拷贝到设备                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);                         // 执行推理 (enqueueV3 是异步的)                        executionContext.executeV3(cudaStream);                         // 等待推理完成                        cudaStream.Synchronize();                                   // 将结果从设备 拷贝回主机                         // 这里的拷贝是同步的,会等待 GPU 计算完成                        outputGpuMemory.copyToHostAsync(outputData, cudaStream);                        sw.Stop();                        Logger.Instance.INFO( $"Inference time: {sw.ElapsedMilliseconds} ms" );                         // ================= 5. 后处理 =================                        sw.Restart();                        List<DetData> results = PostProcess(outputData, scale, xOffset, yOffset);                        sw.Stop();                        Logger.Instance.INFO( $"Post-processing time: {sw.ElapsedMilliseconds} ms" );                         // ================= 6. 结果可视化 =================                        Mat resultImg = DrawDetResult(results, img);                        Cv2.ImShow( "YOLO11-DET Result" , resultImg);                        Cv2.WaitKey( 0 );                    }                }            }        }         /// <summary>         /// 图像预处理:Letterbox 缩放、归一化、HWC 转 CHW         /// </summary>         private static float[] PreProcess( Mat img, out float scale, out int xOffset, out int yOffset)        {             // 转换颜色空间 BGR -> RGB            Mat rgbImg = new Mat();            Cv2.CvtColor(img, rgbImg, ColorConversionCodes.BGR2RGB);             // 计算 Letterbox 缩放比例             int maxDim = Math.Max(rgbImg.Width, rgbImg.Height);            scale = ( float )maxDim / InputSize;             // 计算缩放后的尺寸             int newWidth = ( int )(rgbImg.Width / scale);             int newHeight = ( int )(rgbImg.Height / scale);             // Resize 图像            Mat resizedImg = new Mat();            Cv2.Resize(rgbImg, resizedImg, new Size(newWidth, newHeight));             // 创建黑色背景 Canvas (InputSize x InputSize)            Mat paddedImg = Mat.Zeros(InputSize, InputSize, MatType.CV_8UC3);             // 计算粘贴位置 (居中)            xOffset = (InputSize - newWidth) / 2 ;            yOffset = (InputSize - newHeight) / 2 ;             // 将图像拷贝到 Canvas 中央            Rect roi = new Rect(xOffset, yOffset, newWidth, newHeight);            resizedImg.CopyTo( new Mat(paddedImg, roi));             // 归一化 (0-255 -> 0-1) 并转为 float 类型            Mat floatImg = new Mat();            paddedImg.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0 );             // HWC 转 CHW 并展平为一维数组            Mat[] channels = Cv2.Split(floatImg);             float [] chwData = new float [ 3 * InputSize * InputSize];             // 拷贝数据:R通道 -> C通道 -> B通道 (OpenCV Split 出来顺序是 B, G, R,对应索引 0, 1, 2)             int channelSize = InputSize * InputSize;             // 将 R, G, B 依次拷入数组            Marshal.Copy(channels[ 0 ].Data, chwData, 0 , channelSize); // R            Marshal.Copy(channels[ 1 ].Data, chwData, channelSize, channelSize); // G            Marshal.Copy(channels[ 2 ].Data, chwData, channelSize * 2 , channelSize); // B             // 释放临时 Mat            rgbImg.Dispose();            resizedImg.Dispose();            paddedImg.Dispose();            floatImg.Dispose();             foreach ( var c in channels) c.Dispose();             return chwData;        }         /// <summary>         /// 后处理:解析 TensorRT 输出、NMS 过滤         /// </summary>         private static List<DetData> PostProcess( float[] result, float scale, int xOffset, int yOffset)        {            List<Rect> boxes = new List<Rect>();            List< float > confidences = new List< float >();            List< int > classIds = new List< int >();             // 遍历所有预测框 (OutputSize)             // 数据布局: [4(box) + 80(classes)] * OutputSize             // 展平数组中,同一属性的数据是连续存储的,例如所有 cx 在一起,所有 cy 在在一起...             int stride = OutputSize; // 步长,不同属性在数组中的偏移量             for ( int i = 0 ; i < OutputSize; i++)            {                 // 查找最大类别概率及其索引                 float maxConf = 0 ;                 int maxClassId = -1 ;                 // 遍历类别                 for ( int c = 0 ; c < CategoryNum; c++)                {                     // 数组索引:(坐标/角度偏移量 + 类别偏移) * 框索引                     // 注意:原始代码中 result[outputSize * j + i] 这种访问方式基于 Transposed 数据布局                     float conf = result[( 4 + c) * stride + i];                     if (conf > maxConf)                    {                        maxConf = conf;                        maxClassId = c;                    }                }                 // 置信度过滤                 if (maxConf > ConfThreshold)                {                     // 提取坐标 (cx, cy, w, h)                     float cx = result[ 0 * stride + i];                     float cy = result[ 1 * stride + i];                     float w = result[ 2 * stride + i];                     float h = result[ 3 * stride + i];                     // 还原坐标到原图尺寸                     int rx = ( int )((cx - xOffset - 0.5 * w) * scale);                     int ry = ( int )((cy - yOffset - 0.5 * h) * scale);                     int rw = ( int )(w * scale);                     int rh = ( int )(h * scale);                    boxes.Add( new Rect(rx, ry, rw, rh));                    confidences.Add(maxConf);                    classIds.Add(maxClassId);                }            }             // 执行 NMS (旋转框 NMS)             // OpenCV 的 NMSBoxes 支持 RotatedRect             int [] indices;            CvDnn.NMSBoxes(boxes, confidences, ConfThreshold, NmsThreshold, out indices);            List<DetData> finalResults = new List<DetData>();             foreach ( int idx in indices)            {                finalResults.Add( new DetData                {                    index = classIds[idx],                    score = confidences[idx],                    box = boxes[idx]                });            }             return finalResults;        }         /// <summary>         /// 绘制检测结果(水平矩形框)         /// </summary>         /// <param name="results">检测结果列表 </param>         /// <param name="image">原始图像 </param>         /// <returns>绘制后的图像 </returns>         public static Mat DrawDetResult( List<DetData> results, Mat image)        {             // 克隆图像以免修改原图            Mat mat = image.Clone();             foreach ( var item in results)            {                 // 1. 绘制矩形框                 // Rect 结构包含 X, Y, Width, Height                Cv2.Rectangle(mat, item.box, new Scalar( 0 , 255 , 0 ), thickness: 2 );                 // 2. 准备标签文本 (类别ID - 置信度)                 string label = $" {item.index} - {item.score:F2}" ;                 // 3. 计算文本的尺寸,用于绘制背景                 int baseLine = 1 ;                Size textSize = Cv2.GetTextSize(label, HersheyFonts.HersheySimplex, 0.6 , 1 , out baseLine);                 // 4. 绘制标签背景(半透明黑色矩形),防止文字与背景混淆                 // 位置:矩形左上角略微上移,或者直接贴着左上角                Point labelPosition = new Point(item.box.X, item.box.Y - ( int )textSize.Height - 5 );                 // 确保标签不画出图像边界                 if (labelPosition.Y < 0 ) labelPosition.Y = item.box.Y + ( int )textSize.Height + 5 ;                Rect labelBgRect = new Rect(labelPosition.X,                                            labelPosition.Y - ( int )textSize.Height, // OpenCV GetTextSize 返回的高度是基线到底部的距离,需调整                                            ( int )textSize.Width,                                            ( int )textSize.Height + ( int )baseLine);                 // 如果背景框也在图像范围内,则绘制                 // 注意:这里简化处理,直接画在框上方                Cv2.Rectangle(mat,                               new Point(item.box.X, item.box.Y - textSize.Height - 5 ),                               new Point(item.box.X + textSize.Width, item.box.Y),                               new Scalar( 0 , 255 , 0 ),                               thickness: -1 ); // -1 表示填充                 // 5. 绘制文本(白色文字)                Cv2.PutText(mat,                            label,                             new Point(item.box.X, item.box.Y - 5 ),                            HersheyFonts.HersheySimplex,                             0.6 ,                             new Scalar( 0 , 0 , 0 ),                             1 );


using OpenCvSharp; using OpenCvSharp.Dnn; using System.Diagnostics; using System.Runtime.InteropServices; namespace YoloObbBatchInfer {     internal class Program    {         // ================= 配置参数 =================         // 模型输入尺寸 (宽=高)         private const int InputSize = 1024 ;         // 建议根据实际模型动态获取或使用 Netron 查看         private const int OutputSize = 21504 ;         // 模型类别数 (根据您的具体数据集修改,此处假设为15类)         private const int CategoryNum = 15 ;         // 置信度阈值         private const float ConfThreshold = 0.25f ;         // NMS IOU 阈值         private const float NmsThreshold = 0.3f ;         private const int MaxBatchSize = 24 ;         static void Main( string[] args)        {             //  ============= 配置 TensorRT 日志回调 =============             // 定义一个委托,用于处理 TensorRT 内部产生的日志消息。             // 这允许我们将 C++ 层面的日志输出到 C# 的控制台。            LogCallbackFunction _callbackDelegate = (message) =>            {                Console.WriteLine(message);            };             // 将自定义的回调函数注册给 TensorRT 的全局 Logger 实例。            Logger.Instance.SetCallback(_callbackDelegate);             // 设置日志的严重性级别阈值。             // LoggerSeverity.kINFO: 打印信息、警告和错误。             // 开发调试阶段通常设为 kINFO 或 kVERBOSE;生产环境可设为 kWARNING 或 kERROR 以减少输出。            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);             string enginePath = "yolov8s-obb_b.engine" ;             string [] imagePaths = {                 "P0006.png" , "P0016.png" , "P0456.png" , "P0813.png" };             // ================= 1. 加载 TensorRT Engine =================             // 使用 using 语句确保文件流正确关闭             byte [] engineData;             using (FileStream fs = new FileStream(enginePath, FileMode.Open, FileAccess.Read))             using (BinaryReader br = new BinaryReader(fs))            {                engineData = br.ReadBytes(( int )fs.Length);            }             // 反序列化 Engine             // Runtime 必须在 Engine 生命周期内保持存活,通常建议设为全局或静态,或者确保它最后释放            Runtime runtime = new Runtime();            runtime.setMaxThreads( 10 );             // 创建 CudaEngine (此处使用 using 确保推理完成后引擎被销毁)             using (CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(engineData, ( ulong )engineData.Length))            {                 // ================= 2. 初始化推理上下文与显存 =================                 // 创建执行上下文                 using (JYPPX.TensorRtSharp.Nvinfer.ExecutionContext executionContext = cudaEngine.createExecutionContext(TrtExecutionContextAllocationStrategy.kSTATIC))                 using (CudaStream cudaStream = new CudaStream()) // 创建 CUDA 流用于异步执行                {                     // 获取输入维度信息 (用于校验)                    Dims inputDims = executionContext.getTensorShape( "images" );                    Logger.Instance.INFO( $"Input Shape: {inputDims.d[ 0]}x {inputDims.d[ 1]}x {inputDims.d[ 2]}x {inputDims.d[ 3]}" );                     // 计算所需显存大小                     // 输入: Batch=1, Channel=3, Height=1024, Width=1024                     ulong inputSizeInBytes = MaxBatchSize * 3 * InputSize * InputSize;                     // 输出: Batch=1, Channels=CategoryNum+4(box)+1(angle), Num=8400                     int outputChannels = CategoryNum + 5 ; // 4坐标 + 1角度 + N类别                     ulong outputSizeInBytes = ( ulong )(MaxBatchSize * outputChannels * OutputSize);                    Stopwatch sw = new Stopwatch();                     // 分配 GPU 显存                     using (Cuda1DMemory< float > inputGpuMemory = new Cuda1DMemory< float >(inputSizeInBytes))                     using (Cuda1DMemory< float > outputGpuMemory = new Cuda1DMemory< float >(outputSizeInBytes))                    {                         // 绑定显存地址到 TensorRT 上下文                        executionContext.setInputTensorAddress( "images" , inputGpuMemory. get ());                        executionContext.setOutputTensorAddress( "output0" , outputGpuMemory. get ());                         // 关键一步,修改本次推理的形状                        executionContext.setinputShape( "images" , new Dims(imagePaths.Count(), 3 , 1024 , 1024 ));                         // 预热推理 (可选,但推荐,尤其推理时)                        executionContext.executeV3(cudaStream);                        cudaStream.Synchronize();                         // ================= 3. 图像预处理 =================                        List<Mat> images = new List<Mat>();                         foreach ( var path in imagePaths)                        {                            Mat img = Cv2.ImRead(path);                             if (img.Empty())                            {                                Logger.Instance.INFO( "Image not found!" );                                 return ;                            }                            images.Add(img);pluto.xiangxiu.net.cn                        }                        ( float [] inputData1, float [] scales1, int [] xOffsets1, int [] yOffsets1) = PreProcessBatch(images);                        sw.Start();                        ( float [] inputData, float [] scales, int [] xOffsets, int [] yOffsets) = PreProcessBatch(images);                        sw.Stop();                        Logger.Instance.INFO( $"Pre-processing time: {sw.ElapsedMilliseconds} ms" );                         // ================= 4. 推理 =================                         // 准备主机内存接收结果                         float [] outputData1 = new float [imagePaths.Count() * outputChannels * OutputSize];                         // 将数据从主机 拷贝到设备                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);                         // 执行推理 (enqueueV3 是异步的)                        executionContext.executeV3(cudaStream);                         // 等待推理完成                        cudaStream.Synchronize();                         // 将结果从设备 拷贝回主机                         // 这里的拷贝是同步的,会等待 GPU 计算完成                        outputGpuMemory.copyToHostAsync(outputData1, cudaStream);                        sw.Restart();                         // 准备主机内存接收结果                         float [] outputData = new float [imagePaths.Count() * outputChannels * OutputSize];                         // 将数据从主机 拷贝到设备                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);                         // 执行推理 (enqueueV3 是异步的)                        executionContext.executeV3(cudaStream);                         // 等待推理完成                        cudaStream.Synchronize();                         // 将结果从设备 拷贝回主机                         // 这里的拷贝是同步的,会等待 GPU 计算完成                        outputGpuMemory.copyToHostAsync(outputData, cudaStream);                        sw.Stop();                        Logger.Instance.INFO( $"Inference time: {sw.ElapsedMilliseconds} ms" );                         // ================= 5. 后处理 =================                        List<List<ObbData>> results1 = PostProcessBatch(outputData, scales, xOffsets, yOffsets);                        sw.Restart();                        List<List<ObbData>> results = PostProcessBatch(outputData, scales, xOffsets, yOffsets);                        sw.Stop();                        Logger.Instance.INFO( $"Post-processing time: {sw.ElapsedMilliseconds} ms" );                         // ================= 6. 结果可视化 =================                        List<Mat> resultMats = new List<Mat>();                         for ( int i = 0 ; i < results.Count; ++i)                        {                            resultMats.Add(DrawObbResult(results[i], images[i]));                        }                        Mat putResultImgs = StitchHorizontalWithPadding(resultMats);                        Cv2.ImWrite( "YOLO11-OBB Result.png" , putResultImgs);                        Cv2.ImShow( "YOLO11-OBB Result" , putResultImgs);                        Cv2.WaitKey( 0 );app.xiangxiu.net.cn                    }                }            }        }         /// <summary>         /// 图像预处理:Letterbox 缩放、归一化、HWC 转 CHW         /// </summary>         private static ( float[], float[] ,   int[] , int[] ) PreProcessBatch( List<Mat> imgs)        {             int dataLen = 3 * InputSize * InputSize;             float [] chwData = new float [imgs.Count * dataLen];             float [] scales = new float [imgs.Count];             int [] xOffsets = new int [imgs.Count];             int []  yOffsets = new int [imgs.Count];            Parallel.For( 0 , imgs.Count, i =>            {                Mat img = imgs[i];                 // 转换颜色空间 BGR -> RGB                Mat rgbImg = new Mat();                Cv2.CvtColor(img, rgbImg, ColorConversionCodes.BGR2RGB);                 // 计算 Letterbox 缩放比例                 int maxDim = Math.Max(rgbImg.Width, rgbImg.Height);                scales[i] = ( float )maxDim / InputSize;                 // 计算缩放后的尺寸                 int newWidth = ( int )(rgbImg.Width / scales[i]);                 int newHeight = ( int )(rgbImg.Height / scales[i]);                 // Resize 图像                Mat resizedImg = new Mat();                Cv2.Resize(rgbImg, resizedImg, new Size(newWidth, newHeight));                 // 创建黑色背景 Canvas (InputSize x InputSize)                Mat paddedImg = Mat.Zeros(InputSize, InputSize, MatType.CV_8UC3);                 // 计算粘贴位置 (居中)                xOffsets[i] = (InputSize - newWidth) / 2 ;                yOffsets[i] = (InputSize - newHeight) / 2 ;                 // 将图像拷贝到 Canvas 中央                Rect roi = new Rect(xOffsets[i], yOffsets[i], newWidth, newHeight);                resizedImg.CopyTo( new Mat(paddedImg, roi));                 // 归一化 (0-255 -> 0-1) 并转为 float 类型                Mat floatImg = new Mat();tg.xiangxiu.net.cn                paddedImg.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0 );                 // HWC 转 CHW 并展平为一维数组                Mat[] channels = Cv2.Split(floatImg);                 // 拷贝数据:R通道 -> C通道 -> B通道 (OpenCV Split 出来顺序是 B, G, R,对应索引 0, 1, 2)                 int channelSize = InputSize * InputSize;                 // 将 R, G, B 依次拷入数组                Marshal.Copy(channels[ 0 ].Data, chwData, dataLen * i, channelSize); // R                Marshal.Copy(channels[ 1 ].Data, chwData, dataLen * i + channelSize, channelSize); // G                Marshal.Copy(channels[ 2 ].Data, chwData, dataLen * i + channelSize * 2 , channelSize); // B                 // 释放临时 Mat                rgbImg.Dispose();                resizedImg.Dispose();                paddedImg.Dispose();                floatImg.Dispose();           ltalk.xiangxiu.net.cn      foreach ( var c in channels) c.Dispose();            });             return (chwData, scales, xOffsets, yOffsets);        }         /// <summary>         /// 后处理:解析 TensorRT 输出、NMS 过滤         /// </summary>         private static List<List<ObbData>> PostProcessBatch( float [] result, float [] scales, int [] xOffsets, int [] yOffsets)        {            List<ObbData>[] obbDatas = new List<ObbData>[scales.Length];            Parallel.For( 0 , scales.Length, b =>            {dbg.xiangxiu.net.cn                List<RotatedRect> boxes = new List<RotatedRect>();                List< float > confidences = new List< float >();                List< int > classIds = new List< int >();    proxy.xiangxiu.net.cn            // 遍历所有预测框 (OutputSize)                 // 数据布局: [4(box) + 15(classes) + 1(angle)] * OutputSize                 // 展平数组中,同一属性的数据是连续存储的,例如所有 cx 在一起,所有 cy 在在一起...                 int stride = OutputSize; // 步长,不同属性在数组中的偏移量                 int resultDataOffset = OutputSize * (CategoryNum + 5 ) * b;                 for ( int i = 0 ; i < OutputSize; i++)                {                     // 查找最大类别概率及其索引                     float maxConf = 0 ;                     int maxClassId = -1 ;                     // 遍历类别                     for ( int c = 0 ; c < CategoryNum; c++)                    {           whzp.xiangxiu.net.cn              // 数组索引:(坐标/角度偏移量 + 类别偏移) * 框索引                         // 注意:原始代码中 result[outputSize * j + i] 这种访问方式基于 Transposed 数据布局                         float conf = result[( 4 + c) * stride + i + resultDataOffset];                         if (conf > maxConf) vi.xiangxiu.net.cn                        {                            maxConf = conf;img03.xiangxiu.net.cn                            maxClassId = c;                        }                    }                     // 置信度过滤                     if (maxConf > ConfThreshold)                    {                         // 提取坐标 (cx, cy, w, h)                         float cx = result[ 0 * stride + i + resultDataOffset];                         float cy = result[ 1 * stride + i + resultDataOffset];                         float w = result[ 2 * stride + i + resultDataOffset];                         float h = result[ 3 * stride + i + resultDataOffset];                         // 提取角度 (通常在第 5 个位置,即类别之前)                         float angleRad = result[(CategoryNum + 4 ) * stride + i + resultDataOffset];                         // 还原坐标到原图尺寸                         float rx = (cx - xOffsets[b]) * scales[b];                         float ry = (cy - yOffsets[b]) * scales[b];                         float rw = w * scales[b];      jthr.xiangxiu.net.cn                   float rh = h * scales[b];    macime.xiangxiu.net.cn                     // 将弧度转换为角度                         // Normalize angle to [-π/2, π/2] range                         // 将角度归一化到[-π/2, π/2]范围                         if (angleRad >= Math.PI && angleRad <= 0.75 * Math.PI)                        {i03.xiangxiu.net.cn                            angleRad -= ( float )Math.PI;                        }           file01.xiangxiu.net.cn              float angleDeg = angleRad * ( float )( 180f / Math.PI);   // Convert to degrees/转换为角度制                        boxes.Add( new RotatedRect( new Point2f(rx, ry), new Size2f(rw, rh), angleDeg));                        confidences.Add(maxConf);                        classIds.Add(maxClassId);zhishi.xiangxiu.net.cn                    }                }                 // 执行 NMS (旋转框 NMS)                 // OpenCV 的 NMSBoxes 支持 RotatedRect                 int [] indices;fair.xiangxiu.net.cn                CvDnn.NMSBoxes(boxes, confidences, ConfThreshold, NmsThreshold, out indices);                List<ObbData> finalResults = new List<ObbData>();      3g.xiangxiu.net.cn           foreach ( int idx in indices)                {                    finalResults.Add( new ObbData                    {music.xiangxiu.net.cn                        index = classIds[idx],                        score = confidences[idx],                        box = boxes[idx]                    });                }                obbDatas[b] = finalResults;            });           pay.xiangxiu.net.cn             return obbDatas.Select(x => x?.ToList() ?? new List<ObbData>()).ToList();        }         /// <summary>         /// 绘制旋转检测结果         /// </summary>  p4p.xiangxiu.net.cn         public static Mat DrawObbResult( List<ObbData> results, Mat image)        {             // 克隆图像以免修改原图            Mat mat = image.Clone();             foreach ( var item in results)            {                 // 获取旋转矩形的四个顶点                Point2f[] points = item.box.Points();                 // 绘制多边形框                 for ( int j = 0 ; j < 4 ; j++)mall.xiangxiu.net.cn                {x1.xiangxiu.net.cn                    Cv2.Line(mat, (Point)points[j], (Point)points[(j + 1 ) % 4 ],                             new Scalar( 0 , 255 , 0 ), 2 );                }                 // 绘制标签 (类别 - 置信度)                 string label = $" {item.index} - {item.score:F2}" ;kxbz.xiangxiu.net.cn                Point2f textPos = points[ 0 ]; // 左上角                Cv2.PutText(mat, label, (Point)textPos, HersheyFonts.HersheySimplex, 0.8 ,                             new Scalar( 255 , 0 , 0 ), 2 );            }             return mat;tuijian.xiangxiu.net.cn        }         public class ObbData        {             public int index;             public float score;             public RotatedRect box;        }         /// <summary>pv.xiangxiu.net.cn         /// 智能水平拼接:自动处理高度不一致的图片         /// </summary>  dev.xiangxiu.net.cn         /// <param name="images">图片列表 </param>         /// <param name="backgroundColor">填充背景颜色,默认为黑色 </param>         /// <returns>拼接后的 Mat </returns>         public static Mat StitchHorizontalWithPadding( List<Mat> images, Scalar? backgroundColor = null)        {             if (images == null || images.Count == 0 )                 return new Mat();             // 1. 找到所有图片中的最大高度             int maxHeight = images.Max(img => img.Rows);             // 计算总宽度             int totalWidth = images.Sum(img => img.Cols);             // 2. 准备结果画布            Mat result = new Mat(maxHeight, totalWidth, images[ 0 ].Type(), backgroundColor ?? Scalar.Black);             // 3. 将每一张图片复制到画布的对应位置             int currentX = 0 ; // 当前 X 轴偏移量             foreach ( var img in images)            {                 if (img.Empty()) continue ;                 // 计算当前图片需要垂直偏移多少(底部对齐逻辑)                 // 如果想顶部对齐,yOffset = 0   smtp.xiangxiu.net.cn                 // 如果想居中,yOffset = (maxHeight - img.Rows) / 2                 int yOffset = maxHeight - img.Rows;        ash.xiangxiu.net.cn           // 定义 ROI (感兴趣区域)                Rect roi = new Rect(currentX, yOffset, img.Cols, img.Rows);                 // 将原图片拷贝到结果图的 ROI 区域                img.CopyTo( new Mat(result, roi));mjh.xiangxiu.net.cn

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048957/viewspace-3109461/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2026-01-07

  • 博文量
    108
  • 访问量
    102118