OpenCV C++.快速计算混淆矩阵
OpenCV C++. Quickly compute confusion matrix
使用 OpenCV 和 C++ 计算混淆矩阵的首选方法是什么?
从以下开始:
int TP = 0,FP = 0,FN = 0,TN = 0;
cv::Mat truth(60,60, CV_8UC1);
cv::Mat detections(60,60, CV_8UC1);
this->loadResults(truth, detections); // loadResults(cv::Mat& t, cv::Mat& d);
我尝试了几个不同的选项,例如:
直拨电话:
for(int r = 0; r < detections.rows; ++r) for(int c = 0; c < detections.cols; ++c) { int d,t; d = detection.at<unsigned char>(r,c); t = truth.at<unsigned char>(r,c); if(d&&t) ++TP; if(d&&!t) ++FP; if(!d&&t) ++FN; if(!d&&!t) ++TN; }
内存重矩阵逻辑:
{ cv::Mat truePos = detection.mul(truth); TP = cv::countNonZero(truePos) } { cv::Mat falsePos = detection.mul(~truth); FP = cv::countNonZero(falsePos ) } { cv::Mat falseNeg = truth.mul(~detection); FN = cv::countNonZero(falseNeg ) } { cv::Mat trueNeg = (~truth).mul(~detection); TN = cv::countNonZero(trueNeg ) }
对于每个:
auto lambda = [&, truth,TP,FP,FN,TN](unsigned char d, const int pos[]){ cv::Point2i pt(pos[1], pos[0]); char t = truth.at<unsigned char>(pt); if(d&&t) ++TP; if(d&&!t) ++FP; if(!d&&t) ++FN; if(!d&&!t) ++TN; }; detection.forEach(lambda);
但是有没有标准的方法呢?我可能错过了OpenCV文档中的简单功能。
附言我用的是VS2010 x64;
简而言之,这三者都不是。
在开始之前,让我们定义一个简单的结构来保存我们的结果:
struct result_t
{
int TP;
int FP;
int FN;
int TN;
};
这将允许我们将每个实现包装在一个具有以下签名的函数中,以简化测试和性能评估。(请注意,我使用cv::Mat1b
来明确表示我们只需要CV_8UC1
类型的垫子:
result_t conf_mat_x(cv::Mat1b truth, cv::Mat1b detections);
我将使用随机生成的大小为 4096 x 4096 的数据来衡量性能。
我在这里使用 OpenCV 3.1 MSVS2013 64 位。抱歉,没有准备好使用 OpenCV 设置MSVS2010来测试它,以及使用 c++11 的计时代码,因此您可能需要修改它才能进行编译。
变式1——"直接呼叫">
代码的更新版本如下所示:
result_t conf_mat_1a(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
for (int r(0); r < detections.rows; ++r) {
for (int c(0); c < detections.cols; ++c) {
int d(detections.at<uchar>(r, c));
int t(truth.at<uchar>(r, c));
if (d&&t) { ++result.TP; }
if (d&&!t) { ++result.FP; }
if (!d&&t) { ++result.FN; }
if (!d&&!t) { ++result.TN; }
}
}
return result;
}
性能和结果:
#0: min=120.017 mean=123.258 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
这里的主要问题是这(尤其是VS2010)不太可能被自动矢量化,所以会相当慢。利用 SIMD 可能会实现高达一个数量级的加速。此外,对cv::Mat::at
的重复调用也会增加一些开销。
这里真的没有太多收获,我们应该能够做得更好。
变体 2 -- "RAM 重型">
法典:
result_t conf_mat_2a(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
{
cv::Mat truePos = detections.mul(truth);
result.TP = cv::countNonZero(truePos);
}
{
cv::Mat falsePos = detections.mul(~truth);
result.FP = cv::countNonZero(falsePos);
}
{
cv::Mat falseNeg = truth.mul(~detections);
result.FN = cv::countNonZero(falseNeg);
}
{
cv::Mat trueNeg = (~truth).mul(~detections);
result.TN = cv::countNonZero(trueNeg);
}
return result;
}
性能和结果:
#1: min=63.993 mean=68.674 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
这已经是快了两倍,即使有很多不必要的工作正在做。
乘法(饱和)似乎有点矫枉过正——bitwise_and
也可以完成这项工作,并且可能会节省一点时间。
许多冗余矩阵分配带来了巨大的开销。我们可以为所有 4 种情况重用相同的cv::Mat
,而不是为truePos
、falsePos
、falseNeg
和trueNeg
中的每一个分配一个新矩阵。由于形状和数据类型将始终相同,这意味着只会发生 1 次分配而不是 4 次分配。
法典:
result_t conf_mat_2b(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
cv::Mat temp;
cv::bitwise_and(detections, truth, temp);
result.TP = cv::countNonZero(temp);
cv::bitwise_and(detections, ~truth, temp);
result.FP = cv::countNonZero(temp);
cv::bitwise_and(~detections, truth, temp);
result.FN = cv::countNonZero(temp);
cv::bitwise_and(~detections, ~truth, temp);
result.TN = cv::countNonZero(temp);
return result;
}
性能和结果:
#2: min=50.995 mean=52.440 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
与conf_mat_2a
相比,所需时间减少了~20%。
接下来,请注意,您正在计算~truth
并~detections
两次。因此,我们也可以通过重用它们来消除这些操作以及 2 个额外的分配。
注意:内存使用量不会改变 - 我们之前需要 3 个临时阵列,现在仍然如此。
法典:
result_t conf_mat_2c(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
cv::Mat inv_truth(~truth);
cv::Mat inv_detections(~detections);
cv::Mat temp;
cv::bitwise_and(detections, truth, temp);
result.TP = cv::countNonZero(temp);
cv::bitwise_and(detections, inv_truth, temp);
result.FP = cv::countNonZero(temp);
cv::bitwise_and(inv_detections, truth, temp);
result.FN = cv::countNonZero(temp);
cv::bitwise_and(inv_detections, inv_truth, temp);
result.TN = cv::countNonZero(temp);
return result;
}
性能和结果:
#3: min=37.997 mean=38.569 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
与conf_mat_2a
相比,所需时间减少了~40%。
仍有改进的潜力。让我们做一些观察。
element_count == rows * cols
其中rows
和cols
表示cv::Mat
的高度和宽度(我们可以使用cv::Mat::total()
)。TP + FP + FN + TN == element_count
因为每个元素正好属于 4 个集合中的 1 个。positive_count
是detections
中非零元素的数量。negative_count
是detections
中零元素的数量。positive_count + negative_count == element_count
因为每个元素正好属于 2 个集合中的 1
个TP + FP == positive_count
TN + FN == negative_count
使用这些信息,我们可以使用简单的算术计算TN
,从而消除一个bitwise_and
和一个countNonZero
。我们同样可以计算FP
,消除另一个bitwise_and
,并使用第二个countNonZero
来计算positive_count
。
由于我们消除了inv_truth
的两种用途,我们也可以放弃它。
注意:内存使用量减少了 - 我们现在只有 2 个临时阵列。
法典:
result_t conf_mat_2d(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
cv::Mat1b inv_detections(~detections);
int positive_count(cv::countNonZero(detections));
int negative_count(static_cast<int>(truth.total()) - positive_count);
cv::Mat1b temp;
cv::bitwise_and(truth, detections, temp);
result.TP = cv::countNonZero(temp);
result.FP = positive_count - result.TP;
cv::bitwise_and(truth, inv_detections, temp);
result.FN = cv::countNonZero(temp);
result.TN = negative_count - result.FN;
return result;
}
性能和结果:
#4: min=22.494 mean=22.831 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
与conf_mat_2a
相比,所需时间减少了~65%。
最后,由于我们只需要inv_detections
一次,因此我们可以重用temp
来存储它,从而摆脱更多的分配,并进一步减少内存占用。
注意:内存使用量已减少 - 我们现在只有 1 个临时阵列。
法典:
result_t conf_mat_2e(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
int positive_count(cv::countNonZero(detections));
int negative_count(static_cast<int>(truth.total()) - positive_count);
cv::Mat1b temp;
cv::bitwise_and(truth, detections, temp);
result.TP = cv::countNonZero(temp);
result.FP = positive_count - result.TP;
cv::bitwise_not(detections, temp);
cv::bitwise_and(truth, temp, temp);
result.FN = cv::countNonZero(temp);
result.TN = negative_count - result.FN;
return result;
}
性能和结果:
#5: min=16.999 mean=17.391 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
与conf_mat_2a
相比,所需时间减少了~72%。
变体 3 -- "每个都有 lambda">
这再次遇到与变体 1 相同的问题,即它不太可能被矢量化,因此它会相对较慢。
实现的主要问题是forEach
在输入的多个切片上并行运行函数,并且缺乏任何同步。当前实现返回不正确的结果。
但是,并行化的想法可以通过一些努力应用于变体 2 的最佳效果。
变式4——"并行">
让我们通过利用cv::parallel_for_
来改进conf_mat_2e
。在工作线程之间分配负载的最简单方法是逐行分配。
我们可以通过共享一个中间cv::Mat3i
来避免同步的需要,该中间将保存每一行的TP
、FP
和FN
(回想一下,TN
可以从最后的其他 3 个计算出来)。由于每一行仅由单个工作线程处理,因此我们不需要同步。处理完所有行后,一个简单的cv::sum
将给我们一个总的TP
、FP
和FN
。然后计算TN
。
注意:我们可以再次降低内存需求 - 我们需要一个缓冲区,为每个工作线程跨越一行。此外,我们需要3 * rows
整数来存储中间结果。
法典:
class ParallelConfMat : public cv::ParallelLoopBody
{
public:
enum
{
TP = 0
, FP = 1
, FN = 2
};
ParallelConfMat(cv::Mat1b& truth, cv::Mat1b& detections, cv::Mat3i& result)
: truth_(truth)
, detections_(detections)
, result_(result)
{
}
ParallelConfMat& operator=(ParallelConfMat const&)
{
return *this;
};
virtual void operator()(cv::Range const& range) const
{
cv::Mat1b temp;
for (int r(range.start); r < range.end; r++) {
cv::Mat1b detections(detections_.row(r));
cv::Mat1b truth(truth_.row(r));
cv::Vec3i& result(result_.at<cv::Vec3i>(r));
int positive_count(cv::countNonZero(detections));
int negative_count(static_cast<int>(truth.total()) - positive_count);
cv::bitwise_and(truth, detections, temp);
result[TP] = cv::countNonZero(temp);
result[FP] = positive_count - result[TP];
cv::bitwise_not(detections, temp);
cv::bitwise_and(truth, temp, temp);
result[FN] = cv::countNonZero(temp);
}
}
private:
cv::Mat1b& truth_;
cv::Mat1b& detections_;
cv::Mat3i& result_; // TP, FP, FN per row
};
result_t conf_mat_4(cv::Mat1b truth, cv::Mat1b detections)
{
CV_Assert(truth.size == detections.size);
result_t result = { 0 };
cv::Mat3i partial_results(truth.rows, 1);
cv::parallel_for_(cv::Range(0, truth.rows)
, ParallelConfMat(truth, detections, partial_results));
cv::Scalar reduced_results = cv::sum(partial_results);
result.TP = static_cast<int>(reduced_results[ParallelConfMat::TP]);
result.FP = static_cast<int>(reduced_results[ParallelConfMat::FP]);
result.FN = static_cast<int>(reduced_results[ParallelConfMat::FN]);
result.TN = static_cast<int>(truth.total()) - result.TP - result.FP - result.FN;
return result;
}
性能和结果:
#6: min=1.496 mean=1.966 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
这在启用 HT 的 6 核 CPU 上运行(即 12 个线程)。
与conf_mat_2a
相比,运行时间减少了 ~97.5%。
对于非常小的输入,这可能是次优的。理想的实现可能是其中一些方法的组合,根据输入大小进行委派。
测试代码:
#include <opencv2/opencv.hpp>
#include <chrono>
#include <iomanip>
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::microseconds;
struct result_t
{
int TP;
int FP;
int FN;
int TN;
};
/******** PASTE all the conf_mat_xx functions here *********/
int main()
{
int ROWS(4 * 1024), COLS(4 * 1024), ITERS(32);
cv::Mat1b truth(ROWS, COLS);
cv::randu(truth, 0, 2);
truth *= 255;
cv::Mat1b detections(ROWS, COLS);
cv::randu(detections, 0, 2);
detections *= 255;
typedef result_t(*conf_mat_fn)(cv::Mat1b, cv::Mat1b);
struct test_info
{
conf_mat_fn fn;
std::vector<double> d;
result_t r;
};
std::vector<test_info> info;
info.push_back({ conf_mat_1a });
info.push_back({ conf_mat_2a });
info.push_back({ conf_mat_2b });
info.push_back({ conf_mat_2c });
info.push_back({ conf_mat_2d });
info.push_back({ conf_mat_2e });
info.push_back({ conf_mat_4 });
// Warm-up
for (int n(0); n < info.size(); ++n) {
info[n].fn(truth, detections);
}
for (int i(0); i < ITERS; ++i) {
for (int n(0); n < info.size(); ++n) {
high_resolution_clock::time_point t1 = high_resolution_clock::now();
info[n].r = info[n].fn(truth, detections);
high_resolution_clock::time_point t2 = high_resolution_clock::now();
info[n].d.push_back(static_cast<double>(duration_cast<microseconds>(t2 - t1).count()) / 1000.0);
}
}
for (int n(0); n < info.size(); ++n) {
std::cout << "#" << n << ":"
<< std::fixed << std::setprecision(3)
<< "tmin=" << *std::min_element(info[n].d.begin(), info[n].d.end())
<< "tmean=" << cv::mean(info[n].d)[0]
<< "tTP=" << info[n].r.TP
<< "tFP=" << info[n].r.FP
<< "tTN=" << info[n].r.TN
<< "tFN=" << info[n].r.FN
<< "tTotal=" << (info[n].r.TP + info[n].r.FP + info[n].r.TN + info[n].r.FN)
<< "n";
}
}
MSVS2015、Win64、OpenCV 3.4.3 的性能和结果:
#0: min=119.797 mean=121.769 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#1: min=64.130 mean=65.086 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#2: min=51.152 mean=51.758 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#3: min=37.781 mean=38.357 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#4: min=22.329 mean=22.637 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#5: min=17.029 mean=17.297 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
#6: min=1.827 mean=2.017 TP=4192029 FP=4195489 TN=4195118 FN=4194580 Total=16777216
- 为什么"do while"循环不断退出,即使条件计算结果为 false?
- 递归函数计算序列中的平方和(并输出过程)
- (C++)分析树以计算返回错误值的简单算术表达式
- 我的字符计数代码计算错误.为什么
- 在计算中使用二的幂有多有利可图
- 如何计算文件中的"columns"数?
- 计算排序向量的向量中唯一值的计数
- 如何使用 std::累积在 C++ 中计算总和立方体
- 使用Qt C++计算类似Git的SHA1哈希
- OpenCV C++.快速计算混淆矩阵
- cpp二进制搜索问题,计算给定数组中输入元素的出现次数
- C++如何计算用户输入的数字中的偶数位数
- 如何计算数据类型的范围,例如int
- 类似枚举的计算常量
- 计算每个节点的树高,帮助我解释这个代码解决方案
- 多个If语句与使用逻辑运算符计算条件的单个语句的比较
- 计算缩放多边形的比例,得到给定的多边形面积
- 在C++中如何在没有pow的情况下进行基础计算
- 计算平均值,不包括上次得分
- 如何计算多映射中重复对的数量