Skip to content

HoughCircles accuracy decreased in opencv 4.1.1 after upgrade from 3.1.0 #16187

@martinvranka

Description

@martinvranka
System information (version)
  • OpenCV => 4.1.1
  • Operating System / Platform => Windows 64 Bit
  • Compiler =>Visual Studio 2019
Detailed description

The HoughCircles() from version 3.1.0 was used to detect the circles in 8bit gray value images with a resolution of 320x240 pixel. After an upgrade to version 4.1.1 the HoughCircles() detection accuraccy - the euclidean distance between ground truth (HoughCircles 3.1.0) and a detected centre decreased of 10% of an image resolution. Similar accuraccy loss has been observed by the radius values.

dist_centers_graph
Circle position error in pixels tested on 438 images

diff_radii_graph
Radius values error in pixels tested on 438 images
histogram_centre_dist
Histogram with frequencies of error classes.

Steps to reproduce
  1. A sample with HoughCircles call has been compiled with an opencv 3.1.0 build from source code, the results and parameters have been saved as metadata.
  2. A sample with HoughCircles call has been compiled with an opencv 4.1.1 build from source code, the parameters and results from 3.1.0 have been read. The difference has been saved as metadata.
  3. Results have been visualized.
Sample source code
#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc.hpp>

#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <list>


using namespace cv;
using namespace std;

namespace fs = std::filesystem;

bool read_params(const fs::path& prm_file_path, int& rnX, int& rnY, int& rnW, int& rnH, double& rdMinDist, double& rdCanny, double& rdNumVotes, int& rnMinRad, int& rnMaxRad, int& rnNumCircles, double& rdCentX, double& rdCentY, double& rdRadius)
{
    if (!fs::exists(prm_file_path))
    {
        return false;
    }

    ifstream oStream;

    oStream.open(prm_file_path.string());

    if (!oStream.is_open())
    {
        return false;
    }

    int nTmp;

    oStream >> nTmp;                // cnScale
    oStream >> rnX;                 // X
    oStream >> rnY;                 // Y
    oStream >> rnW;                 // W
    oStream >> rnH;                 // H
    oStream >> rdMinDist;           // min dist
    oStream >> rdCanny;             // canny thresh
    oStream >> rdNumVotes;          // num votes
    oStream >> rnMinRad;            // min radius
    oStream >> rnMaxRad;            // max radius
    oStream >> rnNumCircles;        // number of circles found
    if (0 != rnNumCircles)
    {
        oStream >> rdCentX;             // first circle center X
        oStream >> rdCentY;             // first circle center Y
        oStream >> rdRadius;            // first circle radius
    }
    else
    {
        rdCentX = 0.0;
        rdCentY = 0.0;
        rdRadius = 0.0;
    }

    oStream.close();

    return true;
}

bool find_hough_circles(const cv::Mat& input_img, const int& nX, const int& nY, const int& nW, const int& nH, const double& dMinDist, const double& dCanny, const double& dNumVotes, const int& nMinRad, const int& nMaxRad, int& rnNumCircles, double& rdCentX, double& rdCentY, double& rdRadius)
{
    Rect roi = Rect(nX, nY, nW, nH);
    Mat roi_image = input_img(roi);

    vector<Vec3f> aCircles;

    try
    {
        HoughCircles(roi_image, aCircles, cv::HOUGH_GRADIENT, 2.0, dMinDist, dCanny, dNumVotes, nMinRad, nMaxRad);
    }
    catch (const cv::Exception & e)
    {
        std::cout << "Exception thrown: " << e.what() << std::endl;
        return false;
    }

    rnNumCircles = (int)aCircles.size();

    if (rnNumCircles)
    {
        rdCentX = aCircles[0][0];
        rdCentY = aCircles[0][1];
        rdRadius = aCircles[0][2];
    }
    else
    {
        rdCentX = 0.0;
        rdCentY = 0.0;
        rdRadius = 0.0;
    }

    return true;
}

