Advanced C Programming

Spring 2019 :: ECE 264 :: Purdue University

⚠ This is for Spring 2019, not the current semester.
Due 4/24

Parallel image processing

Goals

The goals of this assignment are as follows:
  1. Learn to write parallel programs using multiple threads and the pthread library.
  2. Increase your depth in image programming.

Overview

You will create binarize(…), a function that works with your code from HW11 to convert an image to monochrome (black & white) using the “adaptive threshold” method.

Adaptive Thresholding

Thresholding is a technique used to convert an image to monochrome (black & white). It is important for problems such as scanned document processing, OCR, image segmentation, and medical imaging. Pixels brighter than some threshold are set to white. The rest are set to black. The key question becomes this: What should the threshold be?

If the image is color, you will need to calculate what each pixel would be as grayscale. In the image below (taken with a smartphone), the image at left is actually color. Notice that there is a little tint from the lighting in the room. The image right is true grayscale. Your code should be able to handle either one.


color

gray
// Credit: Donald Knuth; image is a photo of The Art of Computer Programming: Fundamental Algorithms, volume 1, 2nd edition, p. 272; photo by Alex Quinn

In the simplest case, you could use 50% gray ( ) so that        would be converted to       . That method—often called fixed threshold because it uses the same threshold value for all pixels—generally works well for scanned documents, as long as the brightness and contrast were set properly when they were scanned. However, in cases of photographs taken in uneven lighting or certain medical applications, a fixed threshold provides poor results, as you see below.


gray

monochrome, fixed threshold

Even if we were to use 30% gray or 70% gray, we would still have similar problems. Because the lighting is different in different regions of the image, there is no one good threshold to use for every pixel. The solution is adaptive threshold. The basic idea is that for each pixel in an image, you may use a different threshold value to decide whether that pixel should be black or white.


gray

monochrome, adaptive threshold

There are many good ways to decide the threshold, none of them perfect. For this assignment, we will keep it simple:

For a neighborhood radius r, a pixel at (x0, y0) shall be white if and only if its intensity is greater than the average intensity of all pixels in the (2r + 1) × (2r + 1) neighborhood surrounding that pixel. That neighborhood shall consist of all pixels at (xi, yi), such that |xi - x0| ≤ r and |yi - y0| ≤ r, including the current pixel being thresholded. Pixels that are not white shall be black. In calculating the threshold, do not include pixels that are beyond the boundaries of the image. This means that for such edge pixels, the neighborhood will be smaller.

Example: Suppose we binarize the following 6x6 image with radius=1.

250 120 0 20 240 100
220 110 10 30 230 130
40 45 50 55 60 65
70 75 80 85 90 95
155 145 135 125 115 105
165 175 185 195 205 215

The pixel at (x=2, y=3) currently has intensity value of 80. The neighborhood (radius=1) consists of pixels having intensity values 45, 50, 55, 75, 80, 85, 145, 135, and 125. The average of those is 88⅓. Since 80 ≤ 88⅓, the pixel at (x=2, y=3) shall be black.

Always calculate the threshold based on only the values in the input image.

(Optional) More information about adaptive threshold:

Multi-threaded programming

Calculating adaptive threshold can be computationally intensive, especially on high resolution images. Therefore, you will implement the adaptive threshold algorithm in parallel by using the pthread library. How can parallelism be implemented for this algorithm? Each pixel in the output image must have its threshold calculated and its intensity value set. Threads can be created such that each thread operates on a particular region of pixels in the image. How you choose to allocate pixels to a particular thread is up to you!

Your binarize(…) will create a copy, which makes your life a lot easier than it would be if they were modifying the image in place.

A thread is defined as an independent stream of instructions that can be scheduled to run by the operating system. Multi-threading allows multiple threads to exist within the context of a single process. These threads share a process' resources but are able to execute simultaneously and/or independently. You are responsible for creation of num_threads threads and their management. See the man pages for pthread_create(…) and pthread_join(…) for details on their parameters and how to pass data to your worker function. You can refer to example#1 and example#2 covered in class. Also for more examples of thread management you can refer here.

Warm-up exercises

This assignment includes a warm-up exercise to help you get ready. This accounts for 10% of your score for HW13. Scoring will be relatively light, but the usual base requirements apply.

Zero an array. Create a function bool zero array(int✶ array, int array length, int num threads, const char✶✶ a error) that sets every element of the given array to zero using num_threads threads.

Opt out.

In a hurry, and don't need the practice? This warm-up is here to help you learn what you need to succeed on the rest of this assignment—not to add additional work. Therefore, we give you an option. Those who feel that they do not need the practice may "opt out". by modifying warmup.c so that it does nothing but print the following message exactly and then exit:

I already know this and do not need to practice.

If you do that, then your score for HW13 will be based solely on the rest of this assignment. If you leave the warmup.c undone, if you do not turn in a warmup.c, or if the message it prints does not match perfectly, then you will receive 0 for the warmup portion of this assignment (10%).

Start

To get the starter files, enter 264get hw13 in bash. You will get the header files (bmp.h and mtat.h), a skeleton warmup.c, and several image files. The image files with "_bw_" in the filename are the output of the binarize(…) function, using the given radius.

You are strongly encouraged to work with the smallest image (6x6), using only 1, 2, or 3 threads. Keeping things simple and small will make your development and debugging more manageable.

Requirements

  1. Your submission must contain each of the following files, as specified:
  2. file contents
    mtat.c functions
    binarize(const BMPImage✶ image, int radius, int num threads, const char✶✶ a error)
    return type: BMPImage✶
    Convert the given image to monochrome using the adaptive threshold algorithm described above with num_threads threads.
    • image is a valid 24-bit BMP (v3) image.
    • Return a new monochrome BMPImage on the heap.
      • Each pixel must be either black (red==blue==green==0) or white (red==blue==green==255).
    • ⚠ Do not use division in calculating the new color intensity. (See Q&A #2.)
    • You may assume 1 ≤ num_threads ≤ 2,064,376.
      • If the image contains fewer than num_threads pixels, then you may create image -> header.width_px × image -> header.height_px threads, instead.
    • Handle run-time errors using the method described in HW11.
    • Caller is responsible for freeing the returned image using free_bmp(…).
    mtat.h declarations
    declaration of binarize(…)
    bmp.h declarations
    same as previous assignment
    bmp.c functions
    same as previous assignment
    test_mtat.c functions
    main(int argc, char✶ argv[])
    return type: int
    Test your binarize(…).
    • 100% code line coverage is required.
    • Tests will run in a directory containing all of the HW13 starter files.
    • Use your read_bmp(…), write_bmp(…),free_bmp(…) (from HW11), as needed.
    • Use miniunit.h.
    warmup.c functions
    zero array(int✶ array, int array length, int num threads, const char✶✶ a error)
    return type: bool
    Set every element of array to 0 using num_threads threads.
    • If successful, return true and do not modify error.
    • In case of the following run-time errors, return false and set *a_error to a descriptive error message.
      • array_length <= 0
      • array == NULL
      • thread could not be created
    • If array_length >= 1 and array != NULL, you may assume the array is valid and contains array_length int values
    • If num_threads > array_length, change num_threads to array_length of the array.
    • Message should be on the data segment.
  3. You may copy your create_bmp(…) and/or set_pixel(…) from your test_bmp.c.
  4. Do not turn in any image files.
  5. You may assume that images contain no more than 16,777,216 total pixels (e.g., 4096x4096).
  6. binarize(…) should not modify the image that is passed to it.
  7. You may modify mtat.h but not bmp.h. Do not modify the function signature of binarize(…).
  8. Only the following externally defined functions and constants are allowed in your .c files. (You may put the corresponding #include <…> statements in the .c file or in your mtat.h, at your option.) This does not apply to your bmp.c and bmp.h.
    header functions/symbols allowed in…
    assert.h assert(…) mtat.c, test_mtat.c, warmup.c
    pthread.h pthread_t, pthread_create, pthread_join mtat.c, test_mtat.c, warmup.c
    stdbool.h true, false mtat.c, test_mtat.c, warmup.c
    stdlib.h malloc(…), free(…), qsort(…), NULL, EXIT_SUCCESS, EXIT_FAILURE mtat.c, test_mtat.c, warmup.c
    stdio.h FILE mtat.c, test_mtat.c, warmup.c
    stdio.h fopen, fclose test_mtat.c
    string.h strcat(…), strlen(…), strcpy(…), strcmp(…), strerror(…), memcpy(…) mtat.c, test_mtat.c, warmup.c
    errno.h errno mtat.c, warmup.c
    If you need a simple math function like min(…), max(…), floor(…), or ceil(…), just write your own helper function.
  9. Submissions must meet the code quality standards and the policies on homework and academic integrity.

Compile

This assignment relies on your code from HW11 but they must be in separate files. Do not copy your HW11 code into mtat.c or mtat.h. Your code should work with any working HW11 code. Your mtat.c must #include both mtat.h and bmp.h.

To compile thread that uses the pthread library, you must include the -pthread flag.

gcc -pthread -o test_mtat mtat.c bmp.c test_mtat.c

Pre-tester

The pre-tester for HW13 has not yet been released. As soon as it is ready, this note will be changed.

Submit

In general, to submit any assignment for this course, you will use the following command:

264submit ASSIGNMENT FILES…

For HW13, you will type 264submit hw13 warmup.c mtat.c mtat.h bmp.c bmp.h test_mtat.c from inside your hw13 directory.

You can submit as often as you want, even if you are not finished with the assignment. That saves a backup copy which we can retrieve for you if you ever have a problem.

How much work is this?

This assignment does not require a lot of code. However, writing code that uses threads is different from most of what you have done in ECE 264. In particular, it can be challenging to debug. Give yourself time to understand multi-threaded programming.

Bonus #1: Median filter (2 bonus points)

Add a multi-threaded median filter using a very similar technique to the adaptive threshold. Do not use bubble sort or any other n2 algorithm to calculate the median. You may however use qsort(…).

Your function signature will look like this:

BMPImage✶ median(const BMPImage✶ image, int radius, int num threads, const char✶✶ a error)

To indicate that you have completed bonus #1, include the following at the top of your mtat.h file.

Bonus #1 will be accepted for full credit up to 04/29/2019. Add #define BONUS_PARALLEL_MEDIAN_FILTER to your mtat.h to indicate that you have completed Bonus #1. Submit together with the rest of your HW13. Your test_mtat.c should test your median(…), as well as the rest of your HW13. The same testing requirements apply. As long as your tests verify the correctness in some reasonable fashion—and your code passes them—you will get full credit for this bonus. There is generally no partial credit on bonuses. Scoring may be done manually or by an automated testing script, depending on the number who attempt it.

Bonus #1 is recommended for anyone who has finished the main assignment and wishes to pick up some extra points at the end of the semester. The points are intended to be relatively generous.

Bonus #2: Constant time median filter (6 bonus points)

Implement the constant time median filter algorithm described in this article by Simon Perreault & Patrick Hébert.

An in-person code review with the instructor is required for Bonus #2. Email the instructor by 04/29/2019 to arrange a time. The walk-through could occur as late as Sun 5/5/2019. Submit together with the rest of your HW13 any time before your scheduled code review. Add #define BONUS_CONSTANT_TIME_MEDIAN_FILTER to your mtat.h.

Bonus #2 is intended for who are hungry for a challenge. Last time it was offered, 4-5 students attempted it; 2 succeeded.

Q&A

  1. Why can't we use division to calculate pixel intensities?
    This is to ensure that results will be predictable and consistent. If people used division, there might be subtle differences between implementations based on how you structure the calculation.
  2. How can I calculate the color intensity without using division?
    First, let's look at how you would do this conceptually. You would convert each pixel to grayscale by computing the average of its red, blue, and green channels. That would yield a grayscale intensity between 0 and 255 for each pixel in the image. Then, for a given pixel, you would compare its grayscale intensity to the average of the grayscale intensities of the pixels in its neighborhood. Of course, computing these averages would use division. So how can we do it without division?
    Note that the sum of r+g+b for each cell is just 3 times the grayscale intensity. So, instead of dividing by 3, we will simply work with the r+g+b sum for every pixel. Likewise, instead of calculating the average within a neighborhood, we will multiply the value for the single pixel of interest by the number of pixels in its neighborhood and compare that with the sum of the r+g+b sums for every pixel in the neighborhood.
    Summary: Compare (r+g+b)*num_pixels_in_neighborhood with the sum of r+g+b over all cells in the neighborhood.
  3. How can I compare two BMP files?
    For your unit tests, you can create a helper function, probably with a for loop that compares each pixel, one by one.
    For ad hoc checking, you can use command line tools. Suppose we have two files, expected.bmp and actual.bmp. The most basic thing to do would be to use xxd to store the hex dump of each and then diff them.
    xxd expected.bmp > expected.bmp.txt
    xxd   actual.bmp >   actual.bmp.txt
    diff expected.bmp.txt actual.bmp.txt
    
    We can do much better using a bash feature called process substitution, combined with a vim feature called vimdiff.
    vimdiff <(xxd expected.bmp) <(xxd actual.bmp)
    To skip the header, and compare just the pixel data, add -s +54 to each xxd command:
    vimdiff <(xxd -s +54 expected.bmp) <(xxd -s +54 actual.bmp)
    To look at only the header, add -l 54 to each xxd command:
    vimdiff <(xxd -l 54 expected.bmp) <(xxd -l 54 actual.bmp)
    To see the difference at the command line, instead of in vim, substitute diff in place of vimdiff in the above commands.
  4. Why is the maximum number of threads 2,064,376?
    It is a system constant. To see the value on any Linux system, type this command in bash:
    cat /proc/sys/kernel/threads-max

Updates

4/16/2019 Correction: For warmup, error message should be on the data segment (not heap).
4/17/2019 Corrected several minor typos.
4/18/2019 You may assume image passed to binarize(…) is valid.
You may use fopen(…) and fclose(…) in test_mtat.c.
4/24/2019 Submit your miniunit.h and clog.h. This was already stated in one place.