├── CMakeLists.txt ├── src ├── CMakeLists.txt ├── hcd.h ├── main.cpp └── hcd.cpp └── README.markdown /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # project name 2 | PROJECT(HOUGH_CIRCLE_DETECTOR) 3 | 4 | # 2.4.3 is actual minimum, but you have to add additional hacks 5 | # uncomment this if you need 2.4.3 compatibility 6 | # if(COMMAND cmake_policy) 7 | # cmake_policy(SET CMP0003 NEW) 8 | # endif(COMMAND cmake_policy) 9 | CMAKE_MINIMUM_REQUIRED(VERSION 2.6.0) 10 | 11 | # find and setup Qt4 for this project 12 | FIND_PACKAGE(Qt4 REQUIRED) 13 | 14 | # tell cmake to process CMakeLists.txt in these subdirs 15 | SUBDIRS(src) 16 | 17 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}) 2 | 3 | INCLUDE(${QT_USE_FILE}) 4 | 5 | # .cxx sources 6 | SET(HCD_SRCS_CXX 7 | hcd.cpp 8 | main.cpp 9 | ) 10 | 11 | # files which need to be moc'd by Qt 12 | SET(HCD_MOC_SRCS 13 | 14 | ) 15 | 16 | # this moc's the above variable and appends to the cxx sources 17 | QT4_WRAP_CPP(HCD_SRCS_CXX ${HCD_MOC_SRCS}) 18 | 19 | IF(UNIX) 20 | ADD_EXECUTABLE(hcd ${HCD_SRCS_CXX}) 21 | ELSEIF(APPLE) 22 | ADD_EXECUTABLE(hcd MACOSX_BUNDLE ${HCD_SRCS_CXX}) 23 | ELSEIF(WIN32) 24 | ADD_EXECUTABLE(hcd WIN32 ${HCD_SRCS_CXX}) 25 | ENDIF() 26 | 27 | TARGET_LINK_LIBRARIES(hcd ${QT_LIBRARIES}) 28 | 29 | -------------------------------------------------------------------------------- /src/hcd.h: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** For Copyright & Licensing information, see COPYRIGHT in project root 4 | ** 5 | ****************************************************************************/ 6 | 7 | #include 8 | #include 9 | 10 | typedef QVector IntArray; 11 | typedef QVector Image; 12 | 13 | class HoughCircleDetector 14 | { 15 | public: /* class */ 16 | 17 | HoughCircleDetector() {} 18 | ~HoughCircleDetector() {} 19 | 20 | public: /* methods */ 21 | 22 | QImage detect(const QImage &source, unsigned int min_r, unsigned int max_r); 23 | 24 | private: /* methods */ 25 | 26 | void accum_circle(Image &image, const QPoint &position, unsigned int radius); 27 | void accum_pixel(Image &image, const QPoint &position); 28 | 29 | void draw_circle(QImage &image, const QPoint &position, unsigned int radius, const QColor &color); 30 | void draw_pixel(QImage &image, const QPoint &position, const QColor &color); 31 | 32 | QImage edges(const QImage &source); 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** For Copyright & Licensing information, see COPYRIGHT in project root 4 | ** 5 | ****************************************************************************/ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "hcd.h" 13 | 14 | /**************************************************************************** 15 | ** 16 | ** Author: Marc Bowes 17 | ** 18 | ** Arguments: --source=file [--output=file] 19 | ** 20 | ****************************************************************************/ 21 | int main(int argc, char **argv) 22 | { 23 | /*** Process command line inputs ***/ 24 | 25 | QString source, output; 26 | unsigned int min_r = 0, max_r = 0; 27 | 28 | /* .. I guess this could be done in a more general sense (with a QMap..) but 29 | I think the extra effort for so few arguments isn't worth it */ 30 | QRegExp rx_source("^--source=(.*)$"); 31 | QRegExp rx_output("^--output=(.*)$"); 32 | QRegExp rx_minr("^--minr=(.*)$"); 33 | QRegExp rx_maxr("^--maxr=(.*)$"); 34 | 35 | for(int i = 0; i < argc; i++) 36 | { 37 | QString arg = argv[i]; 38 | 39 | if(rx_source.indexIn(arg) != -1) 40 | { 41 | source = rx_source.cap(1); 42 | } 43 | else if(rx_output.indexIn(arg) != -1) 44 | { 45 | output = rx_output.cap(1); 46 | } 47 | else if(rx_minr.indexIn(arg) != -1) 48 | { 49 | min_r = rx_minr.cap(1).toInt(); 50 | } 51 | else if(rx_maxr.indexIn(arg) != -1) 52 | { 53 | max_r = rx_maxr.cap(1).toInt(); 54 | } 55 | } 56 | 57 | if(source.isEmpty()) 58 | { 59 | qDebug() << "Missing source file, please set with `--source=file`"; 60 | return 1; 61 | } 62 | 63 | QImage source_image(source); 64 | if (source_image.isNull()) 65 | { 66 | qDebug() << "Could not load source file"; 67 | qDebug() << "Supported image formats:"; 68 | qDebug() << QImageReader::supportedImageFormats(); 69 | return 1; 70 | } 71 | 72 | if(output.isEmpty()) 73 | { 74 | output = QString("%1.out.jpg").arg(source); 75 | } 76 | 77 | /*** Run the circle detection on the source file, and write the results out to 78 | the specified output file ***/ 79 | 80 | HoughCircleDetector hcd; 81 | QImage result = hcd.detect(source_image, min_r, max_r); 82 | result.save(output); 83 | } 84 | 85 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Practical handout 2 | 3 | ## Problem statement 4 | 5 | Write a simple Hough feature detector that can detect Circles of arbitrary size in 6 | the input images. 7 | 8 | The program must support the following functionality: 9 | 10 | ### Simple edge filter 11 | 12 | A grey-scale image will need to be processed by a simple edge filter (see notes) followed by thresholding, to determine a binary image which contains background and edge pixels only 13 | 14 | ### Highlighting the extracted feature(s) 15 | 16 | Once a feature has been detected, a circle corresponding to the feature must be plotted over the image to show the result. This image can be saved and then viewed. 17 | 18 | ### Multiple features 19 | 20 | The procedure to determine accumulator maxima will need to search for multiple local maxima; each of these will be a new feature (circle). 21 | 22 | Some sample test images with circles are available [here](http://www.cs.uct.ac.za/~patrick/hons/images) 23 | 24 | # My solution 25 | 26 | The solution is written in C++ using some Qt and cmake. Qt provides the convenience for loading and saving images, and cmake is there to generate build files. The solution works as follows: 27 | 28 | * Load source image 29 | * Run Sobel edge detection 30 | * Hough transform for each radius of size 1..n, where n is the maximum radius possible 31 | * Find the bright spots in the Hough-space images, and marks those off as circles in image space 32 | 33 | This practical is very condusive towards parallelism. However, that is outside of the scope of the practical, and I specifically didn't do it so that the solution is as clean as possible. 34 | 35 | Similarly, I have not included a GUI. If you wish to see the intermediate effects, it is possible to just return an image at any point in the detect method. For example, one could: 36 | `return houghs[15];` to see the 15th image in Hough-space or `return binary;` to see the result of the edge detector. 37 | 38 | ## Building the solution 39 | 40 | Change to a build director and run cmake from there. For example, in a Unix-like environment: 41 | 42 | * `cd /path/to/hough-circle-detector` 43 | * `cd build` 44 | * `cmake ..` or add additional options such as `cmake -DCMAKE_BUILD_TYPE=debug ..` 45 | * Use ccmake [ `ccmake ..` ] if you wish to change additional cmake options 46 | * `make` 47 | 48 | On Windows, the steps are equivalent - just set cmake to generate the correct files for your build system (Visual Studio, mingw..) 49 | 50 | ## Running the solution 51 | 52 | By default, cmake will compile the binary into the src/ directory in the build folder. The binary requires at least a source image. I recommend running as: 53 | `cd build; src/hcd --source=test.gif --output=test.out.jpg --minr=10 --maxr=40` 54 | These settings are appropriate for the sample images, as circles tend to be of radius 15 and 32. Other values will work too, but will incur additional processing time. 55 | 56 | The easiest way to run this program is by using a find script. For example, assuming you have the sample images in an `images/` directory, run: 57 | `find /path/to/images/*.gif -exec /patth/to/hcd --source={} \;` 58 | Assuming that the sample images are being used, you can add on the above mentioned min/max-r settings for good performance. 59 | 60 | Qt does not support writing of all image formats. However, by default it does read a large number. I have not explicitly set the output format, so it will try and guess it from the filename. Therefore, 61 | I recommend simply setting the output filename to anything ending in .jpg, as I know that this works. 62 | 63 | For a listing of supported file formats, visit [Reading and Writing Image Files](http://doc.trolltech.com/4.5/qimage.html#reading-and-writing-image-files). 64 | 65 | As a side note, it is possible to get additional file type support by adding plugins. Again, this is out of the scope of the practical, so I didn't bother including ones. 66 | -------------------------------------------------------------------------------- /src/hcd.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** For Copyright & Licensing information, see COPYRIGHT in project root 4 | ** 5 | ****************************************************************************/ 6 | 7 | #include "hcd.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) 15 | 16 | 17 | /**************************************************************************** 18 | __ ___ __ __ __ 19 | ___ __ __/ / / (_)___ __ _ ___ / /_/ / ___ ___/ /__ 20 | / _ \/ // / _ \/ / / __/ / ' \/ -_) __/ _ \/ _ \/ _ (_-< 21 | / .__/\_,_/_.__/_/_/\__/ /_/_/_/\__/\__/_//_/\___/\_,_/___/ 22 | /_/ 23 | 24 | ****************************************************************************/ 25 | 26 | 27 | /**************************************************************************** 28 | ** 29 | ** Author: Marc Bowes 30 | ** 31 | ** Detects circles in the specified QImage 32 | ** 33 | ****************************************************************************/ 34 | QImage HoughCircleDetector::detect(const QImage &source, unsigned int min_r, unsigned int max_r) 35 | { 36 | QImage binary = edges(source); 37 | QImage detection = source.convertToFormat(QImage::Format_RGB888); 38 | 39 | /* build a vector to hold images in Hough-space for radius 1..max_r, where 40 | max_r is specified or the maximum radius of a circle in this image */ 41 | if(min_r == 0) 42 | { 43 | min_r = 5; 44 | } 45 | 46 | if(max_r == 0) 47 | { 48 | max_r = MIN(source.width(), source.height()) / 2; 49 | } 50 | 51 | QVector houghs(max_r - min_r); 52 | 53 | for(unsigned int i = min_r; i < max_r; i++) 54 | { 55 | /* instantiate Hough-space for circles of radius i */ 56 | Image &hough = houghs[i - min_r]; 57 | hough.resize(binary.width()); 58 | for(unsigned int x = 0; x < hough.size(); x++) 59 | { 60 | hough[x].resize(binary.height()); 61 | for(unsigned int y = 0; y < hough[x].size(); y++) 62 | { 63 | hough[x][y] = 0; 64 | } 65 | } 66 | 67 | /* find all the edges */ 68 | for(unsigned int x = 0; x < binary.width(); x++) 69 | { 70 | for(unsigned int y = 0; y < binary.height(); y++) 71 | { 72 | /* edge! */ 73 | if(binary.pixelIndex(x, y) == 1) 74 | { 75 | accum_circle(hough, QPoint(x, y), i); 76 | } 77 | } 78 | } 79 | 80 | /* loop through all the Hough-space images, searching for bright spots, which 81 | indicate the center of a circle, then draw circles in image-space */ 82 | unsigned int threshold = 4.9 * i; 83 | for(unsigned int x = 0; x < hough.size(); x++) 84 | { 85 | for(unsigned int y = 0; y < hough[x].size(); y++) 86 | { 87 | if(hough[x][y] > threshold) 88 | { 89 | draw_circle(detection, QPoint(x, y), i, Qt::yellow); 90 | } 91 | } 92 | } 93 | } 94 | 95 | return detection; 96 | } 97 | 98 | 99 | /**************************************************************************** 100 | _ __ __ __ __ 101 | ___ ____(_) _____ _/ /____ __ _ ___ / /_/ / ___ ___/ /__ 102 | / _ \/ __/ / |/ / _ `/ __/ -_) / ' \/ -_) __/ _ \/ _ \/ _ (_-< 103 | / .__/_/ /_/|___/\_,_/\__/\__/ /_/_/_/\__/\__/_//_/\___/\_,_/___/ 104 | /_/ 105 | 106 | ****************************************************************************/ 107 | 108 | 109 | /**************************************************************************** 110 | ** 111 | ** Author: Marc Bowes 112 | ** 113 | ** Accumulates a circle on the specified image at the specified position with 114 | ** the specified radius, using the midpoint circle drawing algorithm 115 | ** 116 | ** Adapted from: http://en.wikipedia.org/wiki/Midpoint_circle_algorithm 117 | ** 118 | ****************************************************************************/ 119 | void HoughCircleDetector::accum_circle(Image &image, const QPoint &position, unsigned int radius) 120 | { 121 | int f = 1 - radius; 122 | int ddF_x = 1; 123 | int ddF_y = -2 * radius; 124 | int x = 0; 125 | int y = radius; 126 | 127 | accum_pixel(image, QPoint(position.x(), position.y() + radius)); 128 | accum_pixel(image, QPoint(position.x(), position.y() - radius)); 129 | accum_pixel(image, QPoint(position.x() + radius, position.y())); 130 | accum_pixel(image, QPoint(position.x() - radius, position.y())); 131 | 132 | while(x < y) 133 | { 134 | if(f >= 0) 135 | { 136 | y--; 137 | ddF_y += 2; 138 | f += ddF_y; 139 | } 140 | 141 | x++; 142 | ddF_x += 2; 143 | f += ddF_x; 144 | 145 | accum_pixel(image, QPoint(position.x() + x, position.y() + y)); 146 | accum_pixel(image, QPoint(position.x() - x, position.y() + y)); 147 | accum_pixel(image, QPoint(position.x() + x, position.y() - y)); 148 | accum_pixel(image, QPoint(position.x() - x, position.y() - y)); 149 | accum_pixel(image, QPoint(position.x() + y, position.y() + x)); 150 | accum_pixel(image, QPoint(position.x() - y, position.y() + x)); 151 | accum_pixel(image, QPoint(position.x() + y, position.y() - x)); 152 | accum_pixel(image, QPoint(position.x() - y, position.y() - x)); 153 | } 154 | } 155 | 156 | /**************************************************************************** 157 | ** 158 | ** Author: Marc Bowes 159 | ** 160 | ** Accumulates at the specified position 161 | ** 162 | ****************************************************************************/ 163 | void HoughCircleDetector::accum_pixel(Image &image, const QPoint &position) 164 | { 165 | /* bounds checking */ 166 | if(position.x() < 0 || position.x() >= image.size() || 167 | position.y() < 0 || position.y() >= image[position.x()].size()) 168 | { 169 | return; 170 | } 171 | 172 | image[position.x()][position.y()]++; 173 | } 174 | 175 | /**************************************************************************** 176 | ** 177 | ** Author: Marc Bowes 178 | ** 179 | ** Draws a circle on the specified image at the specified position with 180 | ** the specified radius, using the midpoint circle drawing algorithm 181 | ** 182 | ** Adapted from: http://en.wikipedia.org/wiki/Midpoint_circle_algorithm 183 | ** 184 | ****************************************************************************/ 185 | void HoughCircleDetector::draw_circle(QImage &image, const QPoint &position, unsigned int radius, const QColor &color) 186 | { 187 | int f = 1 - radius; 188 | int ddF_x = 1; 189 | int ddF_y = -2 * radius; 190 | int x = 0; 191 | int y = radius; 192 | 193 | draw_pixel(image, QPoint(position.x(), position.y() + radius), color); 194 | draw_pixel(image, QPoint(position.x(), position.y() - radius), color); 195 | draw_pixel(image, QPoint(position.x() + radius, position.y()), color); 196 | draw_pixel(image, QPoint(position.x() - radius, position.y()), color); 197 | 198 | while(x < y) 199 | { 200 | if(f >= 0) 201 | { 202 | y--; 203 | ddF_y += 2; 204 | f += ddF_y; 205 | } 206 | 207 | x++; 208 | ddF_x += 2; 209 | f += ddF_x; 210 | 211 | draw_pixel(image, QPoint(position.x() + x, position.y() + y), color); 212 | draw_pixel(image, QPoint(position.x() - x, position.y() + y), color); 213 | draw_pixel(image, QPoint(position.x() + x, position.y() - y), color); 214 | draw_pixel(image, QPoint(position.x() - x, position.y() - y), color); 215 | draw_pixel(image, QPoint(position.x() + y, position.y() + x), color); 216 | draw_pixel(image, QPoint(position.x() - y, position.y() + x), color); 217 | draw_pixel(image, QPoint(position.x() + y, position.y() - x), color); 218 | draw_pixel(image, QPoint(position.x() - y, position.y() - x), color); 219 | } 220 | } 221 | 222 | /**************************************************************************** 223 | ** 224 | ** Author: Marc Bowes 225 | ** 226 | ** Draws at the specified position 227 | ** 228 | ****************************************************************************/ 229 | void HoughCircleDetector::draw_pixel(QImage &image, const QPoint &position, const QColor &color) 230 | { 231 | /* bounds checking */ 232 | if(position.x() < 0 || position.x() >= image.width() || 233 | position.y() < 0 || position.y() >= image.height()) 234 | { 235 | return; 236 | } 237 | 238 | image.setPixel(position, color.rgb()); 239 | } 240 | 241 | /**************************************************************************** 242 | ** 243 | ** Author: Marc Bowes 244 | ** 245 | ** Detects edges in the specified QImage 246 | ** 247 | ****************************************************************************/ 248 | QImage HoughCircleDetector::edges(const QImage &source) 249 | { 250 | /* initialisation */ 251 | QImage binary = QImage(source.size(), QImage::Format_Mono); 252 | 253 | /*** Sobel edge detection ***/ 254 | 255 | /* Set up Lx, Ly */ 256 | QVector Lx(3), Ly(3); 257 | 258 | Lx[0][0] = -1; Lx[0][1] = +0; Lx[0][2] = +1; 259 | Lx[1][0] = -2; Lx[1][1] = +0; Lx[1][2] = +2; 260 | Lx[2][0] = -1; Lx[2][1] = +0; Lx[2][2] = +1; 261 | 262 | Ly[0][0] = +1; Ly[0][1] = +2; Ly[0][2] = +1; 263 | Ly[1][0] = +0; Ly[1][1] = +0; Ly[1][2] = +0; 264 | Ly[2][0] = -1; Ly[2][1] = -2; Ly[2][2] = -1; 265 | 266 | for(unsigned int x = 0; x < source.width(); x++) 267 | { 268 | for(unsigned int y = 0; y < source.height(); y++) 269 | { 270 | double new_x = 0.0, new_y = 0.0; 271 | 272 | /* gradient */ 273 | for(int i = -1; i <= 1; i++) 274 | { 275 | for(int j = -1; j <= 1; j++) 276 | { 277 | /* these are offset co-ords */ 278 | int _x = x + i; 279 | int _y = y + j; 280 | 281 | /* bounds checking */ 282 | if (_x < 0) _x = -_x; 283 | else if (_x >= source.width()) _x = 2 * source.width() - _x - 2; 284 | 285 | if (_y < 0) _y = -_y; 286 | else if (_y >= source.height()) _y = 2 * source.height() - _y - 2; 287 | 288 | /* accumulate */ 289 | int gray = qGray(source.pixel(_x, _y)); 290 | new_x += static_cast(Lx[i + 1][j + 1]) * gray; 291 | new_y += static_cast(Ly[i + 1][j + 1]) * gray; 292 | } 293 | } 294 | 295 | /* using 128 as a threshold, decide if the steepness is sufficient (= edge = 1) */ 296 | int pixel = sqrt(pow(new_x, 2) + pow(new_y, 2)) > 128 ? 1 : 0; 297 | binary.setPixel(x, y, pixel); 298 | } 299 | } 300 | 301 | return binary; 302 | } 303 | 304 | --------------------------------------------------------------------------------