fs::path find_dbg_filename(const list<fs::path>& dbg_names, const string& dbg_subname)
{
    for (const auto& entry : dbg_names)
    {
        const fs::path& entry_stem = entry.stem();

        if (entry_stem.string() == dbg_subname)
        {
            return entry;
        }
    }

    return fs::path();
}

int main(int argc, char* argv[])
{
    std::cout << "Starting test with opencv 4.1.1\n";

    if (3 == argc)
    {
        if (0 == strcmp("-d", argv[1]))
        {
            std::cout << "Starting the test in directory: " << argv[2] << std::endl;
            string dir_path(argv[2]);

            if (!fs::exists(dir_path) ||
                !fs::is_directory(dir_path))
            {
                std::cout << "Source directory doesn't exist or given path is not a directory. The test will terminate..." << std::endl;
                return -1;
            }

            if (0 == strcmp(&dir_path.back(), "/") ||
                0 == strcmp(&dir_path.back(), "\\"))
            {
                dir_path.pop_back();
            }

            ofstream result_file;
            fs::path src_dir_path(dir_path);
            fs::path result_file_path(fs::current_path());
            fs::path result_file_stem = src_dir_path.stem();
            result_file_path += "\\results_";
            result_file_path += result_file_stem;
            result_file_path += "_411.txt";

            //result_file.open(dir_path + "\\" + "results.txt", ofstream::out | ofstream::trunc);
            result_file.open(result_file_path, ofstream::out | ofstream::trunc);

            if (!result_file.is_open())
            {
                cout << "Can't open result file!" << endl;
                return -1;
            }

            result_file << "Image name" << "\t" << "Num circles found (difference)" << "\t" << "first found circle X (difference)" << "\t"
                << "first found circle Y (difference)" << "\t" << "first found circle R (difference)" << "\t" << "Num circles found (orig)" << "\t" << "first found circle X (orig)" << "\t"
                << "first found circle Y (orig)" << "\t" << "first found circle R (orig)" << "\t" << "Num circles found" << "\t" << "first found circle X" << "\t" << "first found circle Y" << "\t"
                << "first found circle R" << "\t" << "ROI X" << "\t" << "ROI Y" << "\t" << "ROI W" << "\t" << "ROI H" << "\t" << "HC Param min dist" << "\t"
                << "HC Param Canny Threashold" << "\t" << "HC Param Min Votes" << "\t" << "HC Param min r" << "\t" << "HC Param max r" << endl;

            list <fs::path> txt_entries;
            list <fs::path> img_in_entries;
            list <fs::path> img_dbg_entries;

            for (const auto& entry : fs::directory_iterator(dir_path))
            {
                string extension = entry.path().extension().string();

                if (entry.is_regular_file())
                {
                    if (".txt" == entry.path().extension() &&
                        "results" != entry.path().stem())
                    {
                        txt_entries.push_back(entry);
                    }
                    else if (".png" == extension ||
                        ".jpg" == extension ||
                        ".bmp" == extension)
                    {
                        if (string::npos != entry.path().string().find("_dbg"))
                        {
                            img_dbg_entries.push_back(entry);
                        }
                        else
                        {
                            img_in_entries.push_back(entry);
                        }
                    }
                    else
                    {
                        // nothing to do here (unknown file)
                    }
                }
            }

            //search the circle
            for (const auto& txt_entry : txt_entries)
            {
                // test
                double dDist, dCanny, dVotes, dCx, dCy, dR, dCxOrig, dCyOrig, dROrig;
                int nX, nY, nW, nH, nMin, nMax, nCount, nCountOrig;
                if (read_params(txt_entry, nX, nY, nW, nH, dDist, dCanny, dVotes, nMin, nMax, nCountOrig, dCxOrig, dCyOrig, dROrig))
                {
                    string dbg_img_path_name = txt_entry.stem().string();
                    dbg_img_path_name += "_dbg";
                    fs::path dbg_img_path = find_dbg_filename(img_dbg_entries, dbg_img_path_name);

                    Mat input_img;
                    input_img = imread(dbg_img_path.string(), 0);

                    if (input_img.data)
                    {
                        flip(input_img, input_img, 0);

                        if (find_hough_circles(input_img, nX, nY, nW, nH, dDist, dCanny, dVotes, nMin, nMax, nCount, dCx, dCy, dR))
                        {
                            int nDCount = abs(nCountOrig - nCount);
                            float dDX = abs(dCxOrig - dCx);
                            float dDY = abs(dCyOrig - dCy);
                            float dDR = abs(dROrig - dR);

                            result_file << txt_entry.stem().string() << "\t" << nDCount << "\t" << std::fixed << dDX << "\t" << dDY << "\t" << dDR << "\t" << nCountOrig << "\t" << dCxOrig << "\t"
                                << dCyOrig << "\t" << dROrig << "\t" << nCount << "\t" << dCx << "\t" << dCy << "\t"
                                << dR << "\t" << nX << "\t" << nY << "\t" << nW << "\t" << nH << "\t" << dDist << "\t"
                                << dCanny << "\t" << dVotes << "\t" << nMin << "\t" << nMax << endl;

                            if (nCount)
                            {
                                try
                                {
                                    Mat out_img;
                                    cvtColor(input_img, out_img, cv::COLOR_GRAY2RGB);
                                    Rect roi = Rect(nX, nY, nW, nH);
                                    Mat roi_image = out_img(roi);
                                    circle(roi_image, cv::Point(dCx, dCy), (int)dR, cv::Scalar(255, 255, 0));
                                    fs::path out_img_path(src_dir_path);
                                    out_img_path += "\\";
                                    out_img_path += txt_entry.stem().string();
                                    out_img_path += "_out_411.png";
                                    imwrite(out_img_path.string(), roi_image);
                                }
                                catch (const cv::Exception & e)
                                {
                                    cout << e.what() << endl;
                                }
                            }
                        }
                        else
                        {
                            cout << "Error occured while searchign hough circles in: " << dbg_img_path_name << endl;
                            result_file << "Error occured while searchign hough circles in: " << dbg_img_path_name << endl;
                        }
                    }
                    else
                    {
                        cout << "Can't read image: " << dbg_img_path_name << endl;
                        result_file << "Can't read image: " << dbg_img_path_name << endl;
                    }
                }
                else
                {
                    cout << "Can't read parameter file: " << txt_entry << endl;
                    result_file << "Can't read parameter file: " << txt_entry << endl;
                }
            }

            std::cout << "Results written to: " << result_file_path << std::endl;
        }
        else
        {
            std::cout << "Wrong number of parameters, test will terminate..." << std::endl;
            return -1;
        }
    }
    else
    {
        std::cout << "Wrong number of parameters, test will terminate..." << std::endl;
        return -1;
    }

    return 0;
}

Following test data show the input images (_dbg) results of the 3.1.0 (magenta circles) and results of the 4.1.1 (cyan circles) The tendence of the algorithm to provide mostly large circles can be in the 4.1.1 suppressed by the normalisation (votes/diameter) as the new algorithm provides also the votes of the found circles. This leads to not much better results. The text files contain the parameters which have been used during circles matching. The data have to be put in the directory and path to the directory has to be used as a parameter in the command line.

04925_01_013_04925d080_green_out
04925_01_013_04925d080_green_out_411
04936_01_021_04936d147_brown_dbg
04936_01_021_04936d147_brown_out
04936_01_021_04936d147_brown_out_411
04890_02_021_04890d462_brown.txt
04890_02_021_04890d462_brown_dbg
04890_02_021_04890d462_brown_out
04890_02_021_04890d462_brown_out_411
04908_01_001_04908d098_hazel_dbg
04908_01_001_04908d098_hazel_out
04908_01_001_04908d098_hazel_out_411
04913_01_001_04913d010_blue_dbg
04913_01_001_04913d010_blue_out
04913_01_001_04913d010_blue_out_411
04925_01_013_04925d080_green_dbg

Metadata

Metadata

Assignees

Labels

Hackathonhttps://opencv.org/opencv-hackathon-starts-next-week/category: imgproc

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions