    // Copyright (C) 2014  Davis E. King (davis@dlib.net)
    // License: Boost Software License   See LICENSE.txt for the full license.
    #ifndef DLIB_HOUGH_tRANSFORM_Hh_
    #define DLIB_HOUGH_tRANSFORM_Hh_

    #include "hough_transform_abstract.h"
    #include "../image_processing/generic_image.h"
    #include "../geometry.h"
    #include "../algs.h"
    #include "assign_image.h"
    #include <limits>

    namespace dlib
    {

    // ----------------------------------------------------------------------------------------

        class hough_transform
        {

        public:
            explicit hough_transform (
                unsigned long size_
            ) : _size(size_)
            {
                DLIB_CASSERT(size_ > 0,
                    "\t hough_transform::hough_transform(size_)"
                    << "\n\t Invalid arguments given to this function."
                    );

                even_size = _size - (_size%2);

                const point cent = center(rectangle(0,0,size_-1,size_-1));
                xcos_theta.set_size(size_, size_);
                ysin_theta.set_size(size_, size_);

                std::vector<double> cos_theta(size_), sin_theta(size_);
                const double scale = 1<<16;
                for (unsigned long t = 0; t < size_; ++t)
                {
                    double theta = t*pi/even_size;

                    cos_theta[t] = scale*std::cos(theta)/sqrt_2;
                    sin_theta[t] = scale*std::sin(theta)/sqrt_2;
                }
                const double offset = scale*even_size/4.0 + 0.5;

                for (unsigned long c = 0; c < size_; ++c)
                {
                    const long x = c - cent.x();
                    for (unsigned long t = 0; t < size_; ++t)
                        xcos_theta(c,t) = static_cast<int32>(x*cos_theta[t] + offset);
                }
                for (unsigned long r = 0; r < size_; ++r)
                {
                    const long y = r - cent.y();
                    for (unsigned long t = 0; t < size_; ++t)
                        ysin_theta(r,t) = static_cast<int32>(y*sin_theta[t] + offset);
                }
            }

            inline unsigned long size(
            ) const { return _size; }

            long nr(
            ) const { return _size; }

            long nc(
            ) const { return _size; }

            std::pair<dpoint, dpoint> get_line (
                const dpoint& p
            ) const
            {
                DLIB_ASSERT(rectangle(0,0,size()-1,size()-1).contains(p) == true,
                    "\t pair<dpoint,dpoint> hough_transform::get_line(dpoint)"
                    << "\n\t Invalid arguments given to this function."
                    << "\n\t p:      " << p
                    << "\n\t size(): " << size()
                    );

                // First we compute the radius measured in pixels from the center and the theta
                // angle in radians.
                double theta, radius;
                get_line_properties(p, theta, radius);
                theta *= pi/180;

                // now make a line segment on the line.
                const rectangle box = get_rect(*this);
                const dpoint cent = center(box);
                dpoint v1 = cent + dpoint(size()+1000,0) + dpoint(0,radius);
                dpoint v2 = cent - dpoint(size()+1000,0) + dpoint(0,radius);
                dpoint p1 = rotate_point(cent, v1, theta);
                dpoint p2 = rotate_point(cent, v2, theta);

                clip_line_to_rectangle(box, p1, p2);

                return std::make_pair(p1,p2);
            }

            double get_line_angle_in_degrees (
                const dpoint& p
            ) const
            {
                double angle, radius;
                get_line_properties(p, angle, radius);
                return angle;
            }

            void get_line_properties (
                const dpoint& p,
                double& angle_in_degrees,
                double& radius
            ) const
            {
                const dpoint cent = center(get_rect(*this));
                double theta = p.x()-cent.x();
                radius = p.y()-cent.y();
                angle_in_degrees = 180*theta/even_size;
                radius = radius*sqrt_2 + 0.5;
            }

            template <
                typename image_type
                >
            point get_best_hough_point (
                const point& p,
                const image_type& himg_
            )
            {
                const const_image_view<image_type> himg(himg_);

                DLIB_ASSERT(himg.nr() == size() && himg.nc() == size() &&
                    rectangle(0,0,size()-1,size()-1).contains(p) == true,
                    "\t point hough_transform::get_best_hough_point()"
                    << "\n\t Invalid arguments given to this function."
                    << "\n\t himg.nr(): " << himg.nr()
                    << "\n\t himg.nc(): " << himg.nc()
                    << "\n\t size():    " << size()
                    << "\n\t p:         " << p
                    );

                typedef typename image_traits<image_type>::pixel_type pixel_type;
                COMPILE_TIME_ASSERT(pixel_traits<pixel_type>::grayscale == true);
                pixel_type best_val = std::numeric_limits<pixel_type>::min();
                point best_point;

                const long max_n8 = (himg.nc()/8)*8;
                const long max_n4 = (himg.nc()/4)*4;
                const long r = p.y();
                const long c = p.x();

                const int32* ysin = &ysin_theta(r,0);
                const int32* xcos = &xcos_theta(c,0);
                long t = 0;
                while(t < max_n8)
                {
                    long rr0 = (*xcos++ + *ysin++)>>16;
                    long rr1 = (*xcos++ + *ysin++)>>16;
                    long rr2 = (*xcos++ + *ysin++)>>16;
                    long rr3 = (*xcos++ + *ysin++)>>16;
                    long rr4 = (*xcos++ + *ysin++)>>16;
                    long rr5 = (*xcos++ + *ysin++)>>16;
                    long rr6 = (*xcos++ + *ysin++)>>16;
                    long rr7 = (*xcos++ + *ysin++)>>16;

                    if (himg[rr0][t++] > best_val)
                    {
                        best_val = himg[rr0][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr0;
                    }
                    if (himg[rr1][t++] > best_val)
                    {
                        best_val = himg[rr1][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr1;
                    }
                    if (himg[rr2][t++] > best_val)
                    {
                        best_val = himg[rr2][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr2;
                    }
                    if (himg[rr3][t++] > best_val)
                    {
                        best_val = himg[rr3][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr3;
                    }
                    if (himg[rr4][t++] > best_val)
                    {
                        best_val = himg[rr4][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr4;
                    }
                    if (himg[rr5][t++] > best_val)
                    {
                        best_val = himg[rr5][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr5;
                    }
                    if (himg[rr6][t++] > best_val)
                    {
                        best_val = himg[rr6][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr6;
                    }
                    if (himg[rr7][t++] > best_val)
                    {
                        best_val = himg[rr7][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr7;
                    }
                }
                while(t < max_n4)
                {
                    long rr0 = (*xcos++ + *ysin++)>>16;
                    long rr1 = (*xcos++ + *ysin++)>>16;
                    long rr2 = (*xcos++ + *ysin++)>>16;
                    long rr3 = (*xcos++ + *ysin++)>>16;
                    if (himg[rr0][t++] > best_val)
                    {
                        best_val = himg[rr0][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr0;
                    }
                    if (himg[rr1][t++] > best_val)
                    {
                        best_val = himg[rr1][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr1;
                    }
                    if (himg[rr2][t++] > best_val)
                    {
                        best_val = himg[rr2][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr2;
                    }
                    if (himg[rr3][t++] > best_val)
                    {
                        best_val = himg[rr3][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr3;
                    }
                }
                while(t < himg.nc())
                {
                    long rr0 = (*xcos++ + *ysin++)>>16;
                    if (himg[rr0][t++] > best_val)
                    {
                        best_val = himg[rr0][t-1];
                        best_point.x() = t-1;
                        best_point.y() = rr0;
                    }
                }

                return best_point;
            }

            template <
                typename in_image_type,
                typename out_image_type
                >
            void operator() (
                const in_image_type& img_,
                const rectangle& box,
                out_image_type& himg_
            ) const
            {
                typedef typename image_traits<in_image_type>::pixel_type in_pixel_type;
                typedef typename image_traits<out_image_type>::pixel_type out_pixel_type;

                DLIB_CASSERT(box.width() == size() && box.height() == size(),
                    "\t void hough_transform::operator()"
                    << "\n\t Invalid arguments given to this function."
                    << "\n\t box.width():  " << box.width()
                    << "\n\t box.height(): " << box.height()
                    << "\n\t size():       " << size()
                    );

                COMPILE_TIME_ASSERT(pixel_traits<in_pixel_type>::grayscale == true);
                COMPILE_TIME_ASSERT(pixel_traits<out_pixel_type>::grayscale == true);

                const_image_view<in_image_type> img(img_);
                image_view<out_image_type> himg(himg_);

                himg.set_size(size(), size());
                assign_all_pixels(himg, 0);

                auto record_hit = [&](const point& hough_point, const point& /*img_point*/, const in_pixel_type& val)
                {
                    himg[hough_point.y()][hough_point.x()] += val;
                };
                perform_generic_hough_transform(img_, box, record_hit);
            }

            template <
                typename in_image_type,
                typename out_image_type
                >
            void operator() (
                const in_image_type& img_,
                out_image_type& himg_
            ) const
            {
                rectangle box(0,0, num_columns(img_)-1, num_rows(img_)-1);
                (*this)(img_, box, himg_);
            }

            template <
                typename in_image_type
                >
            std::vector<std::vector<point>> find_pixels_voting_for_lines (
                const in_image_type& img,
                const rectangle& box,
                const std::vector<point>& hough_points,
                const unsigned long angle_window_size = 1,
                const unsigned long radius_window_size = 1
            ) const
            {

                typedef typename image_traits<in_image_type>::pixel_type in_pixel_type;

                DLIB_CASSERT(angle_window_size >= 1);
                DLIB_CASSERT(radius_window_size >= 1);
                DLIB_CASSERT(box.width() == size() && box.height() == size(),
                    "\t std::vector<std::vector<point>> hough_transform::find_pixels_voting_for_lines()"
                    << "\n\t Invalid arguments given to this function."
                    << "\n\t box.width():  " << box.width()
                    << "\n\t box.height(): " << box.height()
                    << "\n\t size():       " << size()
                    );
    #ifdef ENABLE_ASSERTS
                for (auto& p : hough_points)
                    DLIB_CASSERT(get_rect(*this).contains(p),
                        "You gave a hough_points that isn't actually in the Hough space of this object."
                        << "\n\t get_rect(*this): "<< get_rect(*this)
                        << "\n\t p: "<< p
                        );
    #endif

                std::vector<std::vector<point>> constituent_points(hough_points.size());

                // make a map that lets us look up in constant time if a hough point is in the
                // constituent_points output and if so where.
                matrix<uint32> hmap(size(),size());
                hmap = hough_points.size();
                for (size_t i = 0; i < hough_points.size(); ++i)
                {
                    rectangle area = centered_rect(hough_points[i],angle_window_size,radius_window_size).intersect(get_rect(hmap));
                    for (long r = area.top(); r <= area.bottom(); ++r)
                    {
                        for (long c = area.left(); c <= area.right(); ++c)
                        {
                            hmap(r,c) = i;
                        }
                    }
                }

                // record that this image point voted for this Hough point
                auto record_hit = [&](const point& hough_point, const point& img_point, in_pixel_type)
                {
                    auto idx = hmap(hough_point.y(), hough_point.x());
                    if (idx < constituent_points.size())
                    {
                        // don't add img_point if it's already in the list.
                        if (constituent_points[idx].size() == 0 || constituent_points[idx].back() != img_point)
                            constituent_points[idx].push_back(img_point);
                    }
                };

                perform_generic_hough_transform(img, box, record_hit);

                return constituent_points;
            }

            template <
                typename in_image_type
                >
            std::vector<std::vector<point>> find_pixels_voting_for_lines (
                const in_image_type& img,
                const std::vector<point>& hough_points,
                const unsigned long angle_window_size = 1,
                const unsigned long radius_window_size = 1
            ) const
            {
                rectangle box(0,0, num_columns(img)-1, num_rows(img)-1);
                return find_pixels_voting_for_lines(img, box, hough_points, angle_window_size, radius_window_size);
            }

            template <
                typename image_type,
                typename thresh_type
                >
            std::vector<point> find_strong_hough_points(
                const image_type& himg_,
                const thresh_type hough_count_threshold,
                const double angle_nms_thresh,
                const double radius_nms_thresh
            )
            {
                const_image_view<image_type> himg(himg_);

                DLIB_CASSERT(himg.nr() == size());
                DLIB_CASSERT(himg.nc() == size());
                DLIB_CASSERT(angle_nms_thresh >= 0)
                DLIB_CASSERT(radius_nms_thresh >= 0)

                std::vector<std::pair<double,point>> initial_lines;
                for (long r = 0; r < himg.nr(); ++r)
                {
                    for (long c = 0; c < himg.nc(); ++c)
                    {
                        if (himg[r][c] >= hough_count_threshold)
                            initial_lines.emplace_back(himg[r][c], point(c,r));
                    }
                }

                std::vector<point> final_lines;
                std::vector<std::pair<double,double>> final_angle_and_radius;

                // Now do non-max suppression.  First, sort the initial_lines so the best lines come first.
                std::sort(initial_lines.rbegin(), initial_lines.rend(),
                    [](const std::pair<double,point>& a, const std::pair<double,point>& b){ return a.first<b.first;});
                for (auto& r : initial_lines)
                {
                    double angle, radius;
                    get_line_properties(r.second, angle, radius);

                    // check if anything in final_lines is too close to r.second.  If
                    // something is found then discard r.second.
                    auto too_close = false;
                    for (auto& ref : final_angle_and_radius)
                    {
                        auto& ref_angle = ref.first;
                        auto& ref_radius = ref.second;

                        // We need to check for wrap around in angle since, for instance, a
                        // line with angle and radius of 90 and 10 is the same line as one with
                        // angle -90 and radius -10.
                        if ((std::abs(ref_angle - angle) < angle_nms_thresh && std::abs(ref_radius-radius) < radius_nms_thresh) ||
                            (180 - std::abs(ref_angle - angle) < angle_nms_thresh && std::abs(ref_radius+radius) < radius_nms_thresh))
                        {
                            too_close = true;
                            break;
                        }
                    }

                    if (!too_close)
                    {
                        final_lines.emplace_back(r.second);
                        final_angle_and_radius.emplace_back(angle, radius);
                    }
                }

                return final_lines;
            }

            template <
                typename in_image_type,
                typename record_hit_function_type
                >
            void perform_generic_hough_transform (
                const in_image_type& img_,
                const rectangle& box,
                record_hit_function_type record_hit
            ) const
            {

                typedef typename image_traits<in_image_type>::pixel_type in_pixel_type;

                DLIB_ASSERT(box.width() == size() && box.height() == size(),
                    "\t void hough_transform::perform_generic_hough_transform()"
                    << "\n\t Invalid arguments given to this function."
                    << "\n\t box.width():  " << box.width()
                    << "\n\t box.height(): " << box.height()
                    << "\n\t size():       " << size()
                    );

                COMPILE_TIME_ASSERT(pixel_traits<in_pixel_type>::grayscale == true);

                const_image_view<in_image_type> img(img_);

                const rectangle area = box.intersect(get_rect(img));

                const long max_n8 = (size()/8)*8;
                const long max_n4 = (size()/4)*4;
                for (long r = area.top(); r <= area.bottom(); ++r)
                {
                    const int32* ysin_base = &ysin_theta(r-box.top(),0);
                    for (long c = area.left(); c <= area.right(); ++c)
                    {
                        const auto val = img[r][c];
                        if (val != 0)
                        {
                            /*
                            // The code in this comment is equivalent to the more complex but
                            // faster code below.  We keep this simple version of the Hough
                            // transform implementation here just to document what it's doing
                            // more clearly.
                            const point cent = center(box);
                            const long x = c - cent.x();
                            const long y = r - cent.y();
                            for (long t = 0; t < size(); ++t)
                            {
                                double theta = t*pi/even_size;
                                double radius = (x*std::cos(theta) + y*std::sin(theta))/sqrt_2 + even_size/2 + 0.5;
                                long rr = static_cast<long>(radius);

                                record_hit(point(t,rr), point(c,r), val);
                            }
                            continue;
                            */

                            // Run the speed optimized version of the code in the above
                            // comment.
                            const int32* ysin = ysin_base;
                            const int32* xcos = &xcos_theta(c-box.left(),0);
                            long t = 0;
                            while(t < max_n8)
                            {
                                long rr0 = (*xcos++ + *ysin++)>>16;
                                long rr1 = (*xcos++ + *ysin++)>>16;
                                long rr2 = (*xcos++ + *ysin++)>>16;
                                long rr3 = (*xcos++ + *ysin++)>>16;
                                long rr4 = (*xcos++ + *ysin++)>>16;
                                long rr5 = (*xcos++ + *ysin++)>>16;
                                long rr6 = (*xcos++ + *ysin++)>>16;
                                long rr7 = (*xcos++ + *ysin++)>>16;

                                record_hit(point(t++,rr0), point(c,r), val);
                                record_hit(point(t++,rr1), point(c,r), val);
                                record_hit(point(t++,rr2), point(c,r), val);
                                record_hit(point(t++,rr3), point(c,r), val);
                                record_hit(point(t++,rr4), point(c,r), val);
                                record_hit(point(t++,rr5), point(c,r), val);
                                record_hit(point(t++,rr6), point(c,r), val);
                                record_hit(point(t++,rr7), point(c,r), val);
                            }
                            while(t < max_n4)
                            {
                                long rr0 = (*xcos++ + *ysin++)>>16;
                                long rr1 = (*xcos++ + *ysin++)>>16;
                                long rr2 = (*xcos++ + *ysin++)>>16;
                                long rr3 = (*xcos++ + *ysin++)>>16;
                                record_hit(point(t++,rr0), point(c,r), val);
                                record_hit(point(t++,rr1), point(c,r), val);
                                record_hit(point(t++,rr2), point(c,r), val);
                                record_hit(point(t++,rr3), point(c,r), val);
                            }
                            while(t < (long)size())
                            {
                                long rr0 = (*xcos++ + *ysin++)>>16;
                                record_hit(point(t++,rr0), point(c,r), val);
                            }
                        }
                    }
                }
            }

            template <
                typename in_image_type,
                typename record_hit_function_type
                >
            void perform_generic_hough_transform (
                const in_image_type& img_,
                record_hit_function_type record_hit
            ) const
            {
                rectangle box(0,0, num_columns(img_)-1, num_rows(img_)-1);
                perform_generic_hough_transform(img_, box, record_hit);
            }

        private:

            unsigned long _size;
            unsigned long even_size; // equal to _size if _size is even, otherwise equal to _size-1.
            matrix<int32> xcos_theta, ysin_theta;
        };
    }

    #endif // DLIB_HOUGH_tRANSFORM_Hh_