Overview:
- ImageIOBase – base class for all image I/O
- ImageIOFactory – create ImageIO objects based on filename
- ImageFilter – base class for image filters: implement at least three yourself
As a suggestion: create your own namespace and put all your code in it.
ImageIOBase
Create an abstract class ImageIOBase, that will serve as the base class for two I/O classes: PipImageIO and MhdImageIO. Your abstract base class ImageIOBase should have:
- A protected constructor that takes a filename
- A protected string that holds the filename
- A virtual destructor
- Deleted copy constructor and assignment operator
- A pure virtual read and write function (they do not require a filename any more)
Next, create the two classes PipImageIO and MhdImageIO, that both derive from the base class ImageIOBase and override the read and write functions.
ImageIOFactory
For convenience of the user, it should not really matter whether he/she enters a PIP or MHD filename. And since both PipImageIO and MhdImageIO have the same public interface, derived from ImageIOBase, it does not really matter in the code as well. So, we are going to create a factory that creates the right I/O object for a given filename.
Create a class ImageIOFactory that has one public function: static ImageIOBase* getIO(const string& filename);
. Implement this function yourself in the ImageIOFactory.h file.
As a result, the following code should work in your main.cpp file:
string filename; cout << "Please enter a PIP or MHD filename: " << endl; cin >> filename; ImageIOBase* io = ImageIOFactory::getIO(filename); auto image = io->read(); delete io; io = nullptr;
and later:
io = ImageIOFactory::getIO("D:/brain_out.mhd"); io->write(image, { 109, 91, 80, 1, 1 }); // image, dimensions delete io; io = nullptr;
Note that the ImageIOFactory::getIO returns a pointer to an ImageIOBase object, which is actually either a PipImageIO or MhdImageIO object. The virtual method table makes sure that the correct read/write functions are called.
ImageFilter
Reading and writing images is nice, but the real fun is in image processing and analysis. For this task, we’ll implement a number of filters for image processing. In order to facilitate this and provide a nice public interface for users of your image processing library, create the following base class ImageFilter:
// Base class for all image filters to be implemented. class ImageFilter { typedef short T; // all we do is short public: // Constructor. ImageFilter() {}; // Destructor. This class does not own any free store allocated objects; // but derived classes might, so it's virtual and empty virtual ~ImageFilter() {}; // Because derived classes can add data/function/type members, we want to // avoid copying (and thus slicing) ImageFilter(const ImageFilter&) = delete; ImageFilter& operator=(const ImageFilter&) = delete; // Set the input image for this filter void setInput(const vector<T>& i) { // The input should be const. However, we cannot store it that way in our // base class. Therefor we cast away the const and store a pointer to the // original data. Upon using the input data in update(), we cast it back. _input = const_cast<vector<T>* >( &i ); } // Get the input back as a const ref const vector<T>& getInput() const { // Didn't touch _input, cast it back to const; return const_cast<const vector<T>&>(*_input); } // Get the output image result of this filter; // available after calling update() vector<T> getOutput() const { return _output; }; // Update the image filter and compute the output virtual void update() { // Didn't touch _input, cast it back to const; then execute() execute(getInput()); } protected: // Container for the output image vector<T> _output; // This method should be overloaded in your derived class and implement the // image filter that fills _output virtual void execute(const vector<T>& i) = 0; private: // Temporary storage of a pointer to the image data. Because we do some // const-magic, we keep it private. vector<T>* _input; };
As a tool builder making use of this base class, you’ll have to override the protected pure virtual function exectute(). Eventually, as a tool user you’ll just work with the setInput(), update(), and getOutput() functions.
As an example, lets implement a ThresholdImageFilter, as explained in section 6.1 of the book “Image Processing, Analysis, and Machine Vision“. Thresholding is the transformation of an input image i to an output image _output where all values above a threshold t are set to 1 and otherwise to 0.
class ThresholdImageFilter : public ImageFilter { typedef short T; public: // Constructor that initializes the threshold at 0 ThresholdImageFilter() : _t(0) {} // Get and set functions for the threshold value T getThreshold() const { return _t; } void setThreshold(T threshold) { _t = threshold; } protected: // Override the execute function virtual void execute(const vector<T>& i) override { // Clear and resize the output _output.clear(); _output.resize(i.size()); // Bring the threshold parameter within scope, so it can be given to // the lambda initializer const auto t = _t; // Do the thresholding with an std::transform, see Chapter 21 transform(begin(i), end(i), begin(_output), [t](T value) {return value > t ? T(1) : T(0); } ); } private: // The threshold T _t; };
Now you can apply thresholding to your images. For example, try the following on the brain.pip image and inspect the results in MeVisLab:
ThresholdImageFilter f; f.setInput(image); f.setThreshold(60); f.update(); image = f.getOutput();
Exercise: implement another three (or more) image filters to be added to your image processing library. Have a look at the Image Processing book for inspiration. Include at least a statistics filter (that does not return an image, but implements functions as getMin(), getMax(), getMean(), etc), a filter with a convolution mask (e.g. a filter that has a setRadius() function and computes an averaging within that radius around each voxel), and a filter that takes two input images and produces one (e.g. a mask filter that copies all voxels in the input to the output, but sets voxels to 0 where the mask is 0; for that add a setInputMask() function and override the update() function).
Next, make some small image processing pipeline based on your filters. For example, an application that masks all voxels below the mean value after smoothing the image.
Be creative. Implement some good image filters, more than three, write nice and clean code, comment clearly, provide example applications. Bonus points will be awarded for that. Don’t implement just the easy filters.
Note: focus on correctness and good code. More features is nice, better code is nicer.
Bonus stuff: just some suggestions in case you want to implement extra things.
- Various image filters using kernels / convolutions masks.
- Median filter, rank filter (where the user can choose the percentile), the efficient median filter algorithm from the IP book of Sonka.
- Edge detection, gradient derivatives, Sobel / Roberts/ Scharr / Prewitt operators. Separable kernels.
- Filters with multiple outputs (e.g. both the gradient magnitude and a 4D image with the gradient directions).
- Hough transforms.
If you are doing this assignment for a second time, you must implement more/new bonus stuff.