与 Python 相比,C# 中的 OpenCV MatchTemplate 太慢了
OpenCV MatchTemplate in C# is too slow compared to Python
我在 Python 中编写了一个解决方案,效果很好,但需要安装多个库和大量官僚设置才能工作。我决定在 Visual Studio Community 2017 上使用 C# 中的 GUI 构建它,但在第一个成功的函数中,结果比 Python 慢得多。 IMO 它实际上应该更快。
代码本质上只是大海捞针图像搜索,通过从文件夹中获取所有图像并在大海捞针中测试每个针(总共 60 张图像),在 python I return 字符串,但在 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 代码中,当至少一个匹配被添加到 retorno
:
时,您完成了迭代
if len(retorno)> 1:
return retorno
在 C# 示例中,您将继续迭代,直到循环遍历所有文件。
这 () 确实解释了您的问题,但顺便补充一下 suggestions/optimizations:
1.) 您的 GetFiles
可以替换为并行文件枚举器,它也可以递归子目录。我在GitHub.
上厚颜无耻地写了一些
2.) 您可以将 foreach 循环并行化为 Parallel.ForEach(files, fname () => { Code(); });
同样,我在 GitHub 上的 FileSearchBenchmark 存储库有大量文件代码并行执行以提供 examples.
我在下面的源代码中结合了 and 提出的解决方案,并做了一些整体清理,所以你可以看到你的代码会怎样。您还会注意到可读性的微小改进,因为我使用 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 阶段、鼠标位置绘画阶段、视频录制阶段等)之间流动的容器。这个想法是创建固定数量的容器并重复使用它们,避免它们的创建和销毁。容器的数量类似于管道系统的 "buffer size"。 当管道的几个阶段使用完这些容器后,它们将返回到管道的起点,返回到一种容器池。
使用这些外部库确实很难实现最后的优化,因为在大多数情况下,它们的 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
}
// ...
});
快乐调音;-)
我在 Python 中编写了一个解决方案,效果很好,但需要安装多个库和大量官僚设置才能工作。我决定在 Visual Studio Community 2017 上使用 C# 中的 GUI 构建它,但在第一个成功的函数中,结果比 Python 慢得多。 IMO 它实际上应该更快。
代码本质上只是大海捞针图像搜索,通过从文件夹中获取所有图像并在大海捞针中测试每个针(总共 60 张图像),在 python I return 字符串,但在 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 代码中,当至少一个匹配被添加到 retorno
:
if len(retorno)> 1:
return retorno
在 C# 示例中,您将继续迭代,直到循环遍历所有文件。
这 (
1.) 您的 GetFiles
可以替换为并行文件枚举器,它也可以递归子目录。我在GitHub.
2.) 您可以将 foreach 循环并行化为 Parallel.ForEach(files, fname () => { Code(); });
同样,我在 GitHub 上的 FileSearchBenchmark 存储库有大量文件代码并行执行以提供 examples.
我在下面的源代码中结合了
真实算法优化
虽然 denfromula 正确地指出了实施问题,并且 HouseCat 提到使用更多 CPU 资源,但真正的收获依赖于关于减少图像搜索算法期间执行的操作数。
TURBO STAGE 1 - 假设
MinMax()
函数遍历图像的所有像素以收集所有这些统计信息,但您只对使用maxValue[0]
。一个极端的微调是 编写一个特定函数,当maxValue[0]
低于您的最小阈值时,该函数停止迭代所有图像的像素 。显然,这就是您的功能所需要的全部。请记住:永远不要消耗所有处理器来计算大量未使用的图像统计信息。TURBO STAGE 2 - 您似乎在尝试识别图像集中的任何图像是否与输入的屏幕截图相匹配(
tela
).如果没有太多要匹配的图像,并且如果您不断检查屏幕以寻找新的匹配项,强烈建议预加载所有这些图像匹配对象,并在您的函数调用中重复使用它们。 恒定的磁盘 IO 操作和实例化位图 类(对于每个屏幕截图)会导致严重的性能损失。TURBO STAGE 3 - 以防万一您每秒截取多个屏幕截图,然后尝试重新使用屏幕截图的缓冲区。不断地重新分配整个屏幕截图的缓冲区,而其尺寸根本没有改变也会导致性能损失。
TURBO STAGE 4 - 这很难得到,取决于你想在这上面投资多少。 将您的图像识别系统想象成一条大管道。位图作为数据在您的阶段(图像匹配阶段、OCR 阶段、鼠标位置绘画阶段、视频录制阶段等)之间流动的容器。这个想法是创建固定数量的容器并重复使用它们,避免它们的创建和销毁。容器的数量类似于管道系统的 "buffer size"。 当管道的几个阶段使用完这些容器后,它们将返回到管道的起点,返回到一种容器池。
使用这些外部库确实很难实现最后的优化,因为在大多数情况下,它们的 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
}
// ...
});
快乐调音;-)