gpt4 book ai didi

c++ - OpenCV C++。快速计算混淆矩阵

转载 作者:太空宇宙 更新时间:2023-11-03 22:19:11 24 4
gpt4 key购买 nike

使用 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;
    }
  • RAM重矩阵逻辑:
    {
    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也可以完成这项工作,并且可能会节省一点时间。

    大量冗余矩阵分配带来了巨大的开销。而不是为每个 truePos 分配一个新矩阵, falsePos , falseNegtrueNeg ,我们可以重用相同的 cv::Mat对于所有 4 种情况。由于形状和数据类型将始终相同,这意味着只会发生 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哪里rowscols表示cv::Mat的高度和宽度(我们可以使用 cv::Mat::total() )。
  • TP + FP + FN + TN == element_count因为每个元素恰好属于 4 个集合中的 1 个。
  • positive_countdetections 中非零元素的数量.
  • negative_countdetections 中零元素的数量.
  • 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——“forEach with lambda”

    这再次遇到与变体 1 相同的问题,即它不太可能被矢量化,因此它会相对较慢。

    您实现的主要问题是 forEach在输入的多个切片上并行运行该函数,并且缺乏任何同步。当前实现返回不正确的结果。

    然而,并行化的想法可以通过一些努力应用到最好的变体 2。

    变体 4——“平行”

    让我们改进 conf_mat_2e利用 cv::parallel_for_ .在工作线程之间分配负载的最简单方法是逐行进行。

    我们可以通过共享一个中间 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(即 12 个线程)的 6 核 CPU 上运行。

    conf_mat_2a 相比,运行时间减少了约 97.5% .

    对于非常小的输入,这可能是次优的。理想的实现可能是其中一些方法的组合,根据输入大小进行委托(delegate)。

    测试代码:
    #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

    关于c++ - OpenCV C++。快速计算混淆矩阵,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53102166/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com