OpencvSharp 中使用 cuda

opencvsharp 是 opencv的c#版本,近期有项目使用了opencvsharp来进行图像处理。这个github上星级很高的项目果然是不错的,运行起来比较稳定,没有出现大的问题。但opencvsharp中没有cuda的完整支持,只有最基本的类型支持,无任何算法支持,想用就只能靠自己添加了。作者的解释如下:

大概就是说cuda需要用户自己编译opencv ,没有一个统一版本的dll提供使用,所以就删除了cuda的支持
其实也对,cuda的使用涉及cuda版本,使用的显卡算力等。使用c++版本的opencv时,也是要自己编译的。
但c#上使用就没有办法了吗?还好作者已经打好了基础,提供了GpuMat的支持,并有大量cup版本的函数进行参考。添加起来还是比较容易的。
项目目录 https://github.com/shimat/opencvsharp

编写步骤:

1.vc++编译带cuda的opencv

编译带cuda的opencv方法比较简单,网上随便搜一大堆,就不特别说明了。但我为了调试opencvsharp快一点,所有并没有像opencvsharp作者一样,编译出静态库,而是编译成共享库。这样反复编译,调试c#版时可以快一点。

2.将编译好的头文件和lib文件导入项目

我在opencvsharp4.1 版本的基础上修改代码的,其默认的头文件和库文件目录在 opencv_files_410 文件夹。我将里面的老文件全部删除,替换为自己编译好的文件。

打开opencvsharp项目,修改OpenCvSharpExtern 的包含目录与库目录

由于我用的是共享库,所以其附加依赖项也要修改下,并添加cuda相关的库依赖

3.启用cuda

opencvsharp默认是没有启用cuda的,需要修改 OpenCvSharp.csproj 文件添加 ENABLED_CUDA 和 修改OpenCvSharpExtern 添加预处理定义 ENABLED_CUDA 。

4.添加cuda函数

下面开始关键步骤,添加一个cuda函数。添加一个函数一般需要在4个地方添加代码。以添加一个 cuda.pyrUp(InputArray src, OutputArray dst, Stream& stream = Stream::Null()) 为例

1)在OpenCvSharpExtern 这个c++项目中添加一个c#方便调用的接口函数。为方便函数的管理。我重新建了个头文件来添加函数 文件名 cuda_warping.h

#ifndef _CPP_GPU_WARPING_H_
#define _CPP_GPU_WARPING_H_

#ifdef ENABLED_CUDA

#include "include_opencv.h"
using namespace cv::cuda;

CVAPI(void) cuda_imgproc_pyrUp(cv::_InputArray *src, cv::_OutputArray *dst, Stream* stream)
{
	cv::cuda::pyrUp(*src, *dst, *stream);
}
#endif

#endif

由于添加了一个新头文件,所以 cuda.cpp 也要改一下,将新加的头文件包含进去

// ReSharper disable CppUnusedIncludeDirective
#include "cuda.h"
#include "cuda_GpuMat.h"
#include "cuda_warping.h"

2)在OpenCvSharp项目中,添加导入c++接口的函数。同样为了方便管理,我在PInvoke/cuda下重新建了一个NativeMethods_cuda_warping.cs文件

#if ENABLED_CUDA

using System;
using System.Runtime.InteropServices;

#pragma warning disable 1591

namespace OpenCvSharp {
    // ReSharper disable InconsistentNaming

    public static partial class NativeMethods {

        [DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern void cuda_imgproc_pyrUp(IntPtr src, IntPtr dst, IntPtr stream);

    }
}
#endif

3)添加c#类,编写c#调用的函数,既最终使用的函数。同样我在Modules/cuda建了个新类 cuda_warping.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace OpenCvSharp.Cuda {

    /// <summary>
    ///  GPU warping
    /// </summary>
    public static partial class cuda {
  
        /// <summary>
        /// GPU pyrUp
        /// </summary>
        public static void pyrUp(InputArray src, OutputArray dst, Stream stream = null) {
            if (src == null)
                throw new ArgumentNullException(nameof(src));
            if (dst == null)
                throw new ArgumentNullException(nameof(dst));
            src.ThrowIfDisposed();
            dst.ThrowIfNotReady();

            NativeMethods.cuda_imgproc_pyrUp(src.CvPtr, dst.CvPtr, stream?.CvPtr ?? Stream.Null.CvPtr);
            GC.KeepAlive(src);
            GC.KeepAlive(dst);
            dst.Fix();
        }
    }
}

代码修改参考了cpu版本的pyrup函数。基本就是照抄,改个函数的名字。

4)测试

改完后要测试一下函数能否正常运行,opencvsharp里正好有测试代码,那就照搬吧~~。在OpenCvSharp.Tests 里新建一个测试 GPUTest.cs

using System;
using Xunit;
using Xunit.Abstractions;

namespace OpenCvSharp.Tests {
    // ReSharper disable InconsistentNaming

    public class GPUTest : TestBase {
        public GPUTest(ITestOutputHelper output) : base(output) {

        }

        [Fact]
        public void SimpleGPUTest() {
            Cuda.GpuMat gpumat = new Cuda.GpuMat();
            Cuda.GpuMat gpumat_des = new Cuda.GpuMat();
            Mat src = Image("lenna.png", ImreadModes.Grayscale);
            gpumat.Upload(src);

            Cuda.cuda.pyrUp(gpumat, gpumat_des);

            Mat des = new Mat();
            gpumat_des.Download(des);
            Cv2.ImWrite("test.png",des);        
        }
    }
}

注意,由于是共享dll,需要将动态库dll文件复制到测试项目的运行目录。我就简单的将一堆opencv_xxx410.dll 复制到了 test\OpenCvSharp.Tests\bin\Debug\netcoreapp2.0 文件夹下

效果

5.添加cuda类

cuda中一些运算是封装在类里面的,例如比较常用的canny算法。添加这些稍微复杂一些,但原理一样的,参考原作者cpu写的就好了~~

1)添加c#调用的c++接口。OpenCvSharpExtern项目新建头文件 cuda_imgproc.h 。我只简单实现了detect函数,其实CannyEdgeDetector还有其他设置参数,获取参数的方法,就不添加了,以免代码太复杂,影响理解,需要时可以再添加。总共有4个函数,一个新建,一个销毁,一个运行及detect,一个用来获取类的指针。

#ifndef _CPP_GPU_IMGPROC_H_
#define _CPP_GPU_IMGPROC_H_

#ifdef ENABLED_CUDA

#include "include_opencv.h"
using namespace cv::cuda;

CVAPI(cv::Ptr<CannyEdgeDetector>*) cuda_createCannyEdgeDetector(double low_thresh, double high_thresh, int apperture_size = 3, bool L2gradient = false)
{
	cv::Ptr<CannyEdgeDetector> ptr = cv::cuda::createCannyEdgeDetector(low_thresh, high_thresh, apperture_size, L2gradient);
	return new cv::Ptr<CannyEdgeDetector>(ptr);
}

CVAPI(void) cuda_CannyEdgeDetector_detect(CannyEdgeDetector *obj, cv::_InputArray *image, cv::_OutputArray *edges, Stream* stream)
{
	obj->detect(*image, *edges, *stream);
}

CVAPI(void) cuda_Ptr_CannyEdgeDetector_delete(cv::Ptr<CannyEdgeDetector> *obj)
{
	delete obj;
}

CVAPI(CannyEdgeDetector*) cuda_Ptr_CannyEdgeDetector_get(
	cv::Ptr<CannyEdgeDetector> *ptr)
{
	return ptr->get();
}

#endif

#endif

当然 cuda.cpp 里也要将这个头文件加进去

2)在OpenCvSharp项目中,添加导入c++接口的函数 。新建文件 NativeMethods_cuda_imgproc.cs

#if ENABLED_CUDA

using System;
using System.Runtime.InteropServices;

#pragma warning disable 1591

namespace OpenCvSharp {
    // ReSharper disable InconsistentNaming

    public static partial class NativeMethods {

        [DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern IntPtr cuda_createCannyEdgeDetector(double low_thresh, double high_thresh, int apperture_size = 3, bool L2gradient = false);

        [DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern void cuda_CannyEdgeDetector_detect(IntPtr self, IntPtr image, IntPtr edges, IntPtr stream);

        [DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern void cuda_Ptr_CannyEdgeDetector_delete(IntPtr obj);

        [DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern IntPtr cuda_Ptr_CannyEdgeDetector_get(IntPtr ptr);

    }
}
#endif

3) 添加c# 类。新建文件 CannyEdgeDetector.cs

using System;

namespace OpenCvSharp.Cuda {
    // ReSharper disable InconsistentNaming

    /// <summary>
    /// Creates implementation for cuda::CannyEdgeDetector
    /// </summary>
    public class CannyEdgeDetector : Algorithm {
        /// <summary>
        /// cv::Ptr<T>
        /// </summary>
        private Ptr objectPtr;

        #region Init & Disposal

        /// <summary>
        /// 
        /// </summary>
        /// <param name="low_thresh"></param>
        /// <param name="high_thresh"></param>
        /// <param name="apperture_size"></param>
        /// <param name="L2gradient"></param>
        /// <returns></returns>
        public static CannyEdgeDetector Create(
            double low_thresh, double high_thresh, int apperture_size = 3, bool L2gradient = false) {
            IntPtr ptr = NativeMethods.cuda_createCannyEdgeDetector(
                low_thresh, high_thresh, apperture_size, L2gradient);
            return new CannyEdgeDetector(ptr);
        }

        internal CannyEdgeDetector(IntPtr ptr) {
            this.objectPtr = new Ptr(ptr);
            this.ptr = objectPtr.Get();
        }

        /// <summary>
        /// Releases managed resources
        /// </summary>
        protected override void DisposeManaged() {
            objectPtr?.Dispose();
            objectPtr = null;
            base.DisposeManaged();
        }

        #endregion

        /// <summary>
        /// Finds edges in an image using the @cite Canny86 algorithm.
        /// </summary>
        /// <param name="image"></param>
        /// <param name="edges"></param>
        /// <param name="stream"></param>
        public virtual void detect(InputArray image, OutputArray edges, Stream stream = null) {
            if (image == null)
                throw new ArgumentNullException(nameof(image));
            if (edges == null)
                throw new ArgumentNullException(nameof(edges));
            image.ThrowIfDisposed();
            edges.ThrowIfNotReady();

            NativeMethods.cuda_CannyEdgeDetector_detect(ptr, image.CvPtr, edges.CvPtr, stream?.CvPtr ?? Stream.Null.CvPtr);

            edges.Fix();
            GC.KeepAlive(this);
            GC.KeepAlive(image);
            GC.KeepAlive(edges);
        }

        //#endregion

        internal class Ptr : OpenCvSharp.Ptr {
            public Ptr(IntPtr ptr) : base(ptr) {
            }

            public override IntPtr Get() {
                var res = NativeMethods.cuda_Ptr_CannyEdgeDetector_get(ptr);
                GC.KeepAlive(this);
                return res;
            }

            protected override void DisposeUnmanaged() {
                NativeMethods.cuda_Ptr_CannyEdgeDetector_delete(ptr);
                base.DisposeUnmanaged();
            }
        }
    }
}

代码基本就是参考复制 BackgroundSubtractorMOG.cs 这个类

4)新建测试代码 GPUTest.cs

using System;
using Xunit;
using Xunit.Abstractions;

namespace OpenCvSharp.Tests {
    // ReSharper disable InconsistentNaming

    public class GPUTest : TestBase {
        public GPUTest(ITestOutputHelper output) : base(output) {

        }

        [Fact]
        public void SimplecannyTest() {
            Cuda.GpuMat gpumat = new Cuda.GpuMat();
            Cuda.GpuMat gpumat_des = new Cuda.GpuMat();
            Mat src = Image("lenna.png", ImreadModes.Grayscale);
            gpumat.Upload(src);

            Cuda.CannyEdgeDetector canny = Cuda.CannyEdgeDetector.Create(100, 50);

            canny.detect(gpumat, gpumat_des);

            Mat des = new Mat();
            gpumat_des.Download(des);
            Cv2.ImWrite("test.png", des);
        }
    }
}

效果

结语:上述代码都只是经过了简单的测试,没有经过实际运行环境的测试。长时间运行的稳定性没有测试,但代码主要是参考cpu部分的,问题应该不大。有问题可能会在gc 内存回收上吧~

OpencvSharp 中使用 cuda》有8个想法

  1. 芒果

    博主你好,请问你是用哪个vs版本重新编译项目的,我用vs2019和vs2017都无法顺利编译,打开项目后项目的属性里都无法更改目标平台。报错如下:

    错误 NETSDK1045 当前 .NET SDK 不支持将 .NET Standard 2.1 设置为目标。请将 .NET Standard 2.0 或更低版本设置为目标,或使用支持 .NET Standard 2.1 的 .NET SDK 版本。 OpenCvSharp.Blob C:\Program Files\dotnet\sdk\2.2.109\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets 150
    但我又无法重新设置,请问有遇到此情况吗

    回复
    1. youji 文章作者

      这个应该是你没有安装.NET Core 编译环境。如果你用的是vs2019,可以重新修改安装vs: 在”.net桌面开发”里 勾选”.net core 2.1/2.2″ 开发工具。安装完成后 可能还需要右击”解决方案” -> “还原 NuGet包”

      回复
        1. youji 文章作者

          其中一个C++项目可以在属性里修改编译的框架 其他C#项目通过直接修改各项目的.csproj文件来实现 具体方法可以Google一下 这样可以删掉你不需要的框架 例如net core的

          回复
  2. henry.lee

    博主你好,读过您的文章,感觉您博学多识,让我获益匪浅
    我遇到一个棘手的问题,opencv4.2的dnn已经支持CUDA后台加速,但opencvsharp4.2还没有支持。
    我想在opencvsharp中dnn使用CUDA加速,应该怎么做?
    恕我愚钝,期待您的回复

    回复
    1. youji 文章作者

      这个不难 需要重新编译OpenCVsharp 且不需要改代码 只需要改一些编译选项 我有项目已经成功使用了。简单说 先编译带cudnn的opencv( 需要同时安装CUDA 与 cudnn) 然后编译最新的opencvsharp。 编译方法与文中写的差不多 改改不报错就好了。可能中间有不少坑 Google下应该可以解决~

      回复
      1. henry.lee

        哇,被临幸了,好激动,
        博主,我opencv已经编译OK,c++版本测试过dnn 可用cuda加速。
        我基本上依照博文1,2两步,重新编译了opencvsharp,把生成的dll跟nuget下载的opencvsharp里面的同名dl做了替换,但使用的时候还是报错讲dnn.cpp没有build cuda。困扰了很多天。不知道哪里导致的。你有博文讲这部分吗?
        可以加Q,当面赐教?万分感谢,感激涕零。

        回复
        1. youji 文章作者

          应该是你只替换了c#的dll,没有替换c++的dll。你可以尝试完全删除后,重新添加opencvsharp编译好的dll。除OpenCvSharp.dll等几个外 应该还有一个opencvsharpextensions 和 一堆opencv编译出来的opencvXX420名字的dll都放到程序根目录。总之nuget下的不要了,全换自己编译的。

          回复

发表回复

您的电子邮箱地址不会被公开。