Canny edge detector: Difference between revisions
(Fixed assert bugs in C code, and other small improvements, "wb", "rb") |
(Better C entry) |
||
Line 23: | Line 23: | ||
* http://en.wikipedia.org/wiki/BMP_file_format |
* http://en.wikipedia.org/wiki/BMP_file_format |
||
* |
* |
||
* Note: the magic number has been removed from the |
* Note: the magic number has been removed from the bmpfile_header_t structure |
||
* since it causes alignment problems |
* since it causes alignment problems |
||
* |
* bmpfile_magic_t should be written/read first |
||
* followed by the |
* followed by the |
||
* |
* bmpfile_header_t |
||
* [this avoids compiler-specific alignment pragmas etc.] |
* [this avoids compiler-specific alignment pragmas etc.] |
||
*/ |
*/ |
||
Line 33: | Line 33: | ||
typedef struct { |
typedef struct { |
||
unsigned char magic[2]; |
unsigned char magic[2]; |
||
} bmpfile_magic_t; |
|||
} bmpfile_magic; |
|||
typedef struct { |
typedef struct { |
||
Line 40: | Line 40: | ||
uint16_t creator2; |
uint16_t creator2; |
||
uint32_t bmp_offset; |
uint32_t bmp_offset; |
||
} bmpfile_header_t; |
|||
} bmpfile_header; |
|||
typedef struct { |
typedef struct { |
||
uint32_t header_sz; |
uint32_t header_sz; |
||
int32_t width; |
int32_t width; |
||
int32_t height; |
int32_t height; |
||
uint16_t nplanes; |
uint16_t nplanes; |
||
uint16_t bitspp; |
uint16_t bitspp; |
||
uint32_t compress_type; |
uint32_t compress_type; |
||
uint32_t bmp_bytesz; |
uint32_t bmp_bytesz; |
||
int32_t hres; |
int32_t hres; |
||
int32_t vres; |
int32_t vres; |
||
uint32_t ncolors; |
uint32_t ncolors; |
||
uint32_t nimpcolors; |
uint32_t nimpcolors; |
||
} bitmap_info_header_t; |
|||
} BITMAPINFOHEADER; |
|||
typedef struct { |
typedef struct { |
||
Line 62: | Line 62: | ||
uint8_t nothing; |
uint8_t nothing; |
||
} rgb_t; |
} rgb_t; |
||
BITMAPINFOHEADER ih; |
|||
// Use int instead `unsigned char' so that we can store negative values. |
// Use int instead `unsigned char' so that we can store negative values. |
||
typedef int pixel_t; |
typedef int pixel_t; |
||
pixel_t *load_bmp(const char *filename, |
pixel_t *load_bmp(const char *filename, bitmap_info_header_t *bitmapInfoHeader) |
||
{ |
{ |
||
FILE *filePtr; // our file pointer |
FILE *filePtr; // our file pointer |
||
bmpfile_magic_t mag; |
|||
bmpfile_header_t bitmapFileHeader; // our bitmap file header |
|||
pixel_t *bitmapImage; // store image data |
pixel_t *bitmapImage; // store image data |
||
size_t i; |
size_t i; |
||
Line 83: | Line 81: | ||
} |
} |
||
if (fread(&mag, sizeof( |
if (fread(&mag, sizeof(bmpfile_magic_t), 1, filePtr) != 1) exit(1); |
||
// verify that this is a bmp file by check bitmap id |
// verify that this is a bmp file by check bitmap id |
||
Line 95: | Line 93: | ||
// read the bitmap file header |
// read the bitmap file header |
||
if (fread(&bitmapFileHeader, sizeof( |
if (fread(&bitmapFileHeader, sizeof(bmpfile_header_t), 1, filePtr) != 1) |
||
exit(1); |
exit(1); |
||
// read the bitmap info header |
// read the bitmap info header |
||
if (fread(bitmapInfoHeader, sizeof( |
if (fread(bitmapInfoHeader, sizeof(bitmap_info_header_t), 1, filePtr) != 1) |
||
exit(1); |
exit(1); |
||
Line 133: | Line 131: | ||
// Return: nonzero on error. |
// Return: nonzero on error. |
||
int save_bmp(const char *filename, const |
int save_bmp(const char *filename, const bitmap_info_header_t *bmp_ih, |
||
const pixel_t *data) |
|||
{ |
{ |
||
uint32_t offset = sizeof( |
uint32_t offset = sizeof(bmpfile_magic_t) + |
||
sizeof( |
sizeof(bmpfile_header_t) + |
||
sizeof( |
sizeof(bitmap_info_header_t) + |
||
((1U << bmp_ih->bitspp) * 4); |
((1U << bmp_ih->bitspp) * 4); |
||
bmpfile_header_t bmp_fh = { |
|||
.filesz = offset + bmp_ih->bmp_bytesz, |
.filesz = offset + bmp_ih->bmp_bytesz, |
||
.creator1 = 0, |
.creator1 = 0, |
||
Line 147: | Line 146: | ||
}; |
}; |
||
bmpfile_magic_t mag = {{0x42, 0x4d}}; |
|||
rgb_t color = {0, 0, 0, 0}; |
rgb_t color = {0, 0, 0, 0}; |
||
size_t i; |
size_t i; |
||
Line 155: | Line 154: | ||
return 1; |
return 1; |
||
fwrite(&mag, 1, sizeof( |
fwrite(&mag, 1, sizeof(bmpfile_magic_t), fp); |
||
fwrite(&bmp_fh, 1, sizeof( |
fwrite(&bmp_fh, 1, sizeof(bmpfile_header_t), fp); |
||
fwrite(bmp_ih, 1, sizeof( |
fwrite(bmp_ih, 1, sizeof(bitmap_info_header_t), fp); |
||
// Palette |
// Palette |
||
Line 180: | Line 179: | ||
// if norm==1, map pixels to range 0..MAX_BRIGHTNESS |
// if norm==1, map pixels to range 0..MAX_BRIGHTNESS |
||
void convolution(const pixel_t *in, pixel_t *out, const float *kernel, |
void convolution(const pixel_t *in, pixel_t *out, const float *kernel, |
||
int nx, int ny, int kn, int norm) |
const int nx, const int ny, const int kn, const int norm) |
||
{ |
{ |
||
int i, j, m, n, c; |
int i, j, m, n, c; |
||
int khalf = (int)floor(kn / 2.0); |
const int khalf = (int)floor(kn / 2.0); |
||
float pixel, min = FLT_MAX, max = FLT_MIN; |
float pixel, min = FLT_MAX, max = FLT_MIN; |
||
Line 189: | Line 188: | ||
for (m = khalf; m < nx - khalf; m++) |
for (m = khalf; m < nx - khalf; m++) |
||
for (n = khalf; n < ny - khalf; n++) { |
for (n = khalf; n < ny - khalf; n++) { |
||
pixel = 0; |
pixel = 0.0; |
||
c = 0; |
c = 0; |
||
for (j = -khalf; j <= khalf; j++) |
for (j = -khalf; j <= khalf; j++) |
||
Line 202: | Line 201: | ||
for (m = khalf; m < nx - khalf; m++) |
for (m = khalf; m < nx - khalf; m++) |
||
for (n = khalf; n < ny - khalf; n++) { |
for (n = khalf; n < ny - khalf; n++) { |
||
pixel = 0; |
pixel = 0.0; |
||
c = 0; |
c = 0; |
||
for (j = -khalf; j <= khalf; j++) |
for (j = -khalf; j <= khalf; j++) |
||
Line 224: | Line 223: | ||
// kernelSize = 2 * int(2*sigma) + 3; |
// kernelSize = 2 * int(2*sigma) + 3; |
||
void gaussian_filter(const pixel_t *in, pixel_t *out, |
void gaussian_filter(const pixel_t *in, pixel_t *out, |
||
const int nx, const int ny, const float sigma) |
|||
{ |
{ |
||
int i, j, c = 0; |
int i, j, c = 0; |
||
Line 248: | Line 248: | ||
* Note: T1 and T2 are lower and upper thresholds. |
* Note: T1 and T2 are lower and upper thresholds. |
||
*/ |
*/ |
||
pixel_t *canny_edge_detection(const pixel_t *in, const bitmap_info_header_t *bmp_ih, |
|||
int tmin, int tmax, float sigma) |
const int tmin, const int tmax, const float sigma) |
||
{ |
{ |
||
int i, j, c, Gmax; |
int i, j, c, Gmax; |
||
Line 260: | Line 260: | ||
0, 0, 0, |
0, 0, 0, |
||
-1,-2,-1}; |
-1,-2,-1}; |
||
const int nx = bmp_ih->width; |
|||
const int ny = bmp_ih->height; |
|||
pixel_t *G = calloc(nx * ny * sizeof(pixel_t), 1); |
pixel_t *G = calloc(nx * ny * sizeof(pixel_t), 1); |
||
⚫ | |||
pixel_t *after_Gx = calloc(nx * ny * sizeof(pixel_t), 1); |
pixel_t *after_Gx = calloc(nx * ny * sizeof(pixel_t), 1); |
||
⚫ | |||
pixel_t *after_Gy = calloc(nx * ny * sizeof(pixel_t), 1); |
pixel_t *after_Gy = calloc(nx * ny * sizeof(pixel_t), 1); |
||
⚫ | |||
pixel_t *nms = calloc(nx * ny * sizeof(pixel_t), 1); |
pixel_t *nms = calloc(nx * ny * sizeof(pixel_t), 1); |
||
pixel_t *edges, *out; |
|||
pixel_t *edges; |
|||
int nedges, k, t; |
int nedges, k, t; |
||
int nbs[8]; // neighbours |
int nbs[8]; // neighbours |
||
if (!G || !after_Gx || !after_Gy || !nms) |
|||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
gaussian_filter(in, out, nx, ny, sigma); |
gaussian_filter(in, out, nx, ny, sigma); |
||
Line 322: | Line 328: | ||
// Tracing edges with hysteresis . Non-recursive implementation. |
// Tracing edges with hysteresis . Non-recursive implementation. |
||
for (c = 1, j = 1; j < ny - 1; j++) |
for (c = 1, j = 1; j < ny - 1; j++) |
||
for (i = 1; i < nx-1; i++) { |
for (i = 1; i < nx - 1; i++) { |
||
if (nms[c] >= tmax && out[c] == 0) { // trace edges |
if (nms[c] >= tmax && out[c] == 0) { // trace edges |
||
out[c] = MAX_BRIGHTNESS; |
out[c] = MAX_BRIGHTNESS; |
||
Line 342: | Line 348: | ||
for (k = 0; k < 8; k++) |
for (k = 0; k < 8; k++) |
||
if (nms[nbs[k]] >= tmin && out[ |
if (nms[nbs[k]] >= tmin && out[nbs[k]] == 0) { |
||
out[nbs[k]] = MAX_BRIGHTNESS; |
out[nbs[k]] = MAX_BRIGHTNESS; |
||
edges[nedges++] = nbs[k]; |
edges[nedges++] = nbs[k]; |
||
Line 355: | Line 361: | ||
free(G); |
free(G); |
||
free(nms); |
free(nms); |
||
return out; |
|||
} |
} |
||
int main(int argc, const char ** const argv) |
int main(const int argc, const char ** const argv) |
||
{ |
{ |
||
pixel_t * |
pixel_t *in_bitmap_data, *out_bitmap_data; |
||
static bitmap_info_header_t ih; |
|||
if (argc < 2) { |
if (argc < 2) { |
||
Line 366: | Line 375: | ||
} |
} |
||
in_bitmap_data = load_bmp(argv[1], &ih); |
|||
if ( |
if (in_bitmap_data == NULL) |
||
exit(1); |
|||
⚫ | |||
⚫ | |||
exit(1); |
exit(1); |
||
printf("Info: %d x %d x %d\n", ih.width, ih.height, ih.bitspp); |
printf("Info: %d x %d x %d\n", ih.width, ih.height, ih.bitspp); |
||
canny_edge_detection( |
out_bitmap_data = canny_edge_detection(in_bitmap_data, &ih, 45, 50, 1.0f); |
||
if (out_bitmap_data == NULL) |
|||
⚫ | |||
if (save_bmp("out.bmp", &ih, |
if (save_bmp("out.bmp", &ih, out_bitmap_data) != 0) |
||
exit(1); |
exit(1); |
||
⚫ | |||
free( |
free(in_bitmap_data); |
||
⚫ | |||
return 0; |
return 0; |
Revision as of 00:38, 10 March 2012
Task: Write a program that performs so-called canny edge detection on an image. The algorithm consists of the following steps:
- Noise reduction. May be performed by Gaussian filter.
- Compute intensity gradient (matrices and ) and its magnitude .
May be performed by convolution of an image with Sobel operators. - Non-maximum suppression. For each pixel compute the orientation of intensity gradient vector: . Transform angle to one of four directions: 0, 45, 90, 135 degrees. Compute new array : if
where is the current pixel, and are the two neighbour pixels in the direction of gradient, then , otherwise . Nonzero pixels in resulting array correspond to local maxima of in direction . - Tracing edges with hysteresis. At this stage two thresholds for the values of are introduced: and . Starting from pixels with find all paths of pixels with and put them to the resulting image.
C
The following program reads an 8 bits per pixel grayscale BMP file and saves the result to `out.bmp'. Compile with `-lm'. <lang c>#include <stdint.h>
- include <stdio.h>
- include <stdlib.h>
- include <float.h>
- include <math.h>
- include <string.h>
- define MAX_BRIGHTNESS 255
/*
* Loading part taken from * http://www.vbforums.com/showthread.php?t=261522 * BMP info: * http://en.wikipedia.org/wiki/BMP_file_format * * Note: the magic number has been removed from the bmpfile_header_t structure * since it causes alignment problems * bmpfile_magic_t should be written/read first * followed by the * bmpfile_header_t * [this avoids compiler-specific alignment pragmas etc.] */
typedef struct {
unsigned char magic[2];
} bmpfile_magic_t;
typedef struct {
uint32_t filesz; uint16_t creator1; uint16_t creator2; uint32_t bmp_offset;
} bmpfile_header_t;
typedef struct {
uint32_t header_sz; int32_t width; int32_t height; uint16_t nplanes; uint16_t bitspp; uint32_t compress_type; uint32_t bmp_bytesz; int32_t hres; int32_t vres; uint32_t ncolors; uint32_t nimpcolors;
} bitmap_info_header_t;
typedef struct {
uint8_t r; uint8_t g; uint8_t b; uint8_t nothing;
} rgb_t;
// Use int instead `unsigned char' so that we can store negative values. typedef int pixel_t;
pixel_t *load_bmp(const char *filename, bitmap_info_header_t *bitmapInfoHeader) {
FILE *filePtr; // our file pointer bmpfile_magic_t mag; bmpfile_header_t bitmapFileHeader; // our bitmap file header pixel_t *bitmapImage; // store image data size_t i; unsigned char c;
filePtr = fopen(filename, "rb"); if (filePtr == NULL) { perror("fopen()"); exit(1); }
if (fread(&mag, sizeof(bmpfile_magic_t), 1, filePtr) != 1) exit(1);
// verify that this is a bmp file by check bitmap id // warning: dereferencing type-punned pointer will break // strict-aliasing rules [-Wstrict-aliasing] if (*((uint16_t*)mag.magic) != 0x4D42) { fprintf(stderr, "Not a BMP file: magic=%c%c\n", mag.magic[0], mag.magic[1]); fclose(filePtr); return NULL; }
// read the bitmap file header if (fread(&bitmapFileHeader, sizeof(bmpfile_header_t), 1, filePtr) != 1) exit(1);
// read the bitmap info header if (fread(bitmapInfoHeader, sizeof(bitmap_info_header_t), 1, filePtr) != 1) exit(1);
if (bitmapInfoHeader->compress_type != 0) fprintf(stderr, "Warning, compression is not supported.\n");
// move file point to the beginning of bitmap data fseek(filePtr, bitmapFileHeader.bmp_offset, SEEK_SET);
// allocate enough memory for the bitmap image data bitmapImage = (pixel_t*)malloc(bitmapInfoHeader->bmp_bytesz * sizeof(pixel_t));
// verify memory allocation if (bitmapImage == NULL) { fclose(filePtr); return NULL; }
// read in the bitmap image data for (i = 0; i < bitmapInfoHeader->bmp_bytesz; i++) { if (fread(&c, sizeof(unsigned char), 1, filePtr) != 1) exit(1); bitmapImage[i] = (int)c; }
// If we were using unsigned char as pixel_t, then: // fread(bitmapImage, 1, bitmapInfoHeader->bmp_bytesz, filePtr);
// close file and return bitmap image data fclose(filePtr); return bitmapImage;
}
// Return: nonzero on error. int save_bmp(const char *filename, const bitmap_info_header_t *bmp_ih,
const pixel_t *data)
{
uint32_t offset = sizeof(bmpfile_magic_t) + sizeof(bmpfile_header_t) + sizeof(bitmap_info_header_t) + ((1U << bmp_ih->bitspp) * 4);
bmpfile_header_t bmp_fh = { .filesz = offset + bmp_ih->bmp_bytesz, .creator1 = 0, .creator2 = 0, .bmp_offset = offset };
bmpfile_magic_t mag = Template:0x42, 0x4d; rgb_t color = {0, 0, 0, 0}; size_t i; FILE* fp = fopen(filename, "wb");
if (fp == NULL) return 1;
fwrite(&mag, 1, sizeof(bmpfile_magic_t), fp); fwrite(&bmp_fh, 1, sizeof(bmpfile_header_t), fp); fwrite(bmp_ih, 1, sizeof(bitmap_info_header_t), fp);
// Palette for (i = 0; i < (1U << bmp_ih->bitspp); i++) { color.r = (uint8_t)i; color.g = (uint8_t)i; color.b = (uint8_t)i; fwrite(&color, 1, sizeof(rgb_t), fp); }
// We use int instead of uchar, so we can't write img in 1 call any more. // fwrite(data, 1, bmp_ih->bmp_bytesz, fp); for (i = 0; i < bmp_ih->bmp_bytesz; i++) { unsigned char c = (unsigned char)data[i]; fwrite(&c, sizeof(unsigned char), 1, fp); }
fclose(fp); return 0;
}
// if norm==1, map pixels to range 0..MAX_BRIGHTNESS void convolution(const pixel_t *in, pixel_t *out, const float *kernel,
const int nx, const int ny, const int kn, const int norm)
{
int i, j, m, n, c; const int khalf = (int)floor(kn / 2.0); float pixel, min = FLT_MAX, max = FLT_MIN;
if (norm) for (m = khalf; m < nx - khalf; m++) for (n = khalf; n < ny - khalf; n++) { pixel = 0.0; c = 0; for (j = -khalf; j <= khalf; j++) for (i = -khalf; i <= khalf; i++) pixel += in[(n - j) * nx + m - i] * kernel[c++]; if (pixel < min) min = pixel; if (pixel > max) max = pixel; }
for (m = khalf; m < nx - khalf; m++) for (n = khalf; n < ny - khalf; n++) { pixel = 0.0; c = 0; for (j = -khalf; j <= khalf; j++) for (i = -khalf; i <= khalf; i++) pixel += in[(n - j) * nx + m - i] * kernel[c++];
if (norm) pixel = MAX_BRIGHTNESS * (pixel - min) / (max - min); out[n * nx + m] = (pixel_t)pixel; }
}
// http:// www.songho.ca/dsp/cannyedge/cannyedge.html // determine size of kernel (odd #) // 0.0 <= sigma < 0.5 : 3 // 0.5 <= sigma < 1.0 : 5 // 1.0 <= sigma < 1.5 : 7 // 1.5 <= sigma < 2.0 : 9 // 2.0 <= sigma < 2.5 : 11 // 2.5 <= sigma < 3.0 : 13 ... // kernelSize = 2 * int(2*sigma) + 3;
void gaussian_filter(const pixel_t *in, pixel_t *out,
const int nx, const int ny, const float sigma)
{
int i, j, c = 0; const int n = 2 * (int)(2 * sigma) + 3; float mean = (float)floor(n / 2.0); float kernel[n * n];
fprintf(stderr, "gaussian_filter: kernel size %d, sigma=%g\n", n, sigma); for (i = 0; i < n; i++) for (j = 0; j < n; j++) kernel[c++] = exp(-0.5 * (pow((i - mean) / sigma, 2.0) + pow((j - mean) / sigma, 2.0))) / (2 * M_PI * sigma * sigma); convolution(in, out, kernel, nx, ny, n, 1);
}
/*
* Links: * http:// en.wikipedia.org/wiki/Canny_edge_detector * http:// www.tomgibara.com/computer-vision/CannyEdgeDetector.java * http:// fourier.eng.hmc.edu/e161/lectures/canny/node1.html * http:// www.songho.ca/dsp/cannyedge/cannyedge.html * * Note: T1 and T2 are lower and upper thresholds. */
pixel_t *canny_edge_detection(const pixel_t *in, const bitmap_info_header_t *bmp_ih,
const int tmin, const int tmax, const float sigma)
{
int i, j, c, Gmax;
const float Gx[] = {-1, 0, 1, -2, 0, 2, -1, 0, 1};
const float Gy[] = { 1, 2, 1, 0, 0, 0, -1,-2,-1};
const int nx = bmp_ih->width; const int ny = bmp_ih->height;
pixel_t *G = calloc(nx * ny * sizeof(pixel_t), 1); pixel_t *after_Gx = calloc(nx * ny * sizeof(pixel_t), 1); pixel_t *after_Gy = calloc(nx * ny * sizeof(pixel_t), 1); pixel_t *nms = calloc(nx * ny * sizeof(pixel_t), 1); pixel_t *edges, *out; int nedges, k, t; int nbs[8]; // neighbours
if (!G || !after_Gx || !after_Gy || !nms) exit(1);
out = malloc(bmp_ih->bmp_bytesz * sizeof(pixel_t)); if (out == NULL) exit(1);
gaussian_filter(in, out, nx, ny, sigma);
convolution(out, after_Gx, Gx, nx, ny, 3, 0); convolution(out, after_Gy, Gy, nx, ny, 3, 0);
Gmax = 0; for (i = 1; i < nx - 1; i++) for (j = 1; j < ny - 1; j++) { c = i + nx * j; // G[c] = abs(after_Gx[c]) + abs(after_Gy[c]); G[c] = (pixel_t)hypot(after_Gx[c], after_Gy[c]); if (G[c] > Gmax) Gmax = G[c]; }
// Non-maximum suppression, straightforward implementation. for (i = 1; i < nx - 1; i++) for (j = 1; j < ny - 1; j++) { float dir; int nn, ss, ww, ee, nw, ne, sw, se;
c = i + nx * j; nn = c - nx; ss = c + nx; ww = c + 1; ee = c - 1; nw = nn + 1; ne = nn - 1; sw = ss + 1; se = ss - 1;
dir = (float)(fmod(atan2(after_Gy[c], after_Gx[c]) + M_PI, M_PI) / M_PI) * 8;
if (((dir <= 1 || dir > 7) && G[c] > G[ee] && G[c] > G[ww]) || // 0 deg ((dir > 1 && dir <= 3) && G[c] > G[nw] && G[c] > G[se]) || // 45 deg ((dir > 3 && dir <= 5) && G[c] > G[nn] && G[c] > G[ss]) || // 90 deg ((dir > 5 && dir <= 7) && G[c] > G[ne] && G[c] > G[sw])) // 135 deg nms[c] = G[c]; else nms[c] = 0; }
// Reuse array edges = after_Gy; // used as a stack memset(out, 0, sizeof(pixel_t) * nx * ny); memset(edges, 0, sizeof(pixel_t) * nx * ny);
// Tracing edges with hysteresis . Non-recursive implementation. for (c = 1, j = 1; j < ny - 1; j++) for (i = 1; i < nx - 1; i++) { if (nms[c] >= tmax && out[c] == 0) { // trace edges out[c] = MAX_BRIGHTNESS; nedges = 1; edges[0] = c;
do { nedges--; t = edges[nedges];
nbs[0] = t - nx; // nn nbs[1] = t + nx; // ss nbs[2] = t + 1; // ww nbs[3] = t - 1; // ee nbs[4] = nbs[0] + 1; // nw nbs[5] = nbs[0] - 1; // ne nbs[6] = nbs[1] + 1; // sw nbs[7] = nbs[1] - 1; // se
for (k = 0; k < 8; k++) if (nms[nbs[k]] >= tmin && out[nbs[k]] == 0) { out[nbs[k]] = MAX_BRIGHTNESS; edges[nedges++] = nbs[k]; } } while (nedges > 0); } c++; }
free(after_Gx); free(after_Gy); free(G); free(nms);
return out;
}
int main(const int argc, const char ** const argv) {
pixel_t *in_bitmap_data, *out_bitmap_data; static bitmap_info_header_t ih;
if (argc < 2) { printf("Usage: %s image.bmp\n", argv[0]); exit(1); }
in_bitmap_data = load_bmp(argv[1], &ih); if (in_bitmap_data == NULL) exit(1);
printf("Info: %d x %d x %d\n", ih.width, ih.height, ih.bitspp);
out_bitmap_data = canny_edge_detection(in_bitmap_data, &ih, 45, 50, 1.0f); if (out_bitmap_data == NULL) exit(1);
if (save_bmp("out.bmp", &ih, out_bitmap_data) != 0) exit(1);
free(in_bitmap_data); free(out_bitmap_data);
return 0;
}</lang>