Wed, 05 Aug 2009 15:02:31 +0100
PTdecode: add support for uncompressed data (NOTE: *NOT* supported by the PT-2450DX)
1 /*
2 #
3 # File : radon_transform.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.sourceforge.net )
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 */
44 #define ROUNDING_FACTOR(x) (((x) >= 0) ? 0.5 : -0.5)
45 #include <cmath>
46 #include "CImg.h"
47 using namespace cimg_library;
49 // The lines below are necessary when using a non-standard compiler as visualcpp6.
50 #ifdef cimg_use_visualcpp6
51 #define std
52 #endif
53 #ifdef min
54 #undef min
55 #undef max
56 #endif
58 #ifndef cimg_imagepath
59 #define cimg_imagepath "img/"
60 #endif
62 CImg<double> GaussianKernel(double rho);
63 CImg<float> ApplyGaussian(CImg<unsigned char> im,double rho);
64 CImg<unsigned char> RGBtoGrayScale(CImg<unsigned char> &im);
65 int GetAngle(int dy,int dx);
66 CImg<unsigned char> CannyEdges(CImg<float> im, double T1, double T2,bool doHysteresis);
67 CImg<> RadonTransform(CImg<unsigned char> im,int N);
69 int main(int argc,char **argv) {
70 cimg_usage("Illustration of the Radon Transform");
72 const char *file = cimg_option("-f",cimg_imagepath "parrot_original.ppm","path and file name");
73 const double sigma = cimg_option("-r",1.0,"blur coefficient for gaussian low pass filter (lpf)"),
74 thresh1 = cimg_option("-t1",0.50,"lower threshold for canny edge detector"),
75 thresh2 = cimg_option("-t2",1.25,"upper threshold for canny edge detector");;
76 const int N = cimg_option("-n",64,"number of angles to consider in the Radon transform - should be a power of 2");
78 //color to draw lines
79 const unsigned char green[] = {0,255,0};
80 CImg<unsigned char> src(file);
82 int rhomax = (int)std::sqrt((double)(src.dimx()*src.dimx() + src.dimy()*src.dimy()))/2;
84 if (cimg::dialog(cimg::basename(argv[0]),
85 "Instructions:\n"
86 "Click on space bar or Enter key to display Radon transform of given image\n"
87 "Click on anywhere in the transform window to display a \n"
88 "corresponding green line in the original image\n",
89 "Start", "Quit",0,0,0,0,
90 src.get_resize(100,100,1,3),true)) std::exit(0);
92 //retrieve a grayscale from the image
93 CImg<unsigned char> grayScaleIm;
94 if ((src.dimv() == 3) && (src.dimx() > 0) && (src.dimy() > 0) && (src.dimz() == 1))
95 grayScaleIm = (CImg<unsigned char>)src.get_pointwise_norm(0).quantize(255,false);
96 else if ((src.dimv() == 1)&&(src.dimx() > 0) && (src.dimy() > 0) && (src.dimz() == 1))
97 grayScaleIm = src;
98 else { // image in wrong format
99 if (cimg::dialog(cimg::basename("wrong file format"),
100 "Incorrect file format\n","OK",0,0,0,0,0,
101 src.get_resize(100,100,1,3),true)) std::exit(0);
102 }
104 //blur the image with a Gaussian lpf to remove spurious edges (e.g. noise)
105 CImg<float> blurredIm = ApplyGaussian(grayScaleIm,sigma);
107 //use canny edge detection algorithm to get edge map of the image
108 //- the threshold values are used to perform hysteresis in the edge detection process
109 CImg<unsigned char> cannyEdgeMap = CannyEdges(blurredIm,thresh1,thresh2,false);
110 CImg<unsigned char> radonImage = *(new CImg<unsigned char>(500,400,1,1,0));
112 //display the two windows
113 CImgDisplay dispImage(src,"original image");
114 dispImage.move(CImgDisplay::screen_dimx()/8,CImgDisplay::screen_dimy()/8);
115 CImgDisplay dispRadon(radonImage,"Radon Transform");
116 dispRadon.move(CImgDisplay::screen_dimx()/4,CImgDisplay::screen_dimy()/4);
117 CImgDisplay dispCanny(cannyEdgeMap,"canny edges");
118 //start main display loop
119 while (!dispImage.is_closed && !dispRadon.is_closed &&
120 !dispImage.is_keyQ && !dispRadon.is_keyQ &&
121 !dispImage.is_keyESC && !dispRadon.is_keyESC){
123 CImgDisplay::wait(dispImage,dispRadon);
125 if (dispImage.is_keySPACE || dispRadon.is_keySPACE)
126 {
127 radonImage = (CImg<unsigned char>)RadonTransform(cannyEdgeMap,N).quantize(255,false).resize(500,400);
128 radonImage.display(dispRadon);
129 }
131 //when clicking on dispRadon window, draw line in original image window
132 if (dispRadon.button)
133 {
134 const double rho = dispRadon.mouse_y*rhomax/dispRadon.dimy(),
135 theta = (dispRadon.mouse_x*N/dispRadon.dimx())*2*cimg::valuePI/N,
136 x = src.dimx()/2 + rho*std::cos(theta),
137 y = src.dimy()/2 + rho*std::sin(theta);
138 const int x0 = (int)(x + 1000*std::cos(theta + cimg::valuePI/2)),
139 y0 = (int)(y + 1000*std::sin(theta + cimg::valuePI/2)),
140 x1 = (int)(x - 1000*std::cos(theta + cimg::valuePI/2)),
141 y1 = (int)(y - 1000*std::sin(theta + cimg::valuePI/2));
142 src.draw_line(x0,y0,x1,y1,green,1.0f,0xF0F0F0F0).display(dispImage);
143 }
144 }
145 return 0;
146 }
147 /**
148 * PURPOSE: create a 5x5 gaussian kernel matrix
149 * PARAM rho - gaussiam equation parameter (default = 1.0)
150 * RETURN CImg<double> the gaussian kernel
151 **/
153 CImg<double> GaussianKernel(double sigma = 1.0)
154 {
155 CImg<double> resultIm(5,5,1,1,0);
156 int midX = 3, midY = 3;
157 cimg_forXY(resultIm,X,Y)
158 {
159 resultIm(X,Y) = std::ceil(256.0*(std::exp(-(midX*midX + midY*midY)/(2*sigma*sigma)))/(2*cimg::valuePI*sigma*sigma));
160 }
161 return resultIm;
162 }
163 /*
164 * PURPOSE: convolve a given image with the gaussian kernel
165 * PARAM CImg<unsigned char> im - image to be convolved upon
166 * PARAM double sigma - gaussian equation parameter
167 * RETURN CImg<float> image resulting from the convolution
168 * */
169 CImg<float> ApplyGaussian(CImg<unsigned char> im,double sigma)
170 {
171 CImg<float> smoothIm(im.dimx(),im.dimy(),1,1,0);
173 //make gaussian kernel
174 CImg<float> gk = GaussianKernel(sigma);
175 //apply gaussian
177 CImg_5x5(I,int);
178 cimg_for5x5(im,X,Y,0,0,I)
179 {
180 float sum = 0;
181 sum += gk(0,0)*Ibb + gk(0,1)*Ibp + gk(0,2)*Ibc + gk(0,3)*Ibn + gk(0,4)*Iba;
182 sum += gk(1,0)*Ipb + gk(1,1)*Ipp + gk(1,2)*Ipc + gk(1,3)*Ipn + gk(1,4)*Ipa;
183 sum += gk(2,0)*Icb + gk(2,1)*Icp + gk(2,2)*Icc + gk(2,3)*Icn + gk(2,4)*Ica;
184 sum += gk(3,0)*Inb + gk(3,1)*Inp + gk(3,2)*Inc + gk(3,3)*Inn + gk(3,4)*Ina;
185 sum += gk(4,0)*Iab + gk(4,1)*Iap + gk(4,2)*Iac + gk(4,3)*Ian + gk(4,4)*Iaa;
186 smoothIm(X,Y)= sum/256;
187 }
189 return smoothIm;
190 }
191 /**
192 * PURPOSE: convert a given rgb image to a MxNX1 single vector grayscale image
193 * PARAM: CImg<unsigned char> im - rgb image to convert
194 * RETURN: CImg<unsigned char> grayscale image with MxNx1x1 dimensions
195 **/
197 CImg<unsigned char> RGBtoGrayScale(CImg<unsigned char> &im)
198 {
199 CImg<unsigned char> grayImage(im.dimx(),im.dimy(),im.dimz(),1,0);
200 if (im.dimv() == 3)
201 { cimg_forXYZ(im,X,Y,Z)
202 {
203 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));
204 }
205 }
206 grayImage.quantize(255,false);
207 return grayImage;
208 }
209 /**
210 * PURPOSE: aux. function used by CannyEdges to quantize an angle theta given by gradients, dx and dy
211 * into 0 - 7
212 * PARAM: dx,dy - gradient magnitudes
213 * RETURN int value between 0 and 7
214 **/
215 int GetAngle(int dy,int dx)
216 {
217 double angle = cimg::abs(std::atan2((double)dy,(double)dx));
218 if ((angle >= -cimg::valuePI/8)&&(angle <= cimg::valuePI/8))//-pi/8 to pi/8 => 0
219 return 0;
220 else if ((angle >= cimg::valuePI/8)&&(angle <= 3*cimg::valuePI/8))//pi/8 to 3pi/8 => pi/4
221 return 1;
222 else if ((angle > 3*cimg::valuePI/8)&&(angle <= 5*cimg::valuePI/8))//3pi/8 to 5pi/8 => pi/2
223 return 2;
224 else if ((angle > 5*cimg::valuePI/8)&&(angle <= 7*cimg::valuePI/8))//5pi/8 to 7pi/8 => 3pi/4
225 return 3;
226 else if (((angle > 7*cimg::valuePI/8) && (angle <= cimg::valuePI)) || ((angle <= -7*cimg::valuePI/8)&&(angle >= -cimg::valuePI))) //-7pi/8 to -pi OR 7pi/8 to pi => pi
227 return 4;
228 else return 0;
229 }
230 /**
231 * PURPOSE: create an edge map of the given image with hysteresis using thresholds T1 and T2
232 * PARAMS: CImg<float> im the image to perform edge detection on
233 * T1 lower threshold
234 * T2 upper threshold
235 * RETURN CImg<unsigned char> edge map
236 **/
237 CImg<unsigned char> CannyEdges(CImg<float> im, double T1, double T2, bool doHysteresis=false)
238 {
239 CImg<unsigned char> edges(im);
240 CImg<float> secDerivs(im);
241 secDerivs.fill(0);
242 edges.fill(0);
243 CImgList<float> gradients = im.get_gradient("xy",1);
244 int image_width = im.dimx();
245 int image_height = im.dimy();
247 { cimg_forXY(im,X,Y)
248 {
249 double Gr = std::sqrt(std::pow((double)gradients[0](X,Y),2.0) + std::pow((double)gradients[1](X,Y),2.0));
250 double theta = GetAngle(Y,X);
251 //if Gradient magnitude is positive and X,Y within the image
252 //take the 2nd deriv in the appropriate direction
253 if ((Gr > 0)&&(X < image_width-1)&&(Y < image_height - 1))
254 {
255 if (theta == 0)
256 secDerivs(X,Y) = im(X+2,Y) - 2*im(X+1,Y) + im(X,Y);
257 else if (theta == 1)
258 secDerivs(X,Y) = im(X+2,Y+2) - 2*im(X+1,Y+1) + im(X,Y);
259 else if (theta == 2)
260 secDerivs(X,Y) = im(X,Y+2) - 2*im(X,Y+1) + im(X,Y);
261 else if (theta == 3)
262 secDerivs(X,Y) = im(X+2,Y+2) - 2*im(X+1,Y+1) + im(X,Y);
263 else if (theta == 4)
264 secDerivs(X,Y) = im(X+2,Y) - 2*im(X+1,Y) + im(X,Y);
265 }
266 }}
267 //for each 2nd deriv that crosses a zero point and magnitude passes the upper threshold.
268 //Perform hysteresis in the direction of the gradient, rechecking the gradient
269 //angle for each pixel that meets the threshold requirement. Stop checking when
270 //the lower threshold is not reached.
271 CImg_5x5(I,float);
272 cimg_for5x5(secDerivs,X,Y,0,0,I)
273 {
274 if ( (Ipp*Ibb < 0)||
275 (Ipc*Ibc < 0)||
276 (Icp*Icb < 0) )
277 {
278 double Gr = std::sqrt(std::pow((double)gradients[0](X,Y),2.0) + std::pow((double)gradients[1](X,Y),2.0));
279 int dir = GetAngle(Y,X);
280 int Xt=X, Yt=Y, delta_x = 0, delta_y=0;
281 double GRt = Gr;
282 if (Gr >= T2)
283 edges(X,Y) = 255;
284 //work along the gradient in one direction
285 if (doHysteresis)
286 {
287 while ((Xt > 0) && (Xt < image_width-1) && (Yt > 0) && (Yt < image_height-1))
288 {
289 switch (dir){
290 case 0:delta_x=0;delta_y=1;break;
291 case 1:delta_x=1;delta_y=1;break;
292 case 2:delta_x=1;delta_y=0;break;
293 case 3:delta_x=1;delta_y=-1;break;
294 case 4:delta_x=0;delta_y=1;break;
295 }
296 Xt += delta_x;
297 Yt += delta_y;
298 GRt = std::sqrt(std::pow((double)gradients[0](Xt,Yt),2.0) + std::pow((double)gradients[1](Xt,Yt),2.0));
299 dir = GetAngle(Yt,Xt);
300 if (GRt >= T1)
301 edges(Xt,Yt) = 255;
302 }
303 //work along gradient in other direction
304 Xt = X; Yt = Y;
305 while ((Xt > 0) && (Xt < image_width-1) && (Yt > 0) && (Yt < image_height-1))
306 {
307 switch (dir){
308 case 0:delta_x=0;delta_y=1;break;
309 case 1:delta_x=1;delta_y=1;break;
310 case 2:delta_x=1;delta_y=0;break;
311 case 3:delta_x=1;delta_y=-1;break;
312 case 4:delta_x=0;delta_y=1;break;
313 }
314 Xt -= delta_x;
315 Yt -= delta_y;
316 GRt = std::sqrt(std::pow((double)gradients[0](Xt,Yt),2.0) + std::pow((double)gradients[1](Xt,Yt),2.0));
317 dir = GetAngle(Yt,Xt);
318 if (GRt >= T1)
319 edges(Xt,Yt) = 255;
320 }
321 }
322 }
323 }
324 return edges;
325 }
326 /**
327 * PURPOSE: perform radon transform of given image
328 * PARAM: CImg<unsigned char> im - image to detect lines
329 * int N - number of angles to consider (should be a power of 2)
330 * (the values of N will be spread over 0 to 2PI)
331 * RETURN CImg<unsigned char> - transform of given image of size, N x D
332 * D = rhomax = sqrt(dimx*dimx + dimy*dimy)/2
333 **/
334 CImg<> RadonTransform(CImg<unsigned char> im,int N)
335 {
336 int image_width = im.dimx();
337 int image_height = im.dimy();
339 //calc offsets to center the image
340 float xofftemp = image_width/2.0f - 1;
341 float yofftemp = image_height/2.0f - 1;
342 int xoffset = (int)std::floor(xofftemp + ROUNDING_FACTOR(xofftemp));
343 int yoffset = (int)std::floor(yofftemp + ROUNDING_FACTOR(yofftemp));
344 float dtemp = (float)std::sqrt((double)(xoffset*xoffset + yoffset*yoffset));
345 int D = (int)std::floor(dtemp + ROUNDING_FACTOR(dtemp));
347 CImg<> imRadon(N,D,1,1,0);
349 //for each angle k to consider
350 for (int k= 0 ; k < N; k++)
351 {
352 //only consider from PI/8 to 3PI/8 and 5PI/8 to 7PI/8
353 //to avoid computational complexity of a steep angle
354 if (k == 0){k = N/8;continue;}
355 else if (k == (3*N/8 + 1)){ k = 5*N/8;continue;}
356 else if (k == 7*N/8 + 1){k = N; continue;}
358 //for each rho length, determine linear equation and sum the line
359 //sum is to sum the values along the line at angle k2pi/N
360 //sum2 is to sum the values along the line at angle k2pi/N + N/4
361 //The sum2 is performed merely by swapping the x,y axis as if the image were rotated 90 degrees.
362 for (int d=0; d < D; d++){
363 double theta = 2*k*cimg::valuePI/N;//calculate actual theta
364 double alpha = std::tan(theta+cimg::valuePI/2);//calculate the slope
365 double beta_temp = -alpha*d*std::cos(theta) + d*std::sin(theta);//y-axis intercept for the line
366 int beta = (int)std::floor(beta_temp + ROUNDING_FACTOR(beta_temp));
367 //for each value of m along x-axis, calculate y
368 //if the x,y location is within the boundary for the respective image orientations, add to the sum
369 unsigned int sum1 = 0,
370 sum2 = 0;
371 int M = (image_width >= image_height) ? image_width : image_height;
372 for (int m=0;m < M; m++)
373 {
374 //interpolate in-between values using nearest-neighbor approximation
375 //using m,n as x,y indices into image
376 double n_temp = alpha*(m-xoffset) + beta;
377 int n = (int)std::floor(n_temp + ROUNDING_FACTOR(n_temp));
378 if ((m < image_width) && (n + yoffset >= 0) && (n + yoffset < image_height))
379 {
380 sum1 += im(m, n + yoffset);
381 }
382 n_temp = alpha*(m-yoffset) + beta;
383 n = (int)std::floor(n_temp + ROUNDING_FACTOR(n_temp));
384 if ((m < image_height)&&(n + xoffset >= 0)&&(n + xoffset < image_width))
385 {
386 sum2 += im(-(n + xoffset) + image_width - 1, m);
387 }
388 }
389 //assign the sums into the result matrix
390 imRadon(k,d) = (float)sum1;
391 //assign sum2 to angle position for theta+PI/4
392 imRadon(((k + N/4)%N),d) = (float)sum2;
393 }
394 }
395 return imRadon;
396 }
397 /* references:
398 * 1. See Peter Toft's thesis on the Radon transform: http://petertoft.dk/PhD/index.html
399 * While I changed his basic algorithm, the main idea is still the same and provides an excellent explanation.
400 *
401 * */