作者归档:youji

TorchSharp 调用 PyTorch 语义分割(semantic segmentation)神经网络

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);

CUDA + subPixel + EDGE 边缘检测

子像素的边缘检测算法很多,但使用CUDA进行的不是很多。github上可以找到一个带CUDA的子像素边缘检测算法,但经过运行发现有些小bug,其cpu版本正常,但gpu版本找轮廓时会偶发轮廓被切断的问题。一下就对其进行一些修正。当然也许还有其他bug~~

一.源项目位置

源项目位置https://github.com/CsCsongor/subPixelEdgeDetect 。算法基于论文https://www.ipol.im/pub/art/2017/216/

二.代码分析

通过分析,主要是因为使用CUDA后,导入了线程。而原有CPU算法是单线程的。在涉及到最优前向轮廓或最优后向轮廓的求解时,会因为多线程,导致没有找到最优轮廓,而保留了2个轮廓信息。
就是源算法中cu_chain_edge_points这个函数里的下面这段代码

if (fwd >= 0 && next[from] != fwd && ((alt = prev[fwd]) < 0 || chain(alt, fwd, Ex, Ey, Gx, Gy, rows, cols) < fwd_s)){
	if (next[from] >= 0){			// Remove previous from-x link if one */
		prev[next[from]] = -1;	// Only prev requires explicit reset  */
	}
	next[from] = fwd;					// Set next of from-fwd link          */
	if (alt >= 0){						// Remove alt-fwd link if one
		next[alt] = -1;					// Only next requires explicit reset
	}
	prev[fwd] = from;					// Set prev of from-fwd link
}
if (bck >= 0 && prev[from] != bck && ((alt = next[bck]) < 0 || chain(alt, bck, Ex, Ey, Gx, Gy, rows, cols) > bck_s)){
		if (alt >= 0){					// Remove bck-alt link if one
			prev[alt] = -1;				// Only prev requires explicit reset
		}
		next[bck] = from;				// Set next of bck-from link
		if (prev[from] >= 0){		// Remove previous x-from link if one
			next[prev[from]] = -1; // Only next requires explicit reset
		}
		prev[from] = bck;				// Set prev of bck-from link
}

而原作者好像也发现了这个问题,在拼接所有轮廓的时候,并没有使用cpu版本里的代码去先找每一个轮廓的起始点,然后再拼接轮廓,而是从任意点开始。这样就会导致轮廓被切断了。
以下list_chained_edge_points 函数中的注释掉的那行代码,就是找轮廓起始点的算法。

// Set k to the beginning of the chain, or to i if closed curve
// for (k = i; (n = prev[k]) >= 0 && n != i; k = n); //这句被注释了,替换成了下面这行
if ((n = prev[i]) >=0 && n != i){ k= n;}

三.算法修改

1.修改cu_chain_edge_points函数。 寻找最优5*5轮廓点的算法屏蔽,先记录下所有向前向后轮廓点

// Chain edge points
__global__
void cu_chain_edge_points(int * next, int * prev, double * Ex,	double * Ey,double * Gx, double * Gy, int rows, int cols){
	int x, y, i , j, alt;
	int dx, dy, to;

	x = blockIdx.x*blockDim.x+threadIdx.x+2;
	y = blockIdx.y*blockDim.y+threadIdx.y+2;

	// Try each point to make local chains
	// 2 pixel margin to include the tested neighbors
	if (x < (rows-2) && y < (cols-2)){
		// Must be an edge point
		if (Ex[x + y*rows] >= 0.0 && Ey[x + y*rows] >= 0.0){
			int from = x + y*rows;  // Edge point to be chained
			double fwd_s = 0.0;  	  // Score of best forward chaining
			double bck_s = 0.0;     // Score of best backward chaining
			int fwd = -1;           // Edge point of best forward chaining
			int bck = -1;           // Edge point of best backward chaining

			/* try all neighbors two pixels apart or less.
				looking for candidates for chaining two pixels apart, in most such cases,
				is enough to obtain good chains of edge points that	accurately describes the edge.
			*/
			for (i = -2; i <= 2; i++){
				for (j = -2; j <= 2; j++){
					to = x + i + (y + j)*rows; // Candidate edge point to be chained

					double s = chain(from, to, Ex, Ey, Gx, Gy, rows, cols);  //score from-to

					if (s > fwd_s){ // A better forward chaining found
						fwd_s = s;
						fwd = to;
					} else if (s < bck_s){ // A better backward chaining found
						bck_s = s;
						bck = to;
					}
				}
			}

            if (fwd >= 0){
                next[from] = fwd;					// Set next of from-fwd link
            }
            if (bck >= 0){
                prev[from] = bck;				// Set prev of bck-from link
            }

//			if (fwd >= 0 && next[from] != fwd && ((alt = prev[fwd]) < 0 || chain(alt, fwd, Ex, Ey, Gx, Gy, rows, cols) < fwd_s)){
//				if (next[from] >= 0){			// Remove previous from-x link if one */
//					prev[next[from]] = -1;	// Only prev requires explicit reset  */
//				}
//				next[from] = fwd;					// Set next of from-fwd link          */
//				if (alt >= 0){						// Remove alt-fwd link if one
//					next[alt] = -1;					// Only next requires explicit reset
//				}
//				prev[fwd] = from;					// Set prev of from-fwd link
//			}
//			if (bck >= 0 && prev[from] != bck && ((alt = next[bck]) < 0 || chain(alt, bck, Ex, Ey, Gx, Gy, rows, cols) > bck_s)){
//					if (alt >= 0){					// Remove bck-alt link if one
//						prev[alt] = -1;				// Only prev requires explicit reset
//					}
//					next[bck] = from;				// Set next of bck-from link
//					if (prev[from] >= 0){		// Remove previous x-from link if one
//						next[prev[from]] = -1; // Only next requires explicit reset
//					}
//					prev[from] = bck;				// Set prev of bck-from link
//			}
		}
	}
}

2.增加一个cu_cut_edge_points函数来完成单点最后向前向后轮廓的选择,切掉不是最优的轮廓

__global__
void cu_cut_edge_points(int * next, int * prev, double * Ex,	double * Ey,double * Gx, double * Gy, int rows, int cols){
    int x, y, i , j, alt;
    int dx, dy, to;

    x = blockIdx.x*blockDim.x+threadIdx.x+2;
    y = blockIdx.y*blockDim.y+threadIdx.y+2;

    if (x < (rows-2) && y < (cols-2)){
        if (Ex[x + y*rows] >= 0.0 && Ey[x + y*rows] >= 0.0){
            int center = x + y*rows;
            int temp_next = -1;
            int temp_prev = -1;

            if (x < (rows-2) && y < (cols-2)){
                for (i = -2; i <= 2; i++){
                    for (j = -2; j <= 2; j++){
                        to = x + i + (y + j)*rows;
                        if(next[to] == center){
                            if(temp_next >= 0){
                                if(chain(center, to, Ex, Ey, Gx, Gy, rows, cols) > chain(center, temp_next, Ex, Ey, Gx, Gy, rows, cols)){
                                    next[temp_next] = -1;
                                    temp_next = to;
                                } else {
                                    next[to] = -1;
                                }
                            } else {
                                temp_next = to;
                            }
                        }

                        if(prev[to] == center){
                            if(temp_prev >= 0){
                                if(chain(center, to, Ex, Ey, Gx, Gy, rows, cols) < chain(center, temp_prev, Ex, Ey, Gx, Gy, rows, cols)){
                                    prev[temp_prev] = -1;
                                    temp_prev = to;
                                } else {
                                    prev[to] = -1;
                                }
                            } else {
                                temp_prev = to;
                            }
                        }
                    }
                }
            }
        }
    }
}

3.修改cu_thresholds_remove函数,删掉一些错误的轮廓

__global__
void cu_thresholds_remove(int * next, int * prev,int rows, int cols, int * valid){
	int x, y;

	x = blockIdx.x*blockDim.x+threadIdx.x;
	y = blockIdx.y*blockDim.y+threadIdx.y;
	int idx = x+y*rows;

	if (idx < rows*cols){
		if ((prev[idx] >= 0 || next[idx] >= 0) && !valid[idx]){
			prev[idx] = next[idx] = -1;
		}

        if(prev[idx] != -1 && next[prev[idx]] != idx){
            prev[idx] = -1;
        }
        if(next[idx] != -1 && prev[next[idx]] != idx){
            next[idx] = -1;
        }
	}
}

4.修改list_chained_edge_points函数,重新启用找寻轮廓起始点的代码

// Set k to the beginning of the chain, or to i if closed curve
for (k = i; (n = prev[k]) >= 0 && n != i; k = n);
//if ((n = prev[i]) >=0 && n != i){ k= n;}

5.修改devernay函数,增加刚才添加的函数

cu_chain_edge_points<<<numberOfBlocks, threadsPerBlock, threadsPerBlock.x*threadsPerBlock.y*sizeof(uchar), stream>>>(next, prev, Ex, Ey, Gx, Gy, rows, cols);
cudaDeviceSynchronize();
cu_cut_edge_points<<<numberOfBlocks, threadsPerBlock, threadsPerBlock.x*threadsPerBlock.y*sizeof(uchar), stream>>>(next, prev, Ex, Ey, Gx, Gy, rows, cols);
cudaDeviceSynchronize();
cu_thresholds_with_hysteresis<<<numberOfBlocks, threadsPerBlock, threadsPerBlock.x*threadsPerBlock.y*sizeof(uchar), stream>>>(next, prev, modG, rows, cols, th_high, th_low, valid);
cudaDeviceSynchronize();

完整的代码请下载 subPixelGPU.zip

使用C++库GSL解非线性方程

解方程是一个比较常见的功能,matlab,python都有简单方法实现。C++也有不少实现方法,下面就介绍一下GSL解方程的方法,代码参考了GSL源码中的demo。
GSL解方程的功能相对matlab要简单一些,目前看只支持实数解,并且只有一个解。当然对于大部分实际应用 已经足够了

假设需要解的方程组是如下2元非线性方程(方程右边不为0,移项一下就好了)

继续阅读

Opencv Inpaint + CUDA

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

算法来源

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

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

继续阅读

C# 混淆工具 obfuscar

c# 编译生成的exe 与 dll 如果不进行混淆,可以方便的反编译出源代码。虽然软件开源是趋势,但这种非自愿,非正式的公开还是给人不爽的感觉。c#混淆工具很多,收费的也很多,下面介绍一个开源混淆工具obfuscar,简单混淆一下的使用场景足够了。使用方法参考了https://www.qiufengblog.com/articles/csharp-code-encryption.html.obfuscar源码放在github上https://github.com/obfuscar/obfuscar

一. 安装obfuscar

obfuscar可以通过Nuget工具方便的安装。在Nuget安装界面搜索obfuscar后点击安装即可

继续阅读

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图片并转换为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的代码

继续阅读

树莓派安装gitlab 及 https设置

最新的树莓派4B最大支持8G内存,相对于树莓派派3的2G内存,已经足够gitlab的运行。安装搭建gitlab的环境可以使用源码编译,或参考官网上的安装步骤。但最方便的还是使用docker进行安装。官方的docker镜像没有ARM64的镜像,但提供了一个非官方的链接,使用其提供的镜像可以方便的安装gitlab。

安装方法

1.安装docker镜镜像
相应的安装方法网上很多,不进行累述。

2.pull gitlab镜像

继续阅读

gRPC 参数设置 – 大小限制 重连时间

gRPC是google开发的RPC框架。使用gRPC可以方便的在程序间传递protobuf序列化后的数据,而不必考虑数据量大小,断开重连等比较难处理的问题。C++下的使用方法可以参考https://grpc.io/docs/languages/cpp/。下面记录下一些gRPC设置的小技巧

  • 数据量大小限制的解除

gRPC默认传输的大小限制为4M,如果要传输更大的,需要进行设置。

继续阅读

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后的图片,后续的使用也不需要考虑显存的分配和释放。