与Python相比,C#中的OpenCV MatchTemplate太慢了


问题内容

我已经用Python编写了一个效果很好的解决方案,但是需要安装多个库并需要大量管理工作。我已经决定在Visual Studio Community
2017上使用C#中的GUI来构建它,但是在第一个成功的函数中,其结果要比Python慢​​得多。实际上哪个IMO应该更快。

从本质上讲,代码只是在大海捞针图像搜索中做针,通过从文件夹中获取所有图像并测试大海捞针中的每根针(总共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中的结果大约需要100ms。

有优化的空间,如果有人想提出任何改进,欢迎他们。


问题答案:

我在下面的源代码中结合了denfromufaHouseCat提出的解决方案,并进行了一些总体清理,因此您可以看到代码的效果。由于我使用
C#7.0 / .NET 4.7 编写了重构代码,因此您还将注意到可读性方面的一些改进。

实算法优化

尽管 denfromula 正确地指出了实现问题,并且 HouseCat
提到使用更多的CPU资源,但真正的收益取决于减少图像搜索算法期间执行的操作数量。

  • 涡轮增压阶段1- 假设该MinMax()功能遍历图像的所有像素以收集所有这些统计信息,但是您只对使用感兴趣maxValue[0]。极好的微调是 编写一个特定的函数,maxValue[0]低于最小阈值时, 该函数将停止迭代图像的所有像素 。显然,这就是您功能所需要的。切记: 切勿刻录所有处理大量未使用的图像统计信息的处理器

  • TURBO STAGE 2- 似乎您正在尝试识别图像集中的任何图像是否与您输入的屏幕截图(tela)相匹配。如果没有太多要匹配的图像,并且您一直在检查屏幕上是否有新的匹配项,则强烈建议预先加载所有这些图像匹配对象,并在函数调用之间重用它们。 恒定的磁盘IO操作和实例化位图类(对于每个单个屏幕截图)都会导致性能下降。

  • TURBO STAGE 3- 以防万一您每秒要获取多个屏幕截图,然后尝试重用屏幕截图的缓冲区。 当尺寸完全不变时, 不断 重新分配整个屏幕快照的缓冲区也会导致性能下降

  • 涡轮增压阶段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
            }

            // ...
        });

快乐调音;-)