gil icon indicating copy to clipboard operation
gil copied to clipboard

Add example of canny edge detection algorithm

Open Antropovi opened this issue 5 years ago • 10 comments

Add example of canny edge detection using gil as my competency test for GSoC 2019

References

Find more about Canny edge detection on https://en.wikipedia.org/wiki/Canny_edge_detector

Tasklist

  • [ ] Review
  • [ ] Adjust for comments
  • ~~All CI builds and checks have passed~~ ATM, examples are not built by CI jobs

Antropovi avatar Mar 15 '19 14:03 Antropovi

For complete records and references, here is @Antropovi confirmation this is intended as competency test:

https://lists.boost.org/Archives/boost/2019/03/245630.php

As my competencytest, I have implement a simple Canny edge detection algorithm using GIL. https://github.com/boostorg/gil/pull/258

mloskot avatar Mar 29 '19 20:03 mloskot

I believe there is a bug in the last hysteresis step: it will only detect Г shaped edges, and not horizontally flipped L shaped edges. Let me give an example (the values are somewhere in the image padded by zeroes on all sides, the values are intensity of the pixel's luminosity):

Case which current code will detect:

1 0.5 0.5               1 1 1
0.5 0 0         ====>   1 0 0
05. 0 0                 1 0 0

Case which current code will not detect

0.5 0.5 0.5            1 1 1
0.5 0 0.5        ====> 1 0 1
0.5 0.5 1              1 1 1

Here are the lines of code which are connected to the issues:

    // Weak pixels check
    // If weak pixel has a strong neighbor, then it is strong one
    // otherwise it is definitely not-edge pixel
    auto dst_loc = dst.xy_at(1, 1);
    for (int i = 1; i < dst.height() - 1; ++i) {
        for (int j = 1; j < dst.width() - 1; ++j, ++dst_loc.x()) {
            if (dst_loc(0, 0) == MIDDLE_VALUE) {
                if (dst_loc(-1, -1) == TOP_VALUE || dst_loc(-1, 0) == TOP_VALUE ||
                        dst_loc(-1, 1) == TOP_VALUE || dst_loc(1, -1) == TOP_VALUE ||
                        dst_loc(1, 0) == TOP_VALUE || dst_loc(1, 1) == TOP_VALUE ||
                        dst_loc(0, -1) == TOP_VALUE || dst_loc(0, 1) == TOP_VALUE)
                    dst_loc(0, 0) = TOP_VALUE;
                else
                    dst_loc(0, 0) = BOTTOM_VALUE;
            }
        }
        dst_loc += point2<std::ptrdiff_t>(-dst.width() + 2, 1);
    }

Instead, I believe there should be connected components search. The search process basically tries to hop over weak edge or strong edge and reach all of the reachable pixels from start location. If there is at least one strong pixel in the connected component, all should be marked as strong too.

Could you please verify if my theory is correct? I believe a good input image for this test case should be one of the O shaped Unicode characters with upper diagonal part blurred out.

simmplecoder avatar Apr 02 '19 13:04 simmplecoder

This isn't a bug and there are some reasons, why I chose this implementation. First and most important - this is an example. It should be easy to understand. There are a lot of guides and blog posts about this specific implementation. Second - this is simple Canny algorithm. There are more precise and complex algorithms to detect edges. This one dont even construct a list of all edges. Third - you are right. If you use image and rotated on 180 copy of it, output will be slightly different, but the main point still how to choose best threshold values. And btw hysteresis step is not about detection edges. It's more about connection neighbour edges.

Antropovi avatar Apr 03 '19 19:04 Antropovi

Sorry for choosing confusing wording. I meant that weak edges that should've been included in the output will not be included. Thanks for clarification on the decisions.

simmplecoder avatar Apr 03 '19 20:04 simmplecoder

@Antropovi Thank you very much again for your example and explanation. I'd like to incorporate it into GIL's collection in example/. Hopefully, I'll be able to do it in near future. So, let's keep this PR open.

mloskot avatar Apr 09 '19 13:04 mloskot

@stefanseefeld Shall we add those as they are (optionally, with minor stylistic refactoring) to example/ folder? I think, it's never too many examples showing how to actually use GIL. What do you think?

OTOH, perhaps this could be reworked as a library feature, a callable algorithm, not just an example. Any takers?

mloskot avatar Apr 28 '19 01:04 mloskot

With boost 1.80.0 and -std=c++20

I get the following compile errors:

/usr/include/boost/gil/extension/numeric/kernel.hpp:16:1: note: ?#pragma message: This header is deprecated. Use <boost/gil/image_processing/kernel.hpp> instead.?
   16 | BOOST_HEADER_DEPRECATED("<boost/gil/image_processing/kernel.hpp>")
      | ^~~~~~~~~~~~~~~~~~~~~~~
/usr/include/boost/gil/extension/numeric/convolve.hpp:15:1: note: ?#pragma message: This header is deprecated. Use <boost/gil/image_processing/convolve.hpp> instead.?
   15 | BOOST_HEADER_DEPRECATED("<boost/gil/image_processing/convolve.hpp>")
      | ^~~~~~~~~~~~~~~~~~~~~~~
In file included from src/rgw16_edge_align.cpp:32:
src/rgw16_edge_detect.hpp: In function ?void gaussian_blur(const SrcView&)?:
src/rgw16_edge_detect.hpp:49:52: error: ?convolve_option_output_ignore? was not declared in this scope
   49 |   convolve_rows<gray32f_pixel_t>(src, kernel, src, convolve_option_output_ignore);
      |                                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/rgw16_edge_detect.hpp: In function ?void sobel_filtering(const boost::gil::gray8c_view_t&, const boost::gil::gray8_view_t&, const boost::gil::gray32f_view_t&)?:
src/rgw16_edge_detect.hpp:76:75: error: ?convolve_option_output_zero? was not declared in this scope
   76 |   convolve_rows<gray32f_pixel_t>(src, first_sobel_kernel, view(vertical), convolve_option_output_zero);
      |                                                                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~
src/rgw16_edge_detect.hpp: In function ?int main_detect(int, char**)?:
src/rgw16_edge_detect.hpp:229:35: error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  229 |   char* input                   = "test.jpg";
      |                                   ^~~~~~~~~~
src/rgw16_edge_detect.hpp:230:35: error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  230 |   char* output                  = "canny.jpg";
      |                                   ^~~~~~~~~~~
src/rgw16_edge_detect.hpp: In function ?void non_maximal_suppression(const boost::gil::gray8c_view_t&, const boost::gil::gray32fc_view_t&, const boost::gil::gray8_view_t&)?:
src/rgw16_edge_detect.hpp:120:10: error: ?r? may be used uninitialized [-Werror=maybe-uninitialized]
  120 |   int q, r;
      |          ^
src/rgw16_edge_detect.hpp:120:7: error: ?q? may be used uninitialized [-Werror=maybe-uninitialized]
  120 |   int q, r;
      |       ^

All of which are trivially fixed.

It would be nice if example code did not contain compile errors.

Bockeman avatar Oct 26 '22 16:10 Bockeman

@Bockeman it would be nice if you made it clear that your comment relates to this particular PR in any way. Otherwise, it would be nice if you posted it to discussion thread or report a bug or it would be even nicer if you could submit PR with the trivial fixes. Thanks in advance.

mloskot avatar Oct 26 '22 17:10 mloskot

@mloskot Thanks for taking notice! (I have not yet figured out what PR stands for. Not doubt as soon as I submit this comment, it will dawn on me.)

My comment above relates to the example/canny.cpp code, the only one in the "files changed" tab above. (Forgive me, I had assumed this was obvious)

Here are my trivial fixes: (But I am fairly certain these are not in the most appropriate format, and I am nowhere near confident enough to attempt to edit/annoate the source code, as others have done beautifully [with coloured code segments, etc.] above.)

/usr/include/boost/gil/extension/numeric/kernel.hpp:16:1: note: ?#pragma message: This header is deprecated. Use <boost/gil/image_processing/kernel.hpp> instead.?
   16 | BOOST_HEADER_DEPRECATED("<boost/gil/image_processing/kernel.hpp>")
      | ^~~~~~~~~~~~~~~~~~~~~~~

my fix:

//#include <boost/gil/extension/numeric/kernel.hpp>		// #pragma message: This header is deprecated.
#include <boost/gil/image_processing/kernel.hpp>
/usr/include/boost/gil/extension/numeric/convolve.hpp:15:1: note: ?#pragma message: This header is deprecated. Use <boost/gil/image_processing/convolve.hpp> instead.?
   15 | BOOST_HEADER_DEPRECATED("<boost/gil/image_processing/convolve.hpp>")
      | ^~~~~~~~~~~~~~~~~~~~~~~

my fix:

//#include <boost/gil/extension/numeric/convolve.hpp>		// #pragma message: This header is deprecated.
#include <boost/gil/image_processing/convolve.hpp>
In file included from src/rgw16_edge_align.cpp:32:
src/rgw16_edge_detect.hpp: In function ?void gaussian_blur(const SrcView&)?:
src/rgw16_edge_detect.hpp:49:52: error: ?convolve_option_output_ignore? was not declared in this scope
   49 |   convolve_rows<gray32f_pixel_t>(src, kernel, src, convolve_option_output_ignore);
      |                                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

my fix:

  convolve_rows<gray32f_pixel_t>(src, kernel, src
  , boundary_option::output_ignore);
  //, convolve_option_output_ignore);	// error: ?convolve_option_output_ignore? was not declared in this scope
  convolve_cols<gray32f_pixel_t>(src, kernel, src
  , boundary_option::output_ignore);
  //, convolve_option_output_ignore);	// error: ?convolve_option_output_ignore? was not declared in this scope
src/rgw16_edge_detect.hpp: In function ?void sobel_filtering(const boost::gil::gray8c_view_t&, const boost::gil::gray8_view_t&, const boost::gil::gray32f_view_t&)?:
src/rgw16_edge_detect.hpp:76:75: error: ?convolve_option_output_zero? was not declared in this scope
   76 |   convolve_rows<gray32f_pixel_t>(src, first_sobel_kernel, view(vertical), convolve_option_output_zero);
      |                                                                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~

my fix:

  convolve_rows<gray32f_pixel_t>(src, first_sobel_kernel, view(vertical)
  , boundary_option::output_ignore);
  //, convolve_option_output_zero);	// error: ?convolve_option_output_ignore? was not declared in this scope
  convolve_cols<gray32f_pixel_t>(const_view(vertical), second_sobel_kernel, view(vertical)
  , boundary_option::output_ignore);
  //, convolve_option_output_zero);	// error: ?convolve_option_output_ignore? was not declared in this scope

  convolve_rows<gray32f_pixel_t>(src, second_sobel_kernel, view(horizontal)
  , boundary_option::output_ignore);
  //, convolve_option_output_zero);	// error: ?convolve_option_output_ignore? was not declared in this scope
  convolve_cols<gray32f_pixel_t>(const_view(horizontal), first_sobel_kernel, view(horizontal)
  , boundary_option::output_ignore);
  //, convolve_option_output_zero);	// error: ?convolve_option_output_ignore? was not declared in this scope
src/rgw16_edge_detect.hpp: In function ?int main_detect(int, char**)?:
src/rgw16_edge_detect.hpp:229:35: error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  229 |   char* input                   = "test.jpg";
      |                                   ^~~~~~~~~~
src/rgw16_edge_detect.hpp:230:35: error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  230 |   char* output                  = "canny.jpg";
      |                                   ^~~~~~~~~~~

my fix:

  //char* input			= "test.jpg";	// error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  //char* output		= "canny.jpg";	// error: ISO C++ forbids converting a string constant to ?char*? [-Werror=write-strings]
  //char  input[]			= "test.jpg";	// below: error: incompatible types in assignment of ?char*? to ?char [9]?
  //char  output[]		= "canny.jpg";	// below: error: incompatible types in assignment of ?char*? to ?char [10]?
  std::string	input		= "test.jpg";
  std::string	output		= "canny.jpg";
src/rgw16_edge_detect.hpp: In function ?void non_maximal_suppression(const boost::gil::gray8c_view_t&, const boost::gil::gray32fc_view_t&, const boost::gil::gray8_view_t&)?:
src/rgw16_edge_detect.hpp:120:10: error: ?r? may be used uninitialized [-Werror=maybe-uninitialized]
  120 |   int q, r;
      |          ^
src/rgw16_edge_detect.hpp:120:7: error: ?q? may be used uninitialized [-Werror=maybe-uninitialized]
  120 |   int q, r;
      |       ^
  //int q, r;		// error: ?r? may be used uninitialized [-Werror=maybe-uninitialized]
  int q(0);
  int r(0);

Thanks @Antropovi for contributing this code. It works well for me and saved me a lot of time and effort in painstakingly constucting such from scratch. (Aside: I am currently attempting some tweaks, because unwanted artefacts are also being detected as edges)

Bockeman avatar Oct 26 '22 21:10 Bockeman

@Bockeman

(I have not yet figured out what PR stands for.

PR = Pull Request

My comment above relates to the example/canny.cpp code, the only one in the "files changed" tab above.

I see. Well, this PR is a proposal and it has not been accepted and merged into the GIL sources. So it should be considered as a work in progress.

(Aside: I am currently attempting some tweaks, because unwanted artefacts are also being detected as edges)

If you have an improved version of this example here, then please, feel free to contribute it to GIL and submit your version as new PR.

mloskot avatar Oct 27 '22 07:10 mloskot