Austin Schuh | 8c794d5 | 2019-03-03 21:17:37 -0800 | [diff] [blame^] | 1 | /* |
| 2 | # |
| 3 | # File : radon_transform2d.cpp |
| 4 | # ( C++ source file ) |
| 5 | # |
| 6 | # Description : An implementation of the Radon Transform. |
| 7 | # This file is a part of the CImg Library project. |
| 8 | # ( http://cimg.eu ) |
| 9 | # |
| 10 | # Copyright : David G. Starkweather |
| 11 | # ( starkdg@sourceforge.net - starkweatherd@cox.net ) |
| 12 | # |
| 13 | # License : CeCILL v2.0 |
| 14 | # ( http://www.cecill.info/licences/Licence_CeCILL_V2-en.html ) |
| 15 | # |
| 16 | # This software is governed by the CeCILL license under French law and |
| 17 | # abiding by the rules of distribution of free software. You can use, |
| 18 | # modify and/ or redistribute the software under the terms of the CeCILL |
| 19 | # license as circulated by CEA, CNRS and INRIA at the following URL |
| 20 | # "http://www.cecill.info". |
| 21 | # |
| 22 | # As a counterpart to the access to the source code and rights to copy, |
| 23 | # modify and redistribute granted by the license, users are provided only |
| 24 | # with a limited warranty and the software's author, the holder of the |
| 25 | # economic rights, and the successive licensors have only limited |
| 26 | # liability. |
| 27 | # |
| 28 | # In this respect, the user's attention is drawn to the risks associated |
| 29 | # with loading, using, modifying and/or developing or reproducing the |
| 30 | # software by the user in light of its specific status of free software, |
| 31 | # that may mean that it is complicated to manipulate, and that also |
| 32 | # therefore means that it is reserved for developers and experienced |
| 33 | # professionals having in-depth computer knowledge. Users are therefore |
| 34 | # encouraged to load and test the software's suitability as regards their |
| 35 | # requirements in conditions enabling the security of their systems and/or |
| 36 | # data to be ensured and, more generally, to use and operate it in the |
| 37 | # same conditions as regards security. |
| 38 | # |
| 39 | # The fact that you are presently reading this means that you have had |
| 40 | # knowledge of the CeCILL license and that you accept its terms. |
| 41 | # |
| 42 | */ |
| 43 | |
| 44 | #include "CImg.h" |
| 45 | using namespace cimg_library; |
| 46 | #ifndef cimg_imagepath |
| 47 | #define cimg_imagepath "img/" |
| 48 | #endif |
| 49 | |
| 50 | #define ROUNDING_FACTOR(x) (((x) >= 0) ? 0.5 : -0.5) |
| 51 | |
| 52 | CImg<double> GaussianKernel(double rho); |
| 53 | CImg<float> ApplyGaussian(CImg<unsigned char> im,double rho); |
| 54 | CImg<unsigned char> RGBtoGrayScale(CImg<unsigned char> &im); |
| 55 | int GetAngle(int dy,int dx); |
| 56 | CImg<unsigned char> CannyEdges(CImg<float> im, double T1, double T2,bool doHysteresis); |
| 57 | CImg<> RadonTransform(CImg<unsigned char> im,int N); |
| 58 | |
| 59 | // Main procedure |
| 60 | //---------------- |
| 61 | int main(int argc,char **argv) { |
| 62 | cimg_usage("Illustration of the Radon Transform"); |
| 63 | |
| 64 | const char *file = cimg_option("-f",cimg_imagepath "parrot.ppm","path and file name"); |
| 65 | const double sigma = cimg_option("-r",1.0,"blur coefficient for gaussian low pass filter (lpf)"), |
| 66 | thresh1 = cimg_option("-t1",0.50,"lower threshold for canny edge detector"), |
| 67 | thresh2 = cimg_option("-t2",1.25,"upper threshold for canny edge detector");; |
| 68 | const int N = cimg_option("-n",64,"number of angles to consider in the Radon transform - should be a power of 2"); |
| 69 | |
| 70 | //color to draw lines |
| 71 | const unsigned char green[] = {0,255,0}; |
| 72 | CImg<unsigned char> src(file); |
| 73 | |
| 74 | int rhomax = (int)std::sqrt((double)(src.width()*src.width() + src.height()*src.height()))/2; |
| 75 | |
| 76 | if (cimg::dialog(cimg::basename(argv[0]), |
| 77 | "Instructions:\n" |
| 78 | "Click on space bar or Enter key to display Radon transform of given image\n" |
| 79 | "Click on anywhere in the transform window to display a \n" |
| 80 | "corresponding green line in the original image\n", |
| 81 | "Start", "Quit",0,0,0,0, |
| 82 | src.get_resize(100,100,1,3),true)) std::exit(0); |
| 83 | |
| 84 | //retrieve a grayscale from the image |
| 85 | CImg<unsigned char> grayScaleIm; |
| 86 | if ((src.spectrum() == 3) && (src.width() > 0) && (src.height() > 0) && (src.depth() == 1)) |
| 87 | grayScaleIm = (CImg<unsigned char>)src.get_norm(0).quantize(255,false); |
| 88 | else if ((src.spectrum() == 1)&&(src.width() > 0) && (src.height() > 0) && (src.depth() == 1)) |
| 89 | grayScaleIm = src; |
| 90 | else { // image in wrong format |
| 91 | if (cimg::dialog(cimg::basename("wrong file format"), |
| 92 | "Incorrect file format\n","OK",0,0,0,0,0, |
| 93 | src.get_resize(100,100,1,3),true)) std::exit(0); |
| 94 | } |
| 95 | |
| 96 | //blur the image with a Gaussian lpf to remove spurious edges (e.g. noise) |
| 97 | CImg<float> blurredIm = ApplyGaussian(grayScaleIm,sigma); |
| 98 | |
| 99 | //use canny edge detection algorithm to get edge map of the image |
| 100 | //- the threshold values are used to perform hysteresis in the edge detection process |
| 101 | CImg<unsigned char> cannyEdgeMap = CannyEdges(blurredIm,thresh1,thresh2,false); |
| 102 | CImg<unsigned char> radonImage = *(new CImg<unsigned char>(500,400,1,1,0)); |
| 103 | |
| 104 | //display the two windows |
| 105 | CImgDisplay dispImage(src,"original image"); |
| 106 | dispImage.move(CImgDisplay::screen_width()/8,CImgDisplay::screen_height()/8); |
| 107 | CImgDisplay dispRadon(radonImage,"Radon Transform"); |
| 108 | dispRadon.move(CImgDisplay::screen_width()/4,CImgDisplay::screen_height()/4); |
| 109 | CImgDisplay dispCanny(cannyEdgeMap,"canny edges"); |
| 110 | //start main display loop |
| 111 | while (!dispImage.is_closed() && !dispRadon.is_closed() && |
| 112 | !dispImage.is_keyQ() && !dispRadon.is_keyQ() && |
| 113 | !dispImage.is_keyESC() && !dispRadon.is_keyESC()) { |
| 114 | |
| 115 | CImgDisplay::wait(dispImage,dispRadon); |
| 116 | |
| 117 | if (dispImage.is_keySPACE() || dispRadon.is_keySPACE()) { |
| 118 | radonImage = (CImg<unsigned char>)RadonTransform(cannyEdgeMap,N).quantize(255,false).resize(500,400); |
| 119 | radonImage.display(dispRadon); |
| 120 | } |
| 121 | |
| 122 | //when clicking on dispRadon window, draw line in original image window |
| 123 | if (dispRadon.button()) |
| 124 | { |
| 125 | const double rho = dispRadon.mouse_y()*rhomax/dispRadon.height(), |
| 126 | theta = (dispRadon.mouse_x()*N/dispRadon.width())*2*cimg::PI/N, |
| 127 | x = src.width()/2 + rho*std::cos(theta), |
| 128 | y = src.height()/2 + rho*std::sin(theta); |
| 129 | const int x0 = (int)(x + 1000*std::cos(theta + cimg::PI/2)), |
| 130 | y0 = (int)(y + 1000*std::sin(theta + cimg::PI/2)), |
| 131 | x1 = (int)(x - 1000*std::cos(theta + cimg::PI/2)), |
| 132 | y1 = (int)(y - 1000*std::sin(theta + cimg::PI/2)); |
| 133 | src.draw_line(x0,y0,x1,y1,green,1.0f,0xF0F0F0F0).display(dispImage); |
| 134 | } |
| 135 | } |
| 136 | return 0; |
| 137 | } |
| 138 | /** |
| 139 | * PURPOSE: create a 5x5 gaussian kernel matrix |
| 140 | * PARAM rho - gaussiam equation parameter (default = 1.0) |
| 141 | * RETURN CImg<double> the gaussian kernel |
| 142 | **/ |
| 143 | |
| 144 | CImg<double> GaussianKernel(double sigma = 1.0) |
| 145 | { |
| 146 | CImg<double> resultIm(5,5,1,1,0); |
| 147 | int midX = 3, midY = 3; |
| 148 | cimg_forXY(resultIm,X,Y) { |
| 149 | resultIm(X,Y) = std::ceil(256.0*(std::exp(-(midX*midX + midY*midY)/(2*sigma*sigma)))/(2*cimg::PI*sigma*sigma)); |
| 150 | } |
| 151 | return resultIm; |
| 152 | } |
| 153 | /* |
| 154 | * PURPOSE: convolve a given image with the gaussian kernel |
| 155 | * PARAM CImg<unsigned char> im - image to be convolved upon |
| 156 | * PARAM double sigma - gaussian equation parameter |
| 157 | * RETURN CImg<float> image resulting from the convolution |
| 158 | * */ |
| 159 | CImg<float> ApplyGaussian(CImg<unsigned char> im,double sigma) |
| 160 | { |
| 161 | CImg<float> smoothIm(im.width(),im.height(),1,1,0); |
| 162 | |
| 163 | //make gaussian kernel |
| 164 | CImg<float> gk = GaussianKernel(sigma); |
| 165 | //apply gaussian |
| 166 | |
| 167 | CImg_5x5(I,int); |
| 168 | cimg_for5x5(im,X,Y,0,0,I,int) { |
| 169 | float sum = 0; |
| 170 | sum += gk(0,0)*Ibb + gk(0,1)*Ibp + gk(0,2)*Ibc + gk(0,3)*Ibn + gk(0,4)*Iba; |
| 171 | sum += gk(1,0)*Ipb + gk(1,1)*Ipp + gk(1,2)*Ipc + gk(1,3)*Ipn + gk(1,4)*Ipa; |
| 172 | sum += gk(2,0)*Icb + gk(2,1)*Icp + gk(2,2)*Icc + gk(2,3)*Icn + gk(2,4)*Ica; |
| 173 | sum += gk(3,0)*Inb + gk(3,1)*Inp + gk(3,2)*Inc + gk(3,3)*Inn + gk(3,4)*Ina; |
| 174 | sum += gk(4,0)*Iab + gk(4,1)*Iap + gk(4,2)*Iac + gk(4,3)*Ian + gk(4,4)*Iaa; |
| 175 | smoothIm(X,Y) = sum/256; |
| 176 | } |
| 177 | return smoothIm; |
| 178 | } |
| 179 | /** |
| 180 | * PURPOSE: convert a given rgb image to a MxNX1 single vector grayscale image |
| 181 | * PARAM: CImg<unsigned char> im - rgb image to convert |
| 182 | * RETURN: CImg<unsigned char> grayscale image with MxNx1x1 dimensions |
| 183 | **/ |
| 184 | |
| 185 | CImg<unsigned char> RGBtoGrayScale(CImg<unsigned char> &im) |
| 186 | { |
| 187 | CImg<unsigned char> grayImage(im.width(),im.height(),im.depth(),1,0); |
| 188 | if (im.spectrum() == 3) { |
| 189 | cimg_forXYZ(im,X,Y,Z) { |
| 190 | grayImage(X,Y,Z,0) = (unsigned char)(0.299*im(X,Y,Z,0) + 0.587*im(X,Y,Z,1) + 0.114*im(X,Y,Z,2)); |
| 191 | } |
| 192 | } |
| 193 | grayImage.quantize(255,false); |
| 194 | return grayImage; |
| 195 | } |
| 196 | /** |
| 197 | * PURPOSE: aux. function used by CannyEdges to quantize an angle theta given by gradients, dx and dy |
| 198 | * into 0 - 7 |
| 199 | * PARAM: dx,dy - gradient magnitudes |
| 200 | * RETURN int value between 0 and 7 |
| 201 | **/ |
| 202 | int GetAngle(int dy,int dx) |
| 203 | { |
| 204 | double angle = cimg::abs(std::atan2((double)dy,(double)dx)); |
| 205 | if ((angle >= -cimg::PI/8)&&(angle <= cimg::PI/8))//-pi/8 to pi/8 => 0 |
| 206 | return 0; |
| 207 | else if ((angle >= cimg::PI/8)&&(angle <= 3*cimg::PI/8))//pi/8 to 3pi/8 => pi/4 |
| 208 | return 1; |
| 209 | else if ((angle > 3*cimg::PI/8)&&(angle <= 5*cimg::PI/8))//3pi/8 to 5pi/8 => pi/2 |
| 210 | return 2; |
| 211 | else if ((angle > 5*cimg::PI/8)&&(angle <= 7*cimg::PI/8))//5pi/8 to 7pi/8 => 3pi/4 |
| 212 | return 3; |
| 213 | else if (((angle > 7*cimg::PI/8) && (angle <= cimg::PI)) || |
| 214 | ((angle <= -7*cimg::PI/8)&&(angle >= -cimg::PI))) //-7pi/8 to -pi OR 7pi/8 to pi => pi |
| 215 | return 4; |
| 216 | else return 0; |
| 217 | } |
| 218 | /** |
| 219 | * PURPOSE: create an edge map of the given image with hysteresis using thresholds T1 and T2 |
| 220 | * PARAMS: CImg<float> im the image to perform edge detection on |
| 221 | * T1 lower threshold |
| 222 | * T2 upper threshold |
| 223 | * RETURN CImg<unsigned char> edge map |
| 224 | **/ |
| 225 | CImg<unsigned char> CannyEdges(CImg<float> im, double T1, double T2, bool doHysteresis=false) |
| 226 | { |
| 227 | CImg<unsigned char> edges(im); |
| 228 | CImg<float> secDerivs(im); |
| 229 | secDerivs.fill(0); |
| 230 | edges.fill(0); |
| 231 | CImgList<float> gradients = im.get_gradient("xy",1); |
| 232 | int image_width = im.width(); |
| 233 | int image_height = im.height(); |
| 234 | |
| 235 | cimg_forXY(im,X,Y) { |
| 236 | double Gr = std::sqrt(std::pow((double)gradients[0](X,Y),2.0) + std::pow((double)gradients[1](X,Y),2.0)); |
| 237 | double theta = GetAngle(Y,X); |
| 238 | //if Gradient magnitude is positive and X,Y within the image |
| 239 | //take the 2nd deriv in the appropriate direction |
| 240 | if ((Gr > 0)&&(X < image_width - 2)&&(Y < image_height - 2)) { |
| 241 | if (theta == 0) |
| 242 | secDerivs(X,Y) = im(X + 2,Y) - 2*im(X + 1,Y) + im(X,Y); |
| 243 | else if (theta == 1) |
| 244 | secDerivs(X,Y) = im(X + 2,Y + 2) - 2*im(X + 1,Y + 1) + im(X,Y); |
| 245 | else if (theta == 2) |
| 246 | secDerivs(X,Y) = im(X,Y + 2) - 2*im(X,Y + 1) + im(X,Y); |
| 247 | else if (theta == 3) |
| 248 | secDerivs(X,Y) = im(X + 2,Y + 2) - 2*im(X + 1,Y + 1) + im(X,Y); |
| 249 | else if (theta == 4) |
| 250 | secDerivs(X,Y) = im(X + 2,Y) - 2*im(X + 1,Y) + im(X,Y); |
| 251 | } |
| 252 | } |
| 253 | //for each 2nd deriv that crosses a zero point and magnitude passes the upper threshold. |
| 254 | //Perform hysteresis in the direction of the gradient, rechecking the gradient |
| 255 | //angle for each pixel that meets the threshold requirement. Stop checking when |
| 256 | //the lower threshold is not reached. |
| 257 | CImg_5x5(I,float); |
| 258 | cimg_for5x5(secDerivs,X,Y,0,0,I,float) { |
| 259 | if ( (Ipp*Ibb < 0) || |
| 260 | (Ipc*Ibc < 0)|| |
| 261 | (Icp*Icb < 0) ) { |
| 262 | double Gr = std::sqrt(std::pow((double)gradients[0](X,Y),2.0) + std::pow((double)gradients[1](X,Y),2.0)); |
| 263 | int dir = GetAngle(Y,X); |
| 264 | int Xt = X, Yt = Y, delta_x = 0, delta_y=0; |
| 265 | double GRt = Gr; |
| 266 | if (Gr >= T2) |
| 267 | edges(X,Y) = 255; |
| 268 | //work along the gradient in one direction |
| 269 | if (doHysteresis) { |
| 270 | while ((Xt > 0) && (Xt < image_width - 1) && (Yt > 0) && (Yt < image_height - 1)) { |
| 271 | switch (dir){ |
| 272 | case 0 : delta_x=0;delta_y=1;break; |
| 273 | case 1 : delta_x=1;delta_y=1;break; |
| 274 | case 2 : delta_x=1;delta_y=0;break; |
| 275 | case 3 : delta_x=1;delta_y=-1;break; |
| 276 | case 4 : delta_x=0;delta_y=1;break; |
| 277 | } |
| 278 | Xt += delta_x; |
| 279 | Yt += delta_y; |
| 280 | GRt = std::sqrt(std::pow((double)gradients[0](Xt,Yt),2.0) + std::pow((double)gradients[1](Xt,Yt),2.0)); |
| 281 | dir = GetAngle(Yt,Xt); |
| 282 | if (GRt >= T1) |
| 283 | edges(Xt,Yt) = 255; |
| 284 | } |
| 285 | //work along gradient in other direction |
| 286 | Xt = X; Yt = Y; |
| 287 | while ((Xt > 0) && (Xt < image_width - 1) && (Yt > 0) && (Yt < image_height - 1)) { |
| 288 | switch (dir){ |
| 289 | case 0 : delta_x=0;delta_y=1;break; |
| 290 | case 1 : delta_x=1;delta_y=1;break; |
| 291 | case 2 : delta_x=1;delta_y=0;break; |
| 292 | case 3 : delta_x=1;delta_y=-1;break; |
| 293 | case 4 : delta_x=0;delta_y=1;break; |
| 294 | } |
| 295 | Xt -= delta_x; |
| 296 | Yt -= delta_y; |
| 297 | GRt = std::sqrt(std::pow((double)gradients[0](Xt,Yt),2.0) + std::pow((double)gradients[1](Xt,Yt),2.0)); |
| 298 | dir = GetAngle(Yt,Xt); |
| 299 | if (GRt >= T1) |
| 300 | edges(Xt,Yt) = 255; |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | return edges; |
| 306 | } |
| 307 | /** |
| 308 | * PURPOSE: perform radon transform of given image |
| 309 | * PARAM: CImg<unsigned char> im - image to detect lines |
| 310 | * int N - number of angles to consider (should be a power of 2) |
| 311 | * (the values of N will be spread over 0 to 2PI) |
| 312 | * RETURN CImg<unsigned char> - transform of given image of size, N x D |
| 313 | * D = rhomax = sqrt(dimx*dimx + dimy*dimy)/2 |
| 314 | **/ |
| 315 | CImg<> RadonTransform(CImg<unsigned char> im,int N) { |
| 316 | int image_width = im.width(); |
| 317 | int image_height = im.height(); |
| 318 | |
| 319 | //calc offsets to center the image |
| 320 | float xofftemp = image_width/2.0f - 1; |
| 321 | float yofftemp = image_height/2.0f - 1; |
| 322 | int xoffset = (int)std::floor(xofftemp + ROUNDING_FACTOR(xofftemp)); |
| 323 | int yoffset = (int)std::floor(yofftemp + ROUNDING_FACTOR(yofftemp)); |
| 324 | float dtemp = (float)std::sqrt((double)(xoffset*xoffset + yoffset*yoffset)); |
| 325 | int D = (int)std::floor(dtemp + ROUNDING_FACTOR(dtemp)); |
| 326 | |
| 327 | CImg<> imRadon(N,D,1,1,0); |
| 328 | |
| 329 | //for each angle k to consider |
| 330 | for (int k= 0 ; k < N; k++) { |
| 331 | //only consider from PI/8 to 3PI/8 and 5PI/8 to 7PI/8 |
| 332 | //to avoid computational complexity of a steep angle |
| 333 | if (k == 0){k = N/8;continue;} |
| 334 | else if (k == (3*N/8 + 1)){ k = 5*N/8;continue;} |
| 335 | else if (k == 7*N/8 + 1){k = N; continue;} |
| 336 | |
| 337 | //for each rho length, determine linear equation and sum the line |
| 338 | //sum is to sum the values along the line at angle k2pi/N |
| 339 | //sum2 is to sum the values along the line at angle k2pi/N + N/4 |
| 340 | //The sum2 is performed merely by swapping the x,y axis as if the image were rotated 90 degrees. |
| 341 | for (int d=0; d < D; d++) { |
| 342 | double theta = 2*k*cimg::PI/N;//calculate actual theta |
| 343 | double alpha = std::tan(theta + cimg::PI/2);//calculate the slope |
| 344 | double beta_temp = -alpha*d*std::cos(theta) + d*std::sin(theta);//y-axis intercept for the line |
| 345 | int beta = (int)std::floor(beta_temp + ROUNDING_FACTOR(beta_temp)); |
| 346 | //for each value of m along x-axis, calculate y |
| 347 | //if the x,y location is within the boundary for the respective image orientations, add to the sum |
| 348 | unsigned int sum1 = 0, |
| 349 | sum2 = 0; |
| 350 | int M = (image_width >= image_height) ? image_width : image_height; |
| 351 | for (int m=0;m < M; m++) { |
| 352 | //interpolate in-between values using nearest-neighbor approximation |
| 353 | //using m,n as x,y indices into image |
| 354 | double n_temp = alpha*(m - xoffset) + beta; |
| 355 | int n = (int)std::floor(n_temp + ROUNDING_FACTOR(n_temp)); |
| 356 | if ((m < image_width) && (n + yoffset >= 0) && (n + yoffset < image_height)) |
| 357 | { |
| 358 | sum1 += im(m, n + yoffset); |
| 359 | } |
| 360 | n_temp = alpha*(m - yoffset) + beta; |
| 361 | n = (int)std::floor(n_temp + ROUNDING_FACTOR(n_temp)); |
| 362 | if ((m < image_height)&&(n + xoffset >= 0)&&(n + xoffset < image_width)) |
| 363 | { |
| 364 | sum2 += im(-(n + xoffset) + image_width - 1, m); |
| 365 | } |
| 366 | } |
| 367 | //assign the sums into the result matrix |
| 368 | imRadon(k,d) = (float)sum1; |
| 369 | //assign sum2 to angle position for theta + PI/4 |
| 370 | imRadon(((k + N/4)%N),d) = (float)sum2; |
| 371 | } |
| 372 | } |
| 373 | return imRadon; |
| 374 | } |
| 375 | /* references: |
| 376 | * 1. See Peter Toft's thesis on the Radon transform: http://petertoft.dk/PhD/index.html |
| 377 | * While I changed his basic algorithm, the main idea is still the same and provides an excellent explanation. |
| 378 | * |
| 379 | * */ |