Matrix Multiplication In C# - Applying Transformations To Images

Introduction

 
Today, I will show you my implementation of matrix multiplication C# and how to use it to apply basic transformations to images like rotation, stretching, flipping, and modifying color density.
 
Please note that this is not an image processing class. Rather, this article demonstrates in C# three of the core linear algebra concepts, matrix multiplication, dot product, and transformation matrices.
 

Source Code

 
Source code for this article is available on GitHub on the following repository.
 
This implementation is also included in the linear algebra problems component, Elsheimy.Components.Linears, available on,

Matrix Multiplication

 
The math behind matrix multiplication is very straightforward. Very easy explanations can be found here and here.
 
Let’s get directly to the code and start with our main function:
  1. public static double[,] Multiply(double[,] matrix1, double[,] matrix2) {  
  2.   // cahing matrix lengths for better performance  
  3.   var matrix1Rows = matrix1.GetLength(0);  
  4.   var matrix1Cols = matrix1.GetLength(1);  
  5.   var matrix2Rows = matrix2.GetLength(0);  
  6.   var matrix2Cols = matrix2.GetLength(1);  
  7.   
  8.   // checking if product is defined  
  9.   if (matrix1Cols != matrix2Rows)  
  10.     throw new InvalidOperationException  
  11.       ("Product is undefined. n columns of first matrix must equal to n rows of second matrix");  
  12.   
  13.   // creating the final product matrix  
  14.   double[,] product = new double[matrix1Rows, matrix2Cols];  
  15.   
  16.   // looping through matrix 1 rows  
  17.   for (int matrix1_row = 0; matrix1_row < matrix1Rows; matrix1_row++) {  
  18.     // for each matrix 1 row, loop through matrix 2 columns  
  19.     for (int matrix2_col = 0; matrix2_col < matrix2Cols; matrix2_col++) {  
  20.       // loop through matrix 1 columns to calculate the dot product  
  21.       for (int matrix1_col = 0; matrix1_col < matrix1Cols; matrix1_col++) {  
  22.         product[matrix1_row, matrix2_col] +=   
  23.           matrix1[matrix1_row, matrix1_col] *   
  24.           matrix2[matrix1_col, matrix2_col];  
  25.       }  
  26.     }  
  27.   }  
  28.   
  29.   return product;  
  30. }  
We started by fetching matrix row and column counts using Array.GetLength() and stored them inside variables to use them later. There’s a performance hit when calling Array.GetLength() that’s why we stored its results inside variables rather than calling the function multiple times. The performance part of this code is covered later in this article.
 
Next, we guaranteed that the product is defined by comparing the matrix1 number of columns to the matrix2 number of rows. An exception is thrown if the product is undefined.
 
Photo Credit:  MathwareHouse
 
Then we created the final product matrix using the row and column lengths of the original matrices. 
 
After that, we used three loops to move through matrix vectors and to calculate the dot product.
 
Photo Credit: PurpleMath

Transformations

 
Now we can use our multiplication algorithm to create image transformation matrices that can be applied to any point (X, Y) or color (ARGB) to modify it. We will start by defining our abstract IImageTransformation interface that has two members: CreateTransformationMatrix() and IsColorTransformation. The first one returns the relevant transformation matrix, the second indicates if this transformation can be applied to colors (true) or points (false).
  1. public interface IImageTransformation {  
  2.   double[,] CreateTransformationMatrix();  
  3.   
  4.   bool IsColorTransformation { get; }  
  5. }  
Rotation Transformation
 
The 2D rotation matrix is defined as:
 
Photo Credit: Academo 
 
Our code is very clear:
  1. public class RotationImageTransformation : IImageTransformation {  
  2.   public double AngleDegrees { getset; }  
  3.   public double AngleRadians {  
  4.     get { return DegreesToRadians(AngleDegrees); }  
  5.     set { AngleDegrees = RadiansToDegrees(value); }  
  6.   }  
  7.   public bool IsColorTransformation { get { return false; } }  
  8.   
  9.   public static double DegreesToRadians(double degree)  
  10.       { return degree * Math.PI / 180; }  
  11.   public static double RadiansToDegrees(double radians)  
  12.       { return radians / Math.PI * 180; }  
  13.   
  14.   public double[,] CreateTransformationMatrix() {  
  15.     double[,] matrix = new double[2, 2];  
  16.   
  17.     matrix[0, 0] = Math.Cos(AngleRadians);  
  18.     matrix[1, 0] = Math.Sin(AngleRadians);  
  19.     matrix[0, 1] = -1 * Math.Sin(AngleRadians);  
  20.     matrix[1, 1] = Math.Cos(AngleRadians);  
  21.   
  22.     return matrix;  
  23.   }  
  24.   
  25.   public RotationImageTransformation() { }  
  26.   public RotationImageTransformation(double angleDegree) {  
  27.     this.AngleDegrees = angleDegree;  
  28.   }  
  29. }    
As you can see in this code, Sin() and Cos() accept angels in radians, that’s why we have used two extra functions to convert between radians and degrees to keep things simple to the user.
 
A very nice explanation and example of 2D rotation matrices is available here.
 
Stretching/Scaling Transformation
 
The second transformation we have is the factor-scaling transformation. It works by scaling X/Y by the required factor. It is defined as:
  1. public class StretchImageTransformation : IImageTransformation {  
  2.   public double HorizontalStretch { getset; }  
  3.   public double VerticalStretch { getset; }  
  4.   
  5.   public bool IsColorTransformation { get { return false; } }  
  6.   
  7.   public double[,] CreateTransformationMatrix() {  
  8.     double[,] matrix = Matrices.CreateIdentityMatrix(2);  
  9.   
  10.     matrix[0, 0] += HorizontalStretch;  
  11.     matrix[1, 1] += VerticalStretch;  
  12.   
  13.     return matrix;  
  14.   }  
  15.   
  16.   public StretchImageTransformation() { }  
  17.   public StretchImageTransformation(double horizStretch, double vertStretch) {  
  18.     this.HorizontalStretch = horizStretch;  
  19.     this.VerticalStretch = vertStretch;  
  20.   }  
  21. }  
Identity Matrix
 
The previous code requires the use of an identity matrix. Here’s the code that defines CreateIdentityMatrix(),
  1. public static double[,] CreateIdentityMatrix(int length) {  
  2.   double[,] matrix = new double[length, length];  
  3.   
  4.   for (int i = 0, j = 0; i < length; i++, j++)  
  5.     matrix[i, j] = 1;  
  6.   
  7.   return matrix;  
  8. }   
Flipping Transformation
 
The third transformation we have is the flipping transformation. It works by negating the X and Y members to flip the vector over the vertical and horizontal axis respectively.
  1. public class FlipImageTransformation : IImageTransformation {  
  2.   public bool FlipHorizontally { getset; }  
  3.   public bool FlipVertically { getset; }  
  4.   public bool IsColorTransformation { get { return false; } }  
  5.   
  6.   public double[,] CreateTransformationMatrix() {  
  7.     // identity matrix  
  8.     double[,] matrix = Matrices.CreateIdentityMatrix(2);  
  9.   
  10.     if (FlipHorizontally)  
  11.       matrix[0, 0] *= -1;  
  12.     if (FlipVertically)  
  13.       matrix[1, 1] *= -1;  
  14.   
  15.     return matrix;  
  16.   }  
  17.   
  18.   public FlipImageTransformation() { }  
  19.   public FlipImageTransformation(bool flipHoriz, bool flipVert) {  
  20.     this.FlipHorizontally = flipHoriz;  
  21.     this.FlipVertically = flipVert;  
  22.   }  
  23. }  
Color Density Transformation
 
The last transformation we have is the color density transformation. It works by defining different scaling factors to color components (Alpha, Red, Green, and Blue). For example, if you want to make the color 50% transparent we would scale Alpha by 0.5. If you want to remove the Red color completely you could scale it by 0. And so on.
  1. public class DensityImageTransformation : IImageTransformation {  
  2.   public double AlphaDensity { getset; }  
  3.   public double RedDensity { getset; }  
  4.   public double GreenDensity { getset; }  
  5.   public double BlueDensity { getset; }  
  6.   public bool IsColorTransformation { get { return true; } }  
  7.   
  8.   public double[,] CreateTransformationMatrix() {  
  9.     // identity matrix  
  10.     double[,] matrix = new double[,]{  
  11.       { AlphaDensity, 0, 0, 0},  
  12.       { 0, RedDensity, 0, 0},  
  13.       { 0, 0, GreenDensity, 0},  
  14.       { 0, 0, 0, BlueDensity},  
  15.     };  
  16.   
  17.     return matrix;  
  18.   }  
  19.   
  20.   public DensityImageTransformation() { }  
  21.   public DensityImageTransformation(double alphaDensity,   
  22.     double redDensity,   
  23.     double greenDensity,   
  24.     double blueDensity) {  
  25.     this.AlphaDensity = alphaDensity;  
  26.     this.RedDensity = redDensity;  
  27.     this.GreenDensity = greenDensity;  
  28.     this.BlueDensity = blueDensity;  
  29.   }  
  30. }  

Connecting Things Together

 
Now it is time to define the processes and procedures that connect things together. Here’s the full code. An explanation follows:
  1. /// <summary>  
  2. /// Applies image transformations to an image file  
  3. /// </summary>  
  4. public static Bitmap Apply(string file, IImageTransformation[] transformations) {  
  5.   using (Bitmap bmp = (Bitmap)Bitmap.FromFile(file)) {  
  6.     return Apply(bmp, transformations);  
  7.   }  
  8. }  
  9.   
  10. /// <summary>  
  11. /// Applies image transformations bitmap object  
  12. /// </summary>  
  13. public static Bitmap Apply(Bitmap bmp, IImageTransformation[] transformations) {  
  14.   // defining an array to store new image data  
  15.   PointColor[] points = new PointColor[bmp.Width * bmp.Height];  
  16.   
  17.   // filtering transformations  
  18.   var pointTransformations =  
  19.     transformations.Where(s => s.IsColorTransformation == false).ToArray();  
  20.   var colorTransformations =  
  21.     transformations.Where(s => s.IsColorTransformation == true).ToArray();  
  22.   
  23.   double[,] pointTransMatrix =  
  24.     CreateTransformationMatrix(pointTransformations, 2); // x, y  
  25.   double[,] colorTransMatrix =  
  26.     CreateTransformationMatrix(colorTransformations, 4); // a, r, g, b  
  27.   
  28.   // saving some stats to adjust the image later  
  29.   int minX = 0, minY = 0;  
  30.   int maxX = 0, maxY = 0;  
  31.   
  32.   // scanning points and applying transformations  
  33.   int idx = 0;  
  34.   for (int x = 0; x < bmp.Width; x++) { // row by row  
  35.     for (int y = 0; y < bmp.Height; y++) { // column by column  
  36.   
  37.       // applying the point transformations  
  38.       var product =  
  39.         Matrices.Multiply(pointTransMatrix, new double[,] { { x }, { y } });  
  40.   
  41.       var newX = (int)product[0, 0];  
  42.       var newY = (int)product[1, 0];  
  43.   
  44.       // saving stats  
  45.       minX = Math.Min(minX, newX);  
  46.       minY = Math.Min(minY, newY);  
  47.       maxX = Math.Max(maxX, newX);  
  48.       maxY = Math.Max(maxY, newY);  
  49.   
  50.       // applying color transformations  
  51.       Color clr = bmp.GetPixel(x, y); // current color  
  52.       var colorProduct = Matrices.Multiply(  
  53.         colorTransMatrix,  
  54.         new double[,] { { clr.A }, { clr.R }, { clr.G }, { clr.B } });  
  55.       clr = Color.FromArgb(  
  56.         GetValidColorComponent(colorProduct[0, 0]),  
  57.         GetValidColorComponent(colorProduct[1, 0]),  
  58.         GetValidColorComponent(colorProduct[2, 0]),  
  59.         GetValidColorComponent(colorProduct[3, 0])  
  60.         ); // new color  
  61.   
  62.       // storing new data  
  63.       points[idx] = new PointColor() {  
  64.         X = newX,  
  65.         Y = newY,  
  66.         Color = clr  
  67.       };  
  68.   
  69.       idx++;  
  70.     }  
  71.   }  
  72.   
  73.   // new bitmap width and height  
  74.   var width = maxX - minX + 1;  
  75.   var height = maxY - minY + 1;  
  76.   
  77.   // new image  
  78.   var img = new Bitmap(width, height);  
  79.   foreach (var pnt in points)  
  80.     img.SetPixel(  
  81.       pnt.X - minX,  
  82.       pnt.Y - minY,  
  83.       pnt.Color);  
  84.   
  85.   return img;  
  86. }  
  87.   
  88. /// <summary>  
  89. /// Returns color component between 0 and 255  
  90. /// </summary>  
  91. private static byte GetValidColorComponent(double c) {  
  92.   c = Math.Max(byte.MinValue, c);  
  93.   c = Math.Min(byte.MaxValue, c);  
  94.   return (byte)c;  
  95. }  
  96.   
  97. /// <summary>  
  98. /// Combines transformations to create single transformation matrix  
  99. /// </summary>  
  100. private static double[,] CreateTransformationMatrix  
  101.   (IImageTransformation[] vectorTransformations, int dimensions) {  
  102.   double[,] vectorTransMatrix =  
  103.     Matrices.CreateIdentityMatrix(dimensions);  
  104.   
  105.   // combining transformations works by multiplying them  
  106.   foreach (var trans in vectorTransformations)  
  107.     vectorTransMatrix =  
  108.       Matrices.Multiply(vectorTransMatrix, trans.CreateTransformationMatrix());  
  109.   
  110.   return vectorTransMatrix;  
  111. } 
We started by defining two overloads of Apply() function. One that accepts image file name and transformation list and the other accepts a Bitmap object and the transformation list to apply to that image.
 
Inside the Apply() function, we filtered transformations into two groups, those that work on point locations (X and Y) and those that work on colors. We also used the CreateTransformationMatrix() function for each group to combine the transformations into a single transformation matrix.
 
After that, we started scanning the image and applying the transformations to points and colors respectively. Notice that we had to ensure that the transformed color components are byte-sized. After applying the transformations we saved data in an array for later usage.
 
During the scanning process, we recorded our minimum and maximum X and Y values. This will help to set the new image size and shift the points as needed. Some transformations like stretching might increase or decrease image size.
 
Finally, we created the new Bitmap object and set the point data after shifting them.
 

Creating the Client

 
Our client application is simple. Here’s a screenshot of our form,
 
 
Let’s have a look at the code behind it:
  1. private string _file;  
  2. private Stopwatch _stopwatch;  
  3.   
  4.   
  5. public ImageTransformationsForm() {  
  6.   InitializeComponent();  
  7. }  
  8.   
  9. private void BrowseButton_Click(object sender, EventArgs e) {  
  10.   string file = OpenFile();  
  11.   if (file != null) {  
  12.     this.FileTextBox.Text = file;  
  13.   
  14.     _file = file;  
  15.   }  
  16. }  
  17.   
  18. public static string OpenFile() {  
  19.   OpenFileDialog dlg = new OpenFileDialog();  
  20.   dlg.CheckFileExists = true;  
  21.   
  22.   if (dlg.ShowDialog() == DialogResult.OK)  
  23.     return dlg.FileName;  
  24.   
  25.   return null;  
  26. }  
  27.   
  28. private void ApplyButton_Click(object sender, EventArgs e) {  
  29.   if (_file == null)  
  30.     return;  
  31.   
  32.   DisposePreviousImage();  
  33.   
  34.   RotationImageTransformation rotation =  
  35.     new RotationImageTransformation((double)this.AngleNumericUpDown.Value);  
  36.   StretchImageTransformation stretch =  
  37.     new StretchImageTransformation(  
  38.       (double)this.HorizStretchNumericUpDown.Value / 100,  
  39.       (double)this.VertStretchNumericUpDown.Value / 100);  
  40.   FlipImageTransformation flip =  
  41.     new FlipImageTransformation(this.FlipHorizontalCheckBox.Checked, this.FlipVerticalCheckBox.Checked);  
  42.   
  43.   DensityImageTransformation density =  
  44.     new DensityImageTransformation(  
  45.       (double)this.AlphaNumericUpDown.Value / 100,  
  46.       (double)this.RedNumericUpDown.Value / 100,  
  47.       (double)this.GreenNumericUpDown.Value / 100,  
  48.       (double)this.BlueNumericUpDown.Value / 100  
  49.     );  
  50.   
  51.   StartStopwatch();  
  52.   var bmp = ImageTransformer.Apply(_file,  
  53.     new IImageTransformation[] { rotation, stretch, flip, density });  
  54.   StopStopwatch();  
  55.   
  56.   this.ImagePictureBox.Image = bmp;  
  57. }  
  58.   
  59.   
  60. private void StartStopwatch() {  
  61.   if (_stopwatch == null)  
  62.     _stopwatch = new Stopwatch();  
  63.   else  
  64.     _stopwatch.Reset();  
  65.   
  66.   _stopwatch.Start();  
  67. }  
  68.   
  69.   
  70. private void StopStopwatch() {  
  71.   _stopwatch.Stop();  
  72.   this.ExecutionTimeLabel.Text = $"Total execution time is {_stopwatch.ElapsedMilliseconds} milliseconds";  
  73. }  
  74.   
  75. private void DisposePreviousImage() {  
  76.   if (this.ImagePictureBox.Image != null) {  
  77.     var tmpImg = this.ImagePictureBox.Image;  
  78.     this.ImagePictureBox.Image = null;  
  79.     tmpImg.Dispose();  
  80.   }  
  81. }   
The code is straightforward. The only thing to mention is that it has always been a good practice to call Dispose() on disposable objects to ensure best performance.
 

Performance Notes

 
In our core Multiply() method, we mentioned that calling Array.GetLength() involves a huge performance impact. I tried to check the logic behind Array.GetLength() with no success. The method is natively implemented, and I could not view its code using common disassembly tools. However, by benchmarking the two scenarios (code with a bunch of calls to Array.GetLength() and another code with only a single call to the same function) I found that the single call code is 2x faster than the other.
 
Another way to improve the performance of our Multiply() method is to use unsafe code. By accessing array contents directly you achieve superior processing performance.
 
Here’s our new and updated unsafe code:
  1. public static double[,] MultiplyUnsafe(double[,] matrix1, double[,] matrix2) {  
  2.   // cahing matrix lengths for better performance  
  3.   var matrix1Rows = matrix1.GetLength(0);  
  4.   var matrix1Cols = matrix1.GetLength(1);  
  5.   var matrix2Rows = matrix2.GetLength(0);  
  6.   var matrix2Cols = matrix2.GetLength(1);  
  7.   
  8.   // checking if product is defined  
  9.   if (matrix1Cols != matrix2Rows)  
  10.     throw new InvalidOperationException  
  11.       ("Product is undefined. n columns of first matrix must equal to n rows of second matrix");  
  12.   
  13.   // creating the final product matrix  
  14.   double[,] product = new double[matrix1Rows, matrix2Cols];  
  15.   
  16.   unsafe  
  17.   {  
  18.     // fixing pointers to matrices  
  19.     fixed (  
  20.       double* pProduct = product,  
  21.       pMatrix1 = matrix1,  
  22.       pMatrix2 = matrix2) {  
  23.   
  24.       int i = 0;  
  25.       // looping through matrix 1 rows  
  26.       for (int matrix1_row = 0; matrix1_row < matrix1Rows; matrix1_row++) {  
  27.         // for each matrix 1 row, loop through matrix 2 columns  
  28.         for (int matrix2_col = 0; matrix2_col < matrix2Cols; matrix2_col++) {  
  29.           // loop through matrix 1 columns to calculate the dot product  
  30.           for (int matrix1_col = 0; matrix1_col < matrix1Cols; matrix1_col++) {  
  31.   
  32.             var val1 = *(pMatrix1 + (matrix1Rows * matrix1_row) + matrix1_col);  
  33.             var val2 = *(pMatrix2 + (matrix2Cols * matrix1_col) + matrix2_col);  
  34.   
  35.             *(pProduct + i) += val1 * val2;  
  36.   
  37.           }  
  38.   
  39.           i++;  
  40.   
  41.         }  
  42.       }  
  43.   
  44.     }  
  45.   }  
  46.   
  47.   return product;  
  48. }  
Unsafe code will not compile unless you enable it from the Build tab in the Project Settings page.
 
The following figure shows the difference between the three Multiply() scenarios when multiplying the 1000x1000 matrix by itself. The tests ran on my dying Core [email protected] 6GB RAM 1GB Intel Graphics laptop.
 
 
I am not covering any performance improvements in the client or the Apply() method as it is not the core focus of the article.
 

Conclusion

 
This was my implementation of matrix multiplication. Feel free to send me your feedback and comments over the code and to update it as needed.


Similar Articles