601.229 (S20): Assignment 4: Image processing with plugins

Due: Monday, April 13th by 11pm

Update 3/27 — the starter code zipfile csf_assign04.zip has been updated to fix an invalid pnglite.c file

Update 3/29 — starter code zipfile csf_assign04.zip has been updated to include the code for the example swapbg image plugin

Update 4/6 — the Error handling section has been updated to indicate the required format for an error message

Image processing with plugins

In this assignment you will use dynamic loading to implement an image-processing application supporting plugins to allow the program to support arbitrary image transformation algorithms.

Grading criteria

Your grade will be determined as follows:

Make sure you follow the style guidelines.

Your imgproc program and plugin shared libraries should execute without memory errors or memory leaks. Memory errors such as invalid reads or write, or uses of uninitialized memory, will result in a deduction of up to 10 points. Memory leaks will result in a deduction of up to 5 points.

Programming Languages

You may use either C or C++ for this project.

Tasks

This section describes how to get started and the tasks you will need to complete.

You can get started by downloading and unzipping the starter code: csf_assign04.zip

A suggested approach is to start by implementing the driver program and using it to execute the provided swapbg plugin on the example image. If it works correctly, you can move on to implementing the image plugins.

Image data type and functions

The image.h and image.c files in the starter code define a datatype called struct Image, which represents a bitmapped true-color image. The struct Image type is defined as follows:

struct Image {
    unsigned width, height;
    uint32_t *data;
};

The width and height fields represent the width and height of the image. The data array represents each pixel as a uint32_t element. The pixel values are a packed representation, where bits 0-7 are the red component, bits 8-15 are the green component, bits 16-23 are the blue component, and bits 24-31 are the alpha component. The alpha component determines the pixel’s degree of transparency, where 0 means completely transparent and 255 means completely opaque. For most images, the alpha values will be exclusively 255 (opaque.)

The following functions are defined for this datatype:

struct Image *img_create(unsigned width, unsigned height);
struct Image *img_duplicate(struct Image *img);
void img_destroy(struct Image *img);
struct Image *img_read_png(const char *filename);
int img_write_png(struct Image *img, const char *filename);
void img_unpack_pixel(uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a);
uint32_t img_pack_pixel(unsigned r, unsigned g, unsigned b, unsigned a);

Briefly:

The header file image.h has detailed comments describing how each function works.

Driver program

The driver program is responsible for loading the image plugins and carrying out the command specified by the command-line parameters.

The exectable for the driver program should be called imgproc. If invoked without command line arguments, or with incorrect command line arguments, it should print a usage message. Example:

$ ./imgproc
Usage: imgproc <command> [<command args...>]
Commands are:
  list
  exec <plugin> <input img> <output img> [<plugin args...>]

If imgproc is invoked with the list command, it should print the names and descriptions of each available plugin. Example:

$ ./imgproc list
Loaded 5 plugin(s)
 mirrorh: mirror image horizontally
 mirrorv: mirror image vertically
  swapbg: swap blue and green color component values
    tile: tile source image in an NxN arrangement
  expose: adjust the intensity of all pixels

The list command should produce one line of output per loaded plugin, as shown above. You should use the exact description text shown above for your implementations of the mirrorh, mirrorv, tile, and expose plugins. The ordering of the output lines is not important.

If imgproc is invoked with the exec command, it should use the named image plugin to transform a specified source image into a named destination image. For example, here is a possible invocation to execute the swapbg plugin on data/kitten.png to produce the output file kitten_swapbg.png:

./imgproc exec swapbg data/kitten.png kitten_swapbg.png

The tile plugin requires a single command line argument (following the name of the output file), which is an integer specifying the tiling factor. Example invocation:

./imgproc exec tile data/kitten.png kitten_tile_3.png 3

The expose plugin requires a single command line argument (following the name of the output file), which is a floating point value specifying the expose factor. Example invocation:

./imgproc exec expose data/kitten.png kitten_exp_0.5.png 0.5

Hints and specifications for the driver program

Very important: Your driver program must not implement any of the image transformations. All of the image transformations must be implemented within the plugin shared libraries.

The main challenges for implementing the driver program are discovering which plugins are available, loading them, and getting pointers to the plugin API functions for each plugin.

To discover which plugins are available, the driver program should first determine which directory contains the plugin shared libraries. If an environment variable called PLUGIN_DIR is set, the driver program should assume it contains the pathname of the plugin directory. (Use the getenv function to check whether this environment variable is set.) Otherwise, it should assume that the plugin shared libraries are in the ./plugins directory (i.e., the plugins subdirectory of the directory the driver program is running it.)

Once the driver program has determined the plugin directory, it should use the opendir, readdir, and closedir functions to find all of the files in the plugin directory which end in the .so file extension. Each such file should be assumed to be an image plugin shared library.

For each discovered plugin shared library, the driver program should dynamically load the shared library using dlopen, and then use dlsym to find the addresses of the plugin’s get_plugin_name, get_plugin_desc, parse_arguments, and transform_image functions. You may find that it is useful to have a data structure which keeps track of the handle pointer and function pointer values for each loaded image plugin. Here is a possible struct data type which could serve this purpose:

struct Plugin {
    void *handle;
    const char *(*get_plugin_name)(void);
    const char *(*get_plugin_desc)(void);
    void *(*parse_arguments)(int num_args, char *args[]);
    struct Image *(*transform_image)(struct Image *source, void *arg_data);
};

Once the driver program has discovered and loaded the plugins, it should determine which command was specified, and carry out the command.

The list command should iterate through the plugins and use the get_plugin_name and get_plugin_desc commands to get the name and short description of each available plugin.

The exec command should find a plugin whose name matches the specified plugin name, load the specified input image (using img_read_png), pass any command line arguments (past the input and output filenames) to the plugin’s parse_arguments function to produce an argument object, call the plugin’s transform_image function to perform the image transformation (passing the argument object returned by parse_arguments), and then save the resulting image to the named output file (using img_write_png).

Important: The driver program must contain the functions defined in image.c and pnglite.c. Also:

If your Linux environment doesn’t have the zlib development package installed, you will need to install it. On Ubuntu-based systems, including the CSF VM images, use the command

sudo apt-get install zlib1g-dev

Here is an example of what the commands to compile and link the driver program might look like:

gcc -g -Wall -Wextra -pedantic -std=gnu99 -fPIC -c imgproc.c -o imgproc.o
gcc -g -Wall -Wextra -pedantic -std=gnu99 -fPIC -c image.c -o image.o
gcc -g -Wall -Wextra -pedantic -std=gnu99 -fPIC -c pnglite.c -o pnglite.o
gcc -export-dynamic -o imgproc imgproc.o image.o pnglite.o -lz -ldl

Error handling

In any situation where the driver program cannot complete sucessfully, it should print an error message and exit with a non-zero exit code. Examples of situations that are errors include:

Update 4/6: The error message printed if an error occurs should have the form

Error: text of error message

It is not important what text is printed for text of error mesage. Error messages may be printed to either standard output or standard error.

As a special case, your driver program does not need to print an error message if the imgproc executable is invoked without any command line arguments. (It should just print the usage message in this case.)

Image plugins

An image plugin is a Linux shared library defining four specific API functions. You are responsible for implementing four image plugins:

The header file image_plugin.h defines the functions that each image plugin must implement:

const char *get_plugin_name(void);
const char *get_plugin_desc(void);
void *parse_arguments(int num_args, char *args[]);
struct Image *transform_image(struct Image *source, void *arg_data);

There are detailed header comments in image_plugin.h explaining how these functions are intended to work. In general:

The swapbg.c source file is an implementation of a complete image-processing plugin. You can build it using the following commands:

gcc -g -Wall -Wextra -pedantic -std=gnu99 -fPIC -c swapbg.c -o swapbg.o
mkdir -p plugins
gcc -o plugins/swapbg.so -shared swapbg.o

The transformations performed by each plugin are described below. Each transformation is applied to the following source image from placekitten.com (click for full size):

kitten

(Image by latch.r, some rights reserved)

swapbg plugin

The swapbg plugin, which is provided in the starter code, performs a very simple transformation by swapping the blue and green color values of each pixel. It does not take any command line parameters. Example result (click for full-size):

kitten b/g swap

Note that as a result of the transformation, the kitten’s brownish fur turned magenta.

mirrorh plugin

The mirrorh plugin generates a mirror image of the input image, with all pixels being reflected horizontally. It does not take any command line parameters. Example result (click for full-size):

kitten mirrored horizontally

mirrorv plugin

The mirrorv plugin generates a mirror image of the input image, with all pixels being reflected vertically. It does not take any command line parameters. Example result (click for full-size):

kitten mirrored vertically

tile plugin

The tile plugin generates an image containing an N x N arrangement of tiles, each tile being a smaller version of the original image, and the overall result image having the same dimensions as the original image. It takes one command line parameter, an integer specifying the tiling factor N.

Example result images (click for full-size):

N = 2 N = 3
kitten tiled 2x2 kitten tiled 3x3

Note that when the image’s width or height isn’t evenly divisible by N, the excess should be spread out, starting with the leftmost tiles (for excess width) and topmost tiles (for excess height). For example, in the 3 x 3 case for an 800x600 source image, the tile widths should be 267, 267, and 266, and the tile heights should be 200, 200, and 200.

The tiles should sample every Nth pixel from the source image horizontally and vertically.

expose plugin

The expose plugin changes all red/green/blue color component values by a specified factor. It takes a single command line argument, which is the floating point value to use as the factor. The factor must not be negative. Note that if the factor is greater than 1, multiplying the factor by a color component value in the original image could result in a value greater than 255. The transformation should limit all effective color component values to 255: this will cause “over-exposed” pixels to saturate towards white.

Example result images (click for full-size):

Factor = 0.5 Factor = 2.0
kitten exposed 0.5 kitten exposed 2.0

Packaging and submitting

Your implementation must have a Makefile such that executing make (i.e., building the default target) builds all of the following artifacts:

The exact name of the plugin shared libraries isn’t important, but it’s not a bad idea to have the shared library names match the plugin names. For example, your tile plugin could be build as a shared library called plugins/tile.so.

Your Makefile’s clean target should delete all executables and shared libraries.

Submit all of the source files and header files needed by your driver program and plugin implementations, along with your Makefile, in a single zipfile. For example, your command to produce the zipfile might look like the following:

$ zip -9r solution.zip Makefile *.c *.h
  adding: Makefile (deflated 51%)
  adding: demo.c (deflated 47%)
  adding: expose.c (deflated 58%)
  adding: image.c (deflated 69%)
  adding: imgproc.c (deflated 69%)
  adding: mirrorh.c (deflated 53%)
  adding: mirrorv.c (deflated 55%)
  adding: pnglite.c (deflated 76%)
  adding: swapbg.c (deflated 51%)
  adding: tile.c (deflated 63%)
  adding: image.h (deflated 57%)
  adding: image_plugin.h (deflated 56%)
  adding: pnglite.h (deflated 63%)

A reference Makefile is provided: Makefile. You may use or adapt this Makefile if you choose to.

Submit your zipfile to Gradescope as Assignment4.