C#中的OpenCV MatchTemplate与Python相比速度太慢

7
我用Python编写了一个很好的解决方案,但需要安装几个库和大量繁琐的设置才能运行。我决定在Visual Studio Community 2017中使用C#构建GUI,但在第一个成功的功能中,结果要比Python慢得多。而在我看来,它应该更快。
代码本质上只是在做一个鸡毛蒜皮的图像搜索,通过获取文件夹中的所有图像并测试其中每个针(共计60个图像)在大量数据中是否存在。在Python中,我返回字符串,但在C#中我只是打印。
我的Python代码如下:
def getImages(tela):
    retorno = []
    folder = 'Images'
    img_rgb = cv2.imread(tela)
    for filename in os.listdir(folder):
        template = cv2.imread(os.path.join(folder,filename))
        w, h = template.shape[:-1]
        res = cv2.matchTemplate(img_rgb, template, cv2.TM_CCOEFF_NORMED)
        threshold = .96
        loc = np.where(res >= threshold)
        if loc[0]>0:
            retorno.append(filename[0]+filename[1].lower())
            if len(retorno)> 1:
                return retorno

同时,在C#中:

Debug.WriteLine(ofd.FileName);
Image<Bgr, byte> source = new Image<Bgr, byte>(ofd.FileName);
string filepath = Directory.GetCurrentDirectory().ToString()+"\\Images";
DirectoryInfo d = new DirectoryInfo(filepath);
var files = d.GetFiles();
foreach (var fname in files){
    Image<Bgr, byte> template = new Image<Bgr, byte>(fname.FullName);
    Image<Gray, float> result = source.MatchTemplate(template, Emgu.CV.CvEnum.TemplateMatchingType.CcoeffNormed);
    double[] minValues, maxValues;
    Point[] minLocations, maxLocations;
    result.MinMax(out minValues, out maxValues, out minLocations, out maxLocations);
    if (maxValues[0] > 0.96) {
        Debug.WriteLine(fname);
    }
}

我没有测量每个操作之间经过的时间,但我可以说在C#中执行需要约3秒,在Python中则只需大约100毫秒。

如果有人想提出任何改进意见,我们非常欢迎。


你想要找到仅匹配一个图像还是所有匹配的图像?Python代码只会找到一个图像。C#代码将找到所有匹配项。 - Panagiotis Kanavos
顺便提一下,Emgu是Python使用的同一OpenCV库的包装器。如果两个程序执行相同的操作,您不应该看到任何显著的差异。如果在每种情况下都使用并行处理,您可能可以提高性能。在C#中,您可以使用例如PLINQ或Parallel.ForEach。 - Panagiotis Kanavos
3个回答

9
问题在于,在 Python 代码中,当至少添加一个匹配项到 retorno 后,你就结束了迭代:

问题在于,在 Python 代码中,只要向 retorno 添加至少一个匹配项,就会结束迭代:

if len(retorno)> 1:
  return retorno

在C#示例中,您可以继续迭代,直到所有文件都被循环遍历。


2
我已经将denfromufaHouseCat在下面的源代码中提出的解决方案结合起来,并进行了一些整体清理,因此您可以看到您的代码应该是什么样子。您还会注意到一些小的可读性改进,因为我使用了C# 7.0 / .NET 4.7重构了代码。
真正的算法优化
尽管denfromula正确指出了实现问题,而HouseCat则提到了使用更多的CPU资源,但真正的收益在于减少图像搜索算法执行的操作次数。
  • TURBO STAGE 1 - 假设MinMax()函数遍历所有图像像素以收集所有这些统计数据,但您只对使用maxValue [0]感兴趣。一个极端的微调是编写一个特定的函数,在maxValue [0]低于您的最小阈值时停止遍历所有图像像素。显然,这就是您在函数中所需要的全部内容。记住:不要浪费所有处理器计算大量未使用的图像统计信息

  • TURBO STAGE 2 - 看起来您正在尝试识别是否有任何图像与您的输入屏幕截图(tela)匹配。如果要匹配的图像不太多,并且您不断检查屏幕以获取新的匹配项,则强烈建议预加载所有这些图像匹配对象,并在函数调用之间重复使用它们。频繁的磁盘IO操作和实例化位图类(针对每个单独的屏幕截图)会导致性能下降。

  • TURBO STAGE 3 - 如果您每秒拍摄多张截图,请尝试重复使用截图缓冲区。当其尺寸未改变时,不断重新分配整个截图缓冲区也会导致性能损失

  • TURBO STAGE 4 - 这很难理解,并取决于您想投资多少。将您的图像识别系统视为一个大型管道。位图作为数据容器在各个阶段之间流动(图像匹配阶段、OCR阶段、鼠标位置绘制阶段、视频录制阶段等)。这个想法是创建一定数量的容器并重复使用它们,避免它们的创建和销毁。容器的数量就像管道系统的“缓冲区大小”。当管道的几个阶段完成使用这些容器时,它们被返回到管道的开头,成为容器池的一部分。

最后这个优化真的很难使用这些外部库来实现,因为在大多数情况下它们的API需要一些内部位图实例化,并且微调也会导致你的库与外部库之间出现极度的软件耦合。因此,你将不得不深入了解这些好的库是如何工作的,并构建自己的定制框架。我可以说这是一个不错的学习经历。
这些库确实非常酷,适用于许多目的;它们提供通用API以改善功能可重用性。这也意味着它们涵盖比你在单个API调用中所需的更多内容。当涉及到高性能算法时,你应该始终重新思考从这些库中获取哪些关键功能以实现你的目标,如果它们成为你的瓶颈,就要自己动手。
我可以说,一种良好的微调图像识别算法只需要几毫秒就可以完成你想要的操作。我曾经体验过在更大的屏幕截图(例如Eggplant Functional)上几乎瞬间完成这项任务的图像识别应用程序。
现在回到你的代码...
你重构后的代码应该如下所示。我没有包括我提到的所有微调算法 - 你最好在SO上单独提出问题。
        Image<Bgr, byte> source = new Image<Bgr, byte>(ofd.FileName);

        // Preferably use Path.Combine here:
        string dir = Path.Combine(Directory.GetCurrentDirectory(), "Images");

        // Check whether directory exists:
        if (!Directory.Exists(dir))
            throw new Exception($"Directory was not found: '{dir}'");

        // It looks like you just need filenames here...
        // Simple parallel foreach suggested by HouseCat (in 2.):
        Parallel.ForEach(Directory.GetFiles(dir), (fname) =>
        {
            Image<Gray, float> result = source.MatchTemplate(
                new Image<Bgr, byte>(fname.FullName),
                Emgu.CV.CvEnum.TemplateMatchingType.CcoeffNormed);

            // By using C# 7.0, we can do inline out declarations here:
            result.MinMax(
                out double[] minValues,
                out double[] maxValues,
                out Point[] minLocations,
                out Point[] maxLocations);

            if (maxValues[0] > 0.96)
            {
                // ...
                var result = ...
                return result; // <<< As suggested by: denfromufa
            }

            // ...
        });

愉快的调音;-)

1
哇,太棒了!我知道这三个建议的理论,但无法将其应用到代码中,你的建议和解释非常清晰。我会在几分钟内尝试并回来接受答案,如果一切正常,我会奖励你。 - Guilherme Garcia da Rosa
1
不客气。我在痛苦的UI测试自动化项目中已经做了几年这样的事情。需要一段时间将所有必要的SO问题粘合在一起才能达到这个目的;-) - sɐunıɔןɐqɐp

2

这个 (denfromufa的答案) 确实解释了你的问题,但是我想补充一些建议/优化:

1.) 你的 GetFiles 可以被一个并行文件枚举器替换,它也具有递归和子目录功能。我已经在 GitHub 上不要脸地写了几个。

2.) 你可以将 foreach 循环并行化为 Parallel.ForEach(files, fname () => { Code(); }); 再次提供我的 FileSearchBenchmark 存储库上有大量文件代码并行执行的 示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接