标签归档:opencv

利用OPENCV和CUDA实现的基于Gabor滤波的曲线检测算法

opencv中有现成的边缘检测和线检测算法,也有对应的gpu实现。但没有曲线检测的算法。下面就实现一个基于Gabor滤波的曲线检测算法,并使用gpu来提高运算速度。算法参考了GitHub上的代码,通过控制Gabor滤波的参数来控制检测的线宽等特征,并使用了FFT来代替卷积从而提升速度。

1.算法来源

算法参考了https://github.com/Po-Ting-lin/HairRemoval里面的代码。

2.算法流程

  • 根据预输入的参数,生成Gabor滤波矩阵。不同方向生成的矩阵不一样,最多8个方向。
  • 将各个Gabor滤波矩阵与原图片进行卷积。为了加快运行速度,使用了fft来加速卷积的运算速度
  • 取卷积后的图片各方向的最大值组成最后的结果输出
  • 根据实际进行2值化等处理,输出检测出的曲线

3.代码

头文件 DetectLineCUDA.h 的主要部分。关键是构造函数,输入了Gabor滤波的参数,可以用来调整检测曲线的宽带,长度等。

class DetectLineCUDA
{
public:
    DetectLineCUDA(int cols, int rows, std::vector<int> _angles,int MinArea = 20,float Alpha = 1.15f,float Beta = 0.55f,float HairWidth = 12.0f);
	void detect(cv::cuda::GpuMat& src, cv::cuda::GpuMat& mask);
	~DetectLineCUDA();

private: 
      .............
};

#endif // DETECTLINECUDA_H

类文件 DetectLineCUDA.cpp

  • 构造函数初始化了一些变量
  • detect函数是算法运行的主函数,通过_initGaborFilterCube函数生成滤波矩阵,随后通过一个循环进行快速傅里叶,计算各个方向上的卷积。最后通过_cubeReduction函数合并结果
DetectLineCUDA::DetectLineCUDA(int cols, int rows, std::vector<int> _angles, int MinArea, float Alpha, float Beta, float HairWidth)
{
	Width = cols;
	Height = rows;

    NumberOfFilter = 8;
    this->MinArea = MinArea;
    this->Alpha = Alpha;
    this->Beta = Beta;
    this->HairWidth = HairWidth;

	SigmaX = 8.0f * (sqrt(2.0 * log(2) / CV_PI)) * HairWidth / Alpha / Beta / CV_PI;
	SigmaY = 0.8f * SigmaX;
	KernelRadius = ceil(3.0f * SigmaX);  // sigmaX > sigamY
	KernelW = 2 * KernelRadius + 1;
	KernelH = 2 * KernelRadius + 1;
	KernelX = KernelRadius;
	KernelY = KernelRadius;
	FFTH = snapTransformSize(Height + KernelH - 1);
	FFTW = snapTransformSize(Width + KernelW - 1);

	cufftPlan2d(&_fftPlanFwd, FFTH, FFTW, CUFFT_R2C);
	cufftPlan2d(&_fftPlanInv, FFTH, FFTW, CUFFT_C2R);

    this->angles.insert(this->angles.end(), _angles.begin(), _angles.end());
}

int DetectLineCUDA::snapTransformSize(int dataSize) {
	........
}
........
void DetectLineCUDA::detect(cv::cuda::GpuMat& src, cv::cuda::GpuMat& mask) {
	float* d_PaddedData;
	float* d_Kernel;
	float* d_PaddedKernel;
	float* d_DepthResult;
        ........

	//// init data
	float* h_kernels = _initGaborFilterCube();
    cudaMemcpy(d_Kernel, h_kernels, KernelH * KernelW * NumberOfFilter * sizeof(float), cudaMemcpyHostToDevice);

	cuda::GpuMat src_f;
	if (src.channels() == 3) {
		cuda::GpuMat src_gray;
		src.convertTo(src_gray, CV_32FC3);
		cuda::cvtColor(src_gray, src_f,cv::COLOR_BGR2GRAY);
	}
	else {
		src.convertTo(src_f, CV_32F);
	}

	cuda::GpuMat d_src_c_ptr_mat(src.rows, src.cols, CV_32FC1, d_src_c_ptr);

	cuda::GpuMat pand_mat(FFTH, FFTW, CV_32FC1, d_PaddedData);
	cuda::copyMakeBorder(src_f, d_src_c_ptr_mat, 0, FFTH - src.rows, 0, FFTW - src.cols, BORDER_WRAP);
	d_src_c_ptr_mat.copyTo(pand_mat);

	// FFT data
	cufftExecR2C(_fftPlanFwd, (cufftReal*)d_PaddedData, d_DataSpectrum);
	cudaDeviceSynchronize();

    for (int i = 0; i < angles.size(); i++) {
        int kernel_offset = angles[i] * KernelH * KernelW;
		int data_offset = i * FFTH * FFTW;

		_padKernel(d_PaddedKernel, d_Kernel + kernel_offset);

		cv::cuda::GpuMat raw_dst_mat(FFTH, FFTW, CV_32FC1, d_PaddedKernel);
		cv::Mat cpumat;
		raw_dst_mat.download(cpumat);

		// FFT kernel
        cufftExecR2C(_fftPlanFwd, (cufftReal*)d_PaddedKernel, (cufftComplex*)d_KernelSpectrum);
        cudaDeviceSynchronize();

		//// mul
		_modulateAndNormalize(d_TempSpectrum, d_DataSpectrum, d_KernelSpectrum, 1);
        cufftExecC2R(_fftPlanInv, (cufftComplex*)d_TempSpectrum, (cufftReal*)(&d_DepthResult[data_offset]));
        cudaDeviceSynchronize();


		cv::cuda::GpuMat d_DepthResult_mat(FFTH, FFTW, CV_32FC1, d_DepthResult + data_offset);
		d_DepthResult_mat.download(cpumat);
	}

	_cubeReduction(d_DepthResult, d_Result);

	cuda::GpuMat d_Result_mat(Height , Width,CV_8UC1, d_Result);
	d_Result_mat.copyTo(mask);
.........
}

float* DetectLineCUDA::_initGaborFilterCube() {
	float* output = new float[KernelW * KernelH * NumberOfFilter];
	float* output_ptr = output;
	for (int curNum = 0; curNum < NumberOfFilter; curNum++) {
		float theta = (float)CV_PI / NumberOfFilter * curNum;
		for (int y = -KernelRadius; y < KernelRadius + 1; y++) {
			for (int x = -KernelRadius; x < KernelRadius + 1; x++, output_ptr++) {
				float xx = x;
				float yy = y;
				float xp = xx * cos(theta) + yy * sin(theta);
				float yp = yy * cos(theta) - xx * sin(theta);
				*output_ptr = exp((float)(-CV_PI) * (xp * xp / SigmaX / SigmaX + yp * yp / SigmaY / SigmaY)) * cos((float)CV_2PI * Beta / HairWidth * xp + (float)CV_PI);
			}
		}
	}
	return output;
}

cuda 文件 DetectLinekernel.cu

里面的一些函数可以使用opencv里的函数代替,但感觉意义不大,就直接使用原来的了。

函数调用的方法

        Mat inputmat = imread("test.bmp",cv::IMREAD_GRAYSCALE);
        cv::bitwise_not(inputmat,inputmat);//算法找寻的是黑色的曲线,所以将原始图片取反
        cuda::GpuMat inputgpu;
        inputgpu.upload(inputmat);

        std::vector<int> angles;
        angles.push_back(ANGLE_0);
        angles.push_back(ANGLE_23);
        angles.push_back(ANGLE_45);
        angles.push_back(ANGLE_68);
        angles.push_back(ANGLE_90);
        angles.push_back(ANGLE_113);
        angles.push_back(ANGLE_135);
        angles.push_back(ANGLE_157);
        DetectLineCUDA dl(inputmat.cols, inputmat.rows,angles,20,1.95f,0.85f,8.0f);
        cv::cuda::GpuMat m_g = cuda::createContinuous(inputmat.size(), CV_8UC1);
        dl.detect(inputgpu, m_g);

        Mat m;
        m_g.download(m);
        imwrite("m.bmp", m);// m 就是分析出的曲线,后续可以再进行2值化,去掉一些干扰

算法效果

使用TorchSharp 调用 PyTorch 语义分割神经网络模型

TorchSharp 是对 Torch c++的封装,基本继承了c++的全部接口。但使用中会有一些小问题,需要特别注意一些。

  1. 语义分割(semantic segmentation)神经网络训练

训练的代码可以参考github里的官方代码 https://github.com/pytorch/vision/tree/main/references/segmentation

2.模型输出

官方代码的模型 默认输出是list 虽然可以强制输出script文件,但TorchSharp 调用后会报错”Expected Tensor but got GenericDict”.因此需要修改网络

export代码如下:

import torch
import torchvision
from torch import nn
import numpy as np

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self._model = torchvision.models.segmentation.lraspp_mobilenet_v3_large(num_classes=2,
                aux_loss=False,
                pretrained=False)
        
        #修改了输入,将输入改为单通道图片
        #如果输入的是3通道 则不需要修改
        for item in self._model.backbone.items():          
            item[1][0] = nn.Conv2d(1, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
            break                                                         
                                                                 
        checkpoint = torch.load('model/model_119.pth', map_location='cpu')
        self._model.load_state_dict(checkpoint['model'], strict=not True)
        self._model.eval()
        
    def forward(self, x):
        # 修改了输出,将List修改为Tensor 并进行了 argmax 和 转float操作
        result = self._model.forward(x)
        return result["out"].argmax(1).flatten().float()


model = MyModel()
x = torch.rand(1,1, 240, 400)
predictions = model(x)

#使用torch.jit.trace 输出 script pt 网络文件
traced_script_module = torch.jit.trace(model, x)
traced_script_module.save('_seg.pt') 

3.使用TorchSharp 进行预测

由于测试程序使用了opencvsharp,所以下面的代码使用了opencv来读取图片和简单的数据预处理,并使用opencv可视化输出

//导入网络
torch.jit.ScriptModule torch_model;
torch_model = torch.jit.load("_seg.pt");

//导入图片,并使用BlobFromImage 进行数据类型 维度 和归一化的转换
Mat temp = Cv2.ImRead("Pic_42633.bmp", ImreadModes.Grayscale);
Mat tensor_mat = OpenCvSharp.Dnn.CvDnn.BlobFromImage(temp, 1 / 255.0);

//初始化输入的tensor
float[] data_bytes = new float[tensor_mat.Total()];
Marshal.Copy(tensor_mat.Data, data_bytes, 0, data_bytes.Length);
torch.Tensor x = torch.tensor(data_bytes, torch.ScalarType.Float32);

//维度转换 和 normalize(进行normalize是因为官方代码里有这一步处理)
x = x.reshape(1, 1, 240, 400);
x = TorchSharp.torchvision.transforms.functional.normalize(x, new double[] { 0.485 }, new double[] { 0.229 });

DateTime date1 = DateTime.Now;
//进行预测
torch.Tensor _out = torch_model.forward(x);
DateTime date2 = DateTime.Now;
TimeSpan ts = date2 - date1;
Console.WriteLine("No. of Seconds (Difference) = {0}", ts.TotalMilliseconds);
Console.WriteLine(_out);

//使用opencv 将预测出的tensor输出为可视化的图片
Mat result_mat = new Mat(240, 400, MatType.CV_32FC1, _out.bytes.ToArray());
result_mat.ConvertTo(result_mat, MatType.CV_8UC1);
Cv2.ImWrite("mask.bmp", result_mat);

Opencv Inpaint use CUDA Backend

opencv只有cpu接口的Inpaint函数,对于需要使用CUDA进行图片处理时,反复内存显存迁移数据会影响计算速度。在不考虑填充效果十分好的情况下,可以使用如下CUDA算法,简单的进行填充。

算法来源

算法主要参考了https://github.com/Po-Ting-lin/HairRemoval.git 中的填充代码。该项目主要是去除皮肤上的毛发。对于其如何寻找需要填充的区域就不讨论了,直接使用其分析出的mask图进行填充。

该填充算法对细线和小面积的填充效果还可以 大面积的 效果就很一般了

继续阅读

OPENCV + CUDA 实现小波滤波

opencv中有cpu 和 gpu版本的DFT函数,及傅里叶变换的函数,可以实现dft滤波。但opencv中没有DWT,及小波变换。下面将介绍一下实现的方法。

傅里叶变换与小波变换都能实现滤波,不好说那个更好。但傅里叶变换有个缺点,对于图像处理来说,其在处理图像锐利边缘时,很容易出现边缘抖动的情况。原因如下图:

傅里叶变换只能使用余弦波,很难拟合出突变。具体的解释和小波变换的特点。可以参考知乎上的文章 https://zhuanlan.zhihu.com/p/44215123

代码部分主要参考了GitHub上实现方法,经过一些简单的修改以适应vc++,和高频滤波的需要。原链接https://github.com/pierrepaleo/PDWT

继续阅读

Jetson nano 获取CSI相机RAW图片并转换为Opencv Mat

树莓派可以通过加入 参数 “bayer=True” ,来获取CSI相机的raw图片,raw数据会紧跟在jpg图片的末尾,具体提取的方法不在累述,可以方便的google到。而Nvidia的 Jetson nano要获取raw图片的方法网上比较零散,故整理了一下。

获取raw图片的方法其实比较简单,使用的是jetson nano中自带的Libargus api。其api介绍可以参考https://docs.nvidia.com/jetson/l4t-multimedia/group__LibargusAPI.html,api架构可以参考https://docs.nvidia.com/jetson/archives/l4t-archived/l4t-3231/index.html#page/Tegra%20Linux%20Driver%20Package%20Development%20Guide/jetson_xavier_camera_soft_archi.html

相关代码在 Libargus api demo中的 argus\samples\oneShot 简单修改而来。并增加了转换为opencvmat的代码

继续阅读

Opencv中使用CUDA原函数

opencv中的cuda模块封装了大部分常用的图像处理函数。但一些函数只提供了8bit图片的接口,没有16bit图片的接口。如果需要处理10bit 12bit或更高big的图片就需要调用CUDA的原型函数了。下面就简单举例使用opencv中的GpuMat调用cuda原函数的方法。

opencv中存储GPU图片的类型为GpuMat,不需要考虑显存的分配和释放,使用起来比较方便。而CUDA使用的是Npp8u* Npp16u*等指针,这里就涉及指针的转换。以nppiLUTPalette_16u_C1R 函数举例。

Mat img = ….. //读取一张12bit图片
cuda::GpuMat src_img; //初始化Gpu
src_img.upload(img); //上传图片到Gpu
Mat lut(1, 4096, CV_16UC1, lineardata.data()); // 导入lut数据
cuda::GpuMat gpu_lut = new cv::cuda::GpuMat(1, 4096, CV_16UC1);
gpu_lut.upload(lut); //上传lut数据到Gpu,lut数据也要放入Gpu中
cuda::GpuMat dst_gpu(src_img.size(), CV_16UC1); //初始化转化后的GpuMat
NppiSize oSizeROI = { src_img.cols, src_img.rows }; //设置ROI 这里是整张图
//使用prt获取GpuMat的指针
NppStatus status1 = nppiLUTPalette_16u_C1R(src_img.ptr(), static_cast(src_img.step), dst_gpu.ptr(), static_cast(dst_gpu.step), oSizeROI, gpu_lut.ptr(),16);

得到的dst_gpu就是经过lut后的图片,后续的使用也不需要考虑显存的分配和释放。

Opencv中使用cuda进行 dft 与 idft滤波运算

opencv源代码中包含了dft的demo,但没有使用cuda的demo。下文会简单给出一个cuda例程,并进行简单的高频滤波。

为方便说明,将程序分成了若干部分
1.头文件

#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/core/cuda.hpp>
#include <opencv2/cudaarithm.hpp>

using namespace cv;

2.导入图片加载到GPU,为简单使用了单色图

继续阅读

opencv 中使用 cudnn 预测CNN网络

opencv从3版本开始就已经支持CNN网络模型的预测,到4版本,主流工具tensorflow,pytorch 生成的模型文件大部分都可以支持。但其一直没有使用到CUDNN。但最新发布的4.2版本的opencv已经支持CUDNN了。以下是功能测试

一。安装编译环境

CUDNN的安装方法可以参考tensorflow中关于cudnn的安装方法https://www.tensorflow.org/install/gpu 接下来也会使用tensorflow作为例子。

二。编译opencv

编译方法参考链接https://docs.opencv.org/4.2.0/d3/d52/tutorial_windows_install.html 其中为了加入CUDNN的支持,有以下需要设置一下

1.勾选cuda和cudnn

2.设置CUDA_ARCH_BIN
cudnn不支持3.5下的显卡。需要根据自己使用的显卡来设置,列表参考https://developer.nvidia.com/cuda-gpus

继续阅读