OpenCV是计算机视觉和图像处理算法以及通用数值算法的开源库。 该库在C ++开发人员中是众所周知的。 除C ++外,还有适用于Python,Java,Ruby,Matlab,Lua和其他语言的版本。 由于C#(我专门研究的语言)不在该列表中,因此我选择了OpenCv的C#包装器OpenCvSharp来通过PVS-Studio进行检查。 本文讨论了该检查的结果。
引言
在成为PVS-Studio团队的一员之前,我曾参与制作机器人来参加展览。 我的职责包括最基本的维修工作(重大故障由其他人处理)以及各种软件和实用程序的开发。
我,一个疲惫不堪的新手,带了一个刚打开的KIKI机器人。顺便说一下,开发部分很有趣。 每次我们一个人想出一种使展览参观者感到惊讶的新方法时,我们都会提出来进行讨论,如果每个人都喜欢,我们就会开始工作。 一旦我们想到了制造一个可以识别人脸并以欢迎词回应的机器人。
我用谷歌搜索了一些我需要的库,然后偶然发现了计算机视觉算法库OpenCV。 但是当我发现OpenCV是用C ++实现时,我很快就感到失望。 我在大学学习的C ++知识显然还不够。 因此,我在Google上进行了更多搜索,找到了OpenCvSharp,这是我专门研究的C#库的包装。 从那时起大约半年了,该程序已经编写并投入使用,现在我终于决定窥视OpenCvSharp的“内幕”,并使用PVS-Studio静态分析器扫描其源代码。
正在分析的项目
OpenCvSharp是在C#项目中使用的OpenCV的包装。 顺便说一句,
我们过去
已经检查过 OpenCV。 OpenCvSharp的强项是大量的代码示例,跨平台支持(可在Mono支持的任何平台上运行)以及易于安装。
包装器是一个小项目,大约112,200行C#代码。 其中1.2%是评论,我应该说很少。 另一方面,对于一个很小的项目,存在很多错误。 我为本文选择了20多个示例,但分析器实际上发现了更多示例,这些示例并不那么有趣或显而易见。
PVS工作室
PVS-Studio是用于检测用C,C ++,C#和Java编写的程序的源代码中的错误和潜在漏洞的工具。 它可以在Windows,Linux和macOS上运行。 如前所述,除了无法访问的代码,编程错误和错别字之外,PVS-Studio还能检测潜在的安全问题。 因此,可以将其视为静态应用程序安全测试(SAST)工具。
最有趣的警告
是什么让
WriteableBitmapConverter方法与众不同的是它一次触发了四个相同类型的警告:
- V3005将'optimumChannels [PixelFormats.Indexed1]'变量分配给它自己。 WriteableBitmapConverter.cs 22
- V3005将'optimumChannels [PixelFormats.Indexed8]'变量分配给它自己。 WriteableBitmapConverter.cs 23
- V3005将'optimumTypes [PixelFormats.Indexed1]'变量分配给它自己。 WriteableBitmapConverter.cs 50
- V3005将'optimumTypes [PixelFormats.Indexed8]'变量分配给它自己。 WriteableBitmapConverter.cs 51
static WriteableBitmapConverter() { optimumChannels = new Dictionary <PixelFormat, int>(); optimumChannels[PixelFormats.Indexed1] =
PixelFormats类在
System.Windows.Media命名空间中定义,并且是各种像素格式的集合。 分析器指出,在
WriteableBitmapConverter方法中第二次为元素
bestChannels [PixelFormats.Indexed1]和
bestChannels [PixelFormats.Indexed8]分配了值,这没有任何意义。 目前尚不清楚这仅仅是一个错字,还是程序员的意思。 顺便说一句,这个片段是一个静态分析器如何帮助您的生动示例:观察一堆相似的行会降低您的注意力-难怪即使在代码审查后,错别字也不会被注意到。 但是,静态分析器没有麻烦,也不需要休息,因此可以轻松捕获此类错误。
感受静态分析的力量。PVS-Studio诊断消息 :
V3021有两个带有相同条件表达式的'if'语句。 第一个'if'语句包含方法return。 这意味着第二个'if'语句是没有意义的InputArray.cs 394
private static MatType EstimateType(Type t) { .... if (t == typeof(Vec2b)) return MatType.CV_8UC2; if (t == typeof(Vec3b)) return MatType.CV_8UC3; if (t == typeof(Vec4b)) return MatType.CV_8UC4; if (t == typeof(Vec6b)) return MatType.CV_8UC(6); if (t == typeof(Vec2s))
该错误与上一个错误类似。 开发人员正在两次检查相同的条件。 这里没有意义,因为“重复”
if语句的then分支将永远不会执行,因为:
- 如果第一个条件为真,则该方法将返回;
- 如果第一个条件为假,那么第二个也将为假,因为被检查的变量t在两次检查之间不会改变。
该代码需要修改;
Vec2s的第二个副本实际上很有可能是其他一些变量。
PVS-Studio诊断消息 :
V3010需要使用功能“ ToString”的返回值。 ImgProcTest.cs 80
public static RectanglesIntersectTypes RotatedRectangleIntersection(RotatedRect rect1, RotatedRect rect2, out Point2f[] intersectingRegion) { using (var intersectingRegionVec = new VectorOfPoint2f()) { int ret = NativeMethods .imgproc_rotatedRectangleIntersection_vector( rect1, rect2, intersectingRegionVec.CvPtr); intersectingRegion = intersectingRegionVec.ToArray(); return (RectanglesIntersectTypes) ret; } } public void RotatedRectangleIntersectionVector() { var rr1 = new RotatedRect(new Point2f(100, 100), new Size2f(100, 100), 45); var rr2 = new RotatedRect(new Point2f(130, 100), new Size2f(100, 100), 0); Cv2.RotatedRectangleIntersection(rr1, rr2, out var intersectingRegion); .... intersectingRegion.ToString(); }
可通过
intersectingRegion参数访问
RotatedRectangleIntersection方法,并返回
Point2f类型的元素数组。 一旦
intersectingRegion填充了值,就在数组上调用
ToString()方法。 这不会以任何方式影响数组的元素,并且在最后一行中不会执行任何有用的工作,因此可以合理地假设开发人员只是忘记删除该部分。
PVS-Studio诊断消息:- V3021有两个带有相同条件表达式的'if'语句。 第一个'if'语句包含方法return。 这意味着第二个'if'语句是毫无意义的Cv2_calib3d.cs 1370
- V3022表达式'objectPoints == null'始终为false。 Cv2_calib3d.cs 1372
public static double CalibrateCamera(....) { if (objectPoints == null) throw new ArgumentNullException(nameof(objectPoints)); if (objectPoints == null) throw new ArgumentNullException(nameof(objectPoints)); .... }
我们在这里克隆了代码,因此产生了两个警告。 第一个说两个
if语句检查相同的条件。 如果该条件为真,则该方法将在第一个
if语句的thenbranch中返回。 因此,第二个条件将始终为假,这就是第二个警告告诉我们的。 似乎程序员使用复制粘贴克隆了该片段,但忘记更改它。
可爱的复制粘贴。此类型的其他警告:
- V3021有两个带有相同条件表达式的'if'语句。 第一个'if'语句包含方法return。 这意味着第二个'if'语句是毫无意义的Cv2_calib3d.cs 1444
- V3022表达式'objectPoints == null'始终为false。 Cv2_calib3d.cs 1446
PVS-Studio诊断消息: V3022表达式'label == MarkerValue'始终为false。 Labeller.cs 135
internal static class Labeller { .... private const int MarkerValue = -1; public static int Perform(Mat img, CvBlobs blobs) { .... int label = 0; int lastLabel = 0; CvBlob lastBlob = null; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { if (imgIn[x + y * step] == 0) continue; bool labeled = labels[y, x] != 0; if (....) { labeled = true;
将创建一个名为
label的变量并将其初始化为0。如果某个条件为true,则它将递增1。 更重要的是,此变量永远不会在此代码段中递减。 因此,如分析仪所指出的那样,将其检查为常数-1是没有任何意义的。
PVS-Studio诊断消息: V3038参数多次传递给方法。 可能应改为传递其他参数。 Cv2_photo.cs 124
public static void FastNlMeansDenoisingMulti(....) { .... NativeMethods.photo_fastNlMeansDenoisingMulti( srcImgPtrs, srcImgPtrs.Length, dst.CvPtr, imgToDenoiseIndex, templateWindowSize, h, templateWindowSize, searchWindowSize); .... }
要了解分析器告诉我们的内容,让我们看一下
photo_fastNlMeansDenoisingMulti方法的参数:
public static extern void photo_fastNlMeansDenoisingMulti( IntPtr[] srcImgs, int srcImgsLength, IntPtr dst, int imgToDenoiseIndex, int temporalWindowSize, float h, int templateWindowSize, int searchWindowSize)
让我们进一步简化它,使其完全简单明了。 比较这些行:
NativeMethods.photo_fastNlMeansDenoisingMulti( .... templateWindowSize, .... templateWindowSize, ....); public static extern void photo_fastNlMeansDenoisingMulti( .... int temporalWindowSize, .... int templateWindowSize, ....)
templateWindowSize变量被声明两次,但是第一次提到它实际上应该是
temporalWindowSize的声明。 分析器不喜欢的另一件事是,
photo_fastNlMeansDenoisingMulti方法中根本没有使用
temporalWindowSize的值。 这可能是一个有意识的决定,但是如果我是作者,我将仔细研究这段代码。
此类型的其他警告:
- V3038参数多次传递给方法。 可能应改为传递其他参数。 Cv2_photo.cs 149
- V3038参数多次传递给方法。 可能应改为传递其他参数。 Cv2_photo.cs 180
- V3038参数多次传递给方法。 可能应改为传递其他参数。 Cv2_photo.cs 205
下一个示例与上一个示例有些相似。
PVS-Studio诊断消息: V3066传递给“ calib3d_Rodrigues_MatToVec”方法的参数的可能错误顺序:“ matrixM.CvPtr”和“ vectorM.CvPtr”。 Cv2_calib3d.cs 86
public static void Rodrigues(double[,] matrix, out double[] vector, out double[,] jacobian) { .... using (var jacobianM = new Mat<double>()) { NativeMethods.calib3d_Rodrigues_MatToVec (matrixM.CvPtr, vectorM.CvPtr, jacobianM.CvPtr); .... } }
让我们看一下
calib3d_Rodrigues_MatToVec方法的参数:
public static extern void calib3d_Rodrigues_MatToVec( IntPtr vector, IntPtr matrix, IntPtr jacobian)
似乎在
调用calib3d_Rodrigues_MatToVec方法时意外地交换了参数
matrixM.CvPtr和
vectorM.CvPtr 。 作者应检查以下代码段:可能有一个错误会妨碍正确的计算。
PVS-Studio诊断消息: V3063如果条件表达式的一部分被评估,则始终为false:data == null。 Mat.cs 3539
private void CheckArgumentsForConvert(....) { .... if (data == null) throw new ArgumentNullException(nameof(data)); MatType t = Type(); if (data == null || (data.Length * dataDimension)
分析器报告第二个检查
数据== null永远不会为
真,因为如果在第一个条件下
数据等于
null ,则将引发异常并且执行将永远不会达到第二个检查。
我知道您很累,但是我们快完成了。PVS-Studio诊断消息: V3127找到两个相似的代码片段。 也许,这是一个错字,应该使用“窗口”变量而不是“ src2” Cv2_imgproc.cs 1547
public static Point2d PhaseCorrelateRes(....) { if (src1 == null) throw new ArgumentNullException(nameof(src1)); if (src2 == null) throw new ArgumentNullException(nameof(src2)); if (window == null) throw new ArgumentNullException(nameof(src2));
分析人员在此代码段中发现了一个错字。 检查变量是否为
null ,如果为true,则每次检查都会引发异常。 但是,对于
window变量,它不能正常工作。 如果其值等于
null ,则也会引发相应的异常,但文本错误。 不会提
窗户 ; 它将是
src2 。 该条件显然应修改如下:
if (window == null) throw new ArgumentNullException(nameof(window));
PVS-Studio诊断消息: V3142检测到无法访问的代码。 可能存在错误。 MatOfT.cs 873
现在,仅作更改,让我们看一下分析器在技术上对无法访问的代码正确的情况,但实际上没有错误。 这是一个警告,可以同时称为true和false。
public new Mat<TElem> SubMat(params Range[] ranges) { Mat result = base.SubMat(ranges); return Wrap(result); }
分析器告诉我们
return语句不可访问。 让我们看一下
SubMat方法的主体,看看分析器是否在说真话。
public Mat SubMat(params Range[] ranges) { throw new NotImplementedException(); }
如您所见,该函数当前不完整,将始终引发异常。 分析器指出无法到达的代码是绝对正确的-但这不是真正的错误。
接下来的三个缺陷是同一类型,但是它们是如此酷,我不禁将所有三个缺陷都包括在内。
PVS-Studio诊断消息: V3022表达式'String.IsNullOrEmpty(“ winName”)'始终为false。 Cv2_highgui.cs 46
public static void DestroyWindow(string winName) { if (String.IsNullOrEmpty("winName")) .... }
PVS-Studio诊断消息: V3022表达式'string.IsNullOrEmpty(“ fileName”)'始终为false。 FrameSource.cs 37
public static FrameSource CreateFrameSource_Video(string fileName) { if (string.IsNullOrEmpty("fileName")) .... }
PVS-Studio诊断消息: V3022表达式'string.IsNullOrEmpty(“ fileName”)'始终为false。 FrameSource.cs 53
public static FrameSource CreateFrameSource_Video_CUDA(string fileName) { if (string.IsNullOrEmpty("fileName")) .... }
有时,
V3022警告(大约总是正确/错误的表达式)指向真正奇怪或有趣的错误。 上面的所有三个示例都具有相同的错误。 该方法具有
字符串类型的参数,必须检查其值。 但是,要检查的是字符串文字,其文本是变量的名称,即变量的名称用引号引起来。
程序员必须编写一次错误的代码块,然后通过复制粘贴将其克隆。
结论
OpenCvSharp的开发人员完成了重要的工作,作为其库的用户,我对此深表感谢。 谢谢你们!
但是现在我已经成为PVS-Studio团队的一员,并且看到了图书馆的代码,我不得不说,质量方面没有得到应有的重视。 该项目看起来不像是由静态分析仪定期检查的项目,并且许多错误显然是使用更昂贵的技术(例如测试或用户反馈)修复的,并且其中一些错误一直存在于代码中我们用分析仪捕捉到的 在
本篇有关静态分析原理的
小文章中将更详细地讨论该主题。
由于OpenCvSharp是开源的,并且可以在GitHub上
免费使用,因此其作者可以使用PVS-Studio的
免费许可选项之一定期开始使用它。
感谢您的阅读。 不要犹豫,
下载 PVS-Studio的试用版以检查您自己的项目。