For her 8th grade science fair project, my daughter decided to examine the effect of changing tree canopy on the amount of solar energy reaching the ground. She wants to compare pyranometer (solar irradiance) data in an open area against data taken in a wooded area with deciduous trees. Starting in the fall, the amount of sunlight decreases rapidly with the approach of winter, but the trees lose their leaves at the same time. Does this mean that the amount of sunlight will actually increase in the wooded area?
I suggested that she regularly take digital pictures from the pyranometer site, straight up through the canopy, using the widest field of view available on the camera. This gives a visual record of leaf cover, but how can she convert this to a quantitative value? The solution is to convert the image to a black-and-white bitmap image and count the black and white pixels.
I use the freeware IrfanView photo editing program on my computer, and one of its image manipulation options is to produce a two-color black-and-white image. For the purposes of my daughter's project, it is not terribly important how the software makes a "black or white" decision for a particular pixel, but only that it does it consistently. Also, I used an option to resize the image, which will force the software to do some pixel averaging. Again, it is only consistency that is important. My digital camera saves images in .jpg format, but you can save the image from IrfanView in a number of other formats, including .bmp.
The next step is to write a program that will process the .bmp image. Here is an explanation of the .bmp file format. Basically, there are some "header" records that give information about the file and image, followed by the image itself. When IrfanView saves the image in a two-color file, the image part of the file holds 8 pixels per byte -- that is, a pixel is either "on" (white) or "off" black. Note that a two-color image is much smaller than the original image because it requires only one bit (1/8 byte) per pixel instead of as many as 24 bits (3 bytes) per pixel (for "24-bit color").
To make the programming easier, I required that the resized file have a width (in pixels) that is evenly divisible by 8. I chose to resize images to 504 pixels wide -- by default, IrfanView resizes the height of the image to keep the same image shape. This is a reasonable size for images displayed directly on a Web page without additional resizing. A full-color image from 15 October, 2005, resized to 504 pixels wide, is shown below next to the derived black and white image. The leaves have not yet started to change color and drop from the trees. Analysis of the black and white version of this image yields values of about 68% "canopy" and 32% "sky."
Wooded site, photographed straight up from pyranometer site on 15 October, 2005 | Derived B&W image for 15 October, 2005 |
I wrote the program bitmap.c in standard C (with Microsoft's very old but still reliable MS-DOS QuickC compiler), using the simplest approach I could think of. Each byte is read as a single character and converted to an "unsigned integer" as required. Larger integer values, such as the size of the file in bytes, are extracted "manually" by interpreting individual bytes rather than trying to declare a variable type that occupies the required number of bytes. I reasoned that the latter option might work differently with different C compilers and might therefore give unreliable results. By processing the file one byte at a time, using a char data type, and knowing which values are located where in the file, I can keep control over the interpretation of the file contents. A separate function is written to extract the "on/off" pixel information from each byte in the image. Here is the source code for the C program.
/* Program bitmap.c reads uncompressed .bmp files. Copyright 2005, David R. Brooks. Written with Microsoft QuickC. This program can be used without restriction for any educational or other non-commercial purpose, but please acknowledge the source. */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> void bw_count(unsigned char pixel, unsigned long *blk, unsigned long *wht); void main() { FILE *in; unsigned char ch[50],p_ch; char filename[20],yes_no; unsigned int k,i,bits_per_pixel,offset; unsigned long ht,wt,n_bytes; unsigned long blk,wht,sum_blk,sum_wht; /* File name no more than 8 characters, not including extension. */ printf("Give file name without its .bmp extension: " ); scanf("%s",filename); strcat(filename,".bmp"); /* Add .bmp file extension to name. */ in=fopen(filename,"r"); if (in == NULL) { printf("Can't find file. Abort.\n"); exit(1); } /* Read 14-byte file header. */ i=0; printf("File header: \n"); while (i<14) { fscanf(in,"%c",&ch[i]); printf("%i ",ch[i]); i++; } printf("\n"); printf("file size = %li\n", (long)ch[4]*65536+(long)ch[3]*256+(long)ch[2]); offset=(int)ch[11]*256+(int)ch[10]; printf("offset to image = %i\n",offset); /* Read 40-byte image information header. */ i=0; printf("Image header: \n"); while (i<40) { fscanf(in,"%c",&ch[i]); printf("%i ",ch[i]); i++; } printf("\n"); wt=(long)ch[6]*65536+(long)ch[5]*256+(long)ch[4]; ht=(long)ch[10]*65536+(long)ch[9]*256+(long)ch[8]; printf("image width, height, size: %li %li %li\n",wt,ht,wt*ht); printf("image size from header: %li\n", (long)ch[22]*65536+(long)ch[21]*256+(long)ch[20]); bits_per_pixel=(int)ch[14]; printf("bits per pixel = %i\n",bits_per_pixel); /* printf("number of colors = %i\n",(int)ch[33]*256+(int)ch[2]); */ fclose(in); printf("Read image now? (y or n) "); fflush(stdin); scanf("%c",&yes_no); if (yes_no == 'y') { /* Open file as binary to read image. */ in=fopen(filename,"rb"); fseek(in,offset,SEEK_SET); /* Go to first image byte. */ printf("Enter -1 to read entire file, or # of bytes: "); scanf("%li",&n_bytes); if (n_bytes != -1) { /* Read specified number of bytes. */ for (i=1; i<=n_bytes; i++) { fread(&p_ch,1,1,in); printf("%i ",p_ch); } } else { /* Read entire file, one row at a time. */ blk=0; wht=0; sum_blk=0; sum_wht=0; /* Initialize for B&W pixels. */ for (i=1; i<=ht; i++) { printf("\nRow # %4i\n",i); for (k=1; k<=wt/8; k++) { fread(&p_ch,1,1,in); printf("%i ",p_ch); if (bits_per_pixel == 1) { /* Count black and white pixels. */ bw_count(p_ch,&blk,&wht); sum_blk+=blk; sum_wht+=wht; } } fread(&p_ch,1,1,in); /* Read end-of-row byte. */ } printf("\n"); /* End of row. */ } printf("\n... done\n"); fclose(in); } /* End of read file loop. */ /* Display black and white pixel count. */ if ((bits_per_pixel == 1) && (n_bytes == -1)) printf("black white count: %li %li\n",sum_blk,sum_wht); printf("Percent black and white: %.1lf %.1lf\n", 100.*sum_blk/(sum_blk+sum_wht),100.*sum_wht/(sum_blk+sum_wht)); fflush(stdin); printf("Press any digit or character to quit... ");scanf("%c",&p_ch); } void bw_count(unsigned char p, unsigned long *blk, unsigned long *wht) { /* For a black and white image (8 pixels per byte, either 0 or 1), this function counts black and white pixels per byte. */ int i; unsigned long n=1,b=0,w=0; b=0; w=0; for (i=0; i<=7; i++) { /* printf("%li %li\n",n,p&n); */ if ((long)(p&n) == n) w++; else b++; /* Apply bitwise AND operator. */ n*=2; } *blk=b; *wht=w; }
The program asks you to enter the name of the file and then displays some properties of the file. You have the option to process part or all of a file. If you process the entire file, the program displays the number of black and white pixels, and the percentage of each. Shown below is some of the program's output for the 15 October image shown above.
You are welcome to use this program, with or without modification, for your own personal
and educational purposes. You may not use this program for any commercial purpose without my
written permission. The program code is offered as-is, without any promises of any kind about its
performance or applicability to your needs.
Please give an appropriate citation whenever referring to this program:
bitmap.c written by David R. Brooks, 2005.