# The Journey Continues: Creating My Own Image Processing Library

> Source: <https://dev.to/jrichards137/the-journey-continues-creating-my-own-image-processing-library-3enh>
> Published: 2026-09-10 20:47:51+00:00

During my study of Machine Learning, I learned about a method for improving the generalization capability of Neural Networks for image-based datasets called Data Augmentation. Now, I suspected at the time that there existed at least one library in Julia that focused on DA (which turned out to be called `Augmentor`.jl, though there are probably others), but I wanted to see if I could figure out for myself how to do at least one image processing operation without any outside help, in particular, image rotation. So, one day while at work at my manufacturing job I started visualizing matrices representing images in my head, and after about 2 1/2 hours of thinking about it while working, I felt that I'd figured out how to do digital image rotation through arbitrary degree. I went home after work and coded it up in Julia, and, lo and behold, it worked! Now, it was extremely slow at first (about 1000 ms to rotate a 552x736 grayscale image) and used A LOT of memory, but long story short, I eventually got the time down to around 80 ms for the same operation. Nevertheless, that was still almost 10x as long as the image rotation function in `Augmentor`. This puzzled me and frustrated me for a while, and it took me a fairly long time to figure out what made `Augmentor` so much faster than my own method, but somehow I stumbled upon a writeup about backwards warping, or inverse mapping. I already knew about interpolation, though I didn't really get it at first, but after I learned about inverse mapping the concept of interpolation and why it's used for certain image processing operations finally clicked. So, I eventually abandoned (for the time being) my own image rotation method and adopted the inverse mapping + bilinear interpolation method that I suspect `Augmentor` uses, which was much faster and used FAR less memory. After a period of tinkering with the code I finally achieved what I'd set out to achieve when I first tested `Augmentor`: my rotation function was now faster than theirs!

After transitioning to the updated rotation method and implementing it in code both on the CPU and GPU, I decided I would try to figure out all the other image processing operations `Augmentor` has in its library and implement those in code as well, both for a single image and for a batch of images, thus creating my own image processing library to be used for Machine Learning and other purposes. Here are the image transformation operations I have implemented, all of which I figured out how to do myself (with the exception of inverse mapping and bilinear interpolation¹, of course):

Four out of the seven operations use inverse mapping + bilinear interpolation: distortion, rotation, scaling, and zooming. Flipping doesn't use IM + BI because it doesn't need it, since the pixel coordinates for the relevant axis simply get reversed. Image filtering doesn't use bilinear interpolation because it's a different type of operation from the others (I don't know the exact theoretical details on what delineates the two types of operations, just that they're different). Finally, shearing doesn't use bilinear interpolation because I reasoned that, since the image only changed in one dimension, linear interpolation was sufficient. Again, I don't have a strong theoretical justification for doing this, just a logical one.

`Augmentor`
While I was inspired by `Augmentor` and still highly regard it as a generally well-designed and well-organized library, I do have some issues with it.

`GaussianBlur` only. No option for a different filtering type, whereas mine takes a conv kernel as input so that any conv operation under the sun can be performed on an image.`seed`; `Base` and other basic libraries such as `Random`.`:border` or `:value` (though this may not be all that useful in practice).
As an example of what my functions are capable of performance-wise, using my own image distorion function it takes my 1660ti MaxQ laptop GPU about **115 ms** to performm **16 augmentations** of the MNIST Training set that consists of 60000 28x28 grayscale images, which equates to about **7.2 ms** per augmentation. The multithreaded CPU distortion function, by contrast, takes around **180 ms** multi-threaded (**~700 ms** single threaded) to perform a **single augmentation** operation of the MNIST Training dataset, so **16 augmentations** would take about **3.2 s** multi-threaded (**11 s** single threaded). But this is a far cry from `Augmentor`, which takes about **1.8 s** on my system **per augmentation** when multi-threaded to perform the same operation (**11 s** otherwise), and so would take about **29 s** to do **16 augmentations** (around **3 m** single threaded). Again, though imperfect and a little unpolished, I used the distortion function under discussion to attain a max accuracy of 99.46% on the MNIST Test dataset in my Machine Learning program.

Since this blog post is getting a little long, a follow-up post will present numbers from benchmarks to backup my claims that my functions are fast, or at least, that they in general outperform those in `Augmentor` as well as the `Images.jl` `imrotate`, `imresize`, and `imfilter`² functions on the CPU. I will also provide benchmark results for my GPU functions.

*Note: Some of these are a rehash of what I've already discussed.*

`TODO` s
Your comments and suggestions are appreciated. Thanks for reading.

¹ I did try my hand at working out the math for bilinear interpolation, but as is normally the case I made an error, which after referencing the literature and correcting my mistake resulted in image quality comparable to `Augmentor` and `imrotate` and `imfilter`.

² The performance of my filtering functions falls far behind that of `imfilter` for larger kernels. It's hard to say, though, precisely when the inverse is true because it changes based on the image size. Apparently, imfilter uses a heuristic (don't know for sure) to determine when to apply multithreading, when to use FFT for convolutions, etc. and this changes based, no doubt, on several factors, one of which is image size. Mine, on the other hand, currently uses the same method of convolution regardless of kernel size and image size. But I think for the vast majority of non-separable kernels and image combinations that will be used in practice, especially for CNN's, mine is probably usually faster. Will have to do more testing to be sure. Separable kernels are another matter and is another area in which my functions need to be improved, since I don't yet have a method to more efficiently handle them. (Luckily, my GPU filtering functions are always faster than `imfilter` except for small images/arrays.)
