目录
介绍
源代码
矩阵乘法
转换
旋转变换
拉伸/缩放转换
单位矩阵
翻转变换
颜色密度变换
将事物连接在一起
创建客户端
性能说明
结论
介绍今天,我将向您展示我的矩阵乘法的C#实现以及如何使用它对图像应用基本变换,如旋转、拉伸、翻转和修改颜色密度。
请注意,这不是图像处理类。相反,本文在C#中演示了三个核心线性代数概念:矩阵乘法、点积和变换矩阵。
源代码本文的源代码可在以下存储库的GitHub上获得:https : //github.com/elsheimy/Elsheimy.Samples.LinearTransformations
此实现也包含在线性代数问题组件Elsheimy.Components.Linears中,可在以下位置获得:
- GitHub: https://github.com/elsheimy/Elsheimy.Components.Linears
- NuGet: https://www.nuget.org/packages/Elsheimy.Components.Linears
矩阵乘法背后的数学非常简单。可以在此处和此处找到非常简单的解释 。
让我们直接进入代码并从我们的主要函数开始:
public static double[,] Multiply(double[,] matrix1, double[,] matrix2) {
// cahing matrix lengths for better performance
var matrix1Rows = matrix1.GetLength(0);
var matrix1Cols = matrix1.GetLength(1);
var matrix2Rows = matrix2.GetLength(0);
var matrix2Cols = matrix2.GetLength(1);
// checking if product is defined
if (matrix1Cols != matrix2Rows)
throw new InvalidOperationException
("Product is undefined. n columns of first matrix must equal to n rows of second matrix");
// creating the final product matrix
double[,] product = new double[matrix1Rows, matrix2Cols];
// looping through matrix 1 rows
for (int matrix1_row = 0; matrix1_row < matrix1Rows; matrix1_row++) {
// for each matrix 1 row, loop through matrix 2 columns
for (int matrix2_col = 0; matrix2_col < matrix2Cols; matrix2_col++) {
// loop through matrix 1 columns to calculate the dot product
for (int matrix1_col = 0; matrix1_col < matrix1Cols; matrix1_col++) {
product[matrix1_row, matrix2_col] +=
matrix1[matrix1_row, matrix1_col] *
matrix2[matrix1_col, matrix2_col];
}
}
}
return product;
}
我们首先使用Array.GetLength()获取矩阵行数和列数,并将它们存储在变量中以备后用。调用Array.GetLength()会导致性能下降,这就是为什么我们将其结果存储在变量中而不是多次调用该函数的原因。此代码的性能部分将在本文后面介绍。
接下来,我们保证通过比较矩阵1的列数和矩阵2的行数来定义乘积。如果产品未定义,则抛出异常。
图片来源: MathwareHouse
然后我们使用原始矩阵的行和列长度创建最终产品矩阵。
之后,我们使用了三个循环来遍历矩阵向量并计算点积。
图片来源: PurpleMath
转换现在我们可以使用我们的乘法算法来创建可以应用于任何点 (X, Y)或颜色 (ARGB)的图像变换矩阵来修改它。我们将首先定义具有两个成员的抽象IImageTransformation接口:CreateTransformationMatrix()和IsColorTransformation。第一个返回相关的变换矩阵,第二个指示此变换是否可以应用于颜色(真)或点(假)。
public interface IImageTransformation {
double[,] CreateTransformationMatrix();
bool IsColorTransformation { get; }
}
二维旋转矩阵定义为:
图片来源: Academo
我们的代码非常清晰:
public class RotationImageTransformation : IImageTransformation {
public double AngleDegrees { get; set; }
public double AngleRadians {
get { return DegreesToRadians(AngleDegrees); }
set { AngleDegrees = RadiansToDegrees(value); }
}
public bool IsColorTransformation { get { return false; } }
public static double DegreesToRadians(double degree)
{ return degree * Math.PI / 180; }
public static double RadiansToDegrees(double radians)
{ return radians / Math.PI * 180; }
public double[,] CreateTransformationMatrix() {
double[,] matrix = new double[2, 2];
matrix[0, 0] = Math.Cos(AngleRadians);
matrix[1, 0] = Math.Sin(AngleRadians);
matrix[0, 1] = -1 * Math.Sin(AngleRadians);
matrix[1, 1] = Math.Cos(AngleRadians);
return matrix;
}
public RotationImageTransformation() { }
public RotationImageTransformation(double angleDegree) {
this.AngleDegrees = angleDegree;
}
}
正如你在这段代码中看到的,Sin()和Cos()接受以弧度表示的角度,这就是为什么我们使用了两个额外的函数来在弧度和度数之间进行转换,以保持对用户的简单。
2D旋转矩阵的非常好的解释和示例可 在此处获得。
拉伸/缩放转换我们拥有的第二个转换是因子缩放转换。它通过按所需因子缩放X/Y来工作。它被定义为:
public class StretchImageTransformation : IImageTransformation {
public double HorizontalStretch { get; set; }
public double VerticalStretch { get; set; }
public bool IsColorTransformation { get { return false; } }
public double[,] CreateTransformationMatrix() {
double[,] matrix = Matrices.CreateIdentityMatrix(2);
matrix[0, 0] += HorizontalStretch;
matrix[1, 1] += VerticalStretch;
return matrix;
}
public StretchImageTransformation() { }
public StretchImageTransformation(double horizStretch, double vertStretch) {
this.HorizontalStretch = horizStretch;
this.VerticalStretch = vertStretch;
}
}
前面的代码需要使用单位矩阵。这是定义在CreateIdentityMatrix()中的代码,
public static double[,] CreateIdentityMatrix(int length) {
double[,] matrix = new double[length, length];
for (int i = 0, j = 0; i < length; i++, j++)
matrix[i, j] = 1;
return matrix;
}
我们拥有的第三个转换是翻转转换。它通过否定X和Y成员来分别在垂直和水平轴上翻转向量。
public class FlipImageTransformation : IImageTransformation {
public bool FlipHorizontally { get; set; }
public bool FlipVertically { get; set; }
public bool IsColorTransformation { get { return false; } }
public double[,] CreateTransformationMatrix() {
// identity matrix
double[,] matrix = Matrices.CreateIdentityMatrix(2);
if (FlipHorizontally)
matrix[0, 0] *= -1;
if (FlipVertically)
matrix[1, 1] *= -1;
return matrix;
}
public FlipImageTransformation() { }
public FlipImageTransformation(bool flipHoriz, bool flipVert) {
this.FlipHorizontally = flipHoriz;
this.FlipVertically = flipVert;
}
}
我们拥有的最后一个转换是颜色密度转换。它的工作原理是为颜色分量(Alpha、红色、绿色和蓝色)定义不同的缩放因子。例如,如果您想让颜色透明50%,我们会将Alpha缩放 0.5。如果您想完全去除红色,您可以将其缩放0。依此类推。
public class DensityImageTransformation : IImageTransformation {
public double AlphaDensity { get; set; }
public double RedDensity { get; set; }
public double GreenDensity { get; set; }
public double BlueDensity { get; set; }
public bool IsColorTransformation { get { return true; } }
public double[,] CreateTransformationMatrix() {
// identity matrix
double[,] matrix = new double[,]{
{ AlphaDensity, 0, 0, 0},
{ 0, RedDensity, 0, 0},
{ 0, 0, GreenDensity, 0},
{ 0, 0, 0, BlueDensity},
};
return matrix;
}
public DensityImageTransformation() { }
public DensityImageTransformation(double alphaDensity,
double redDensity,
double greenDensity,
double blueDensity) {
this.AlphaDensity = alphaDensity;
this.RedDensity = redDensity;
this.GreenDensity = greenDensity;
this.BlueDensity = blueDensity;
}
}
现在是定义将事物连接在一起的过程和程序的时候了。这是完整的代码。解释如下:
///
/// Applies image transformations to an image file
///
public static Bitmap Apply(string file, IImageTransformation[] transformations) {
using (Bitmap bmp = (Bitmap)Bitmap.FromFile(file)) {
return Apply(bmp, transformations);
}
}
///
/// Applies image transformations bitmap object
///
public static Bitmap Apply(Bitmap bmp, IImageTransformation[] transformations) {
// defining an array to store new image data
PointColor[] points = new PointColor[bmp.Width * bmp.Height];
// filtering transformations
var pointTransformations =
transformations.Where(s => s.IsColorTransformation == false).ToArray();
var colorTransformations =
transformations.Where(s => s.IsColorTransformation == true).ToArray();
double[,] pointTransMatrix =
CreateTransformationMatrix(pointTransformations, 2); // x, y
double[,] colorTransMatrix =
CreateTransformationMatrix(colorTransformations, 4); // a, r, g, b
// saving some stats to adjust the image later
int minX = 0, minY = 0;
int maxX = 0, maxY = 0;
// scanning points and applying transformations
int idx = 0;
for (int x = 0; x < bmp.Width; x++) { // row by row
for (int y = 0; y < bmp.Height; y++) { // column by column
// applying the point transformations
var product =
Matrices.Multiply(pointTransMatrix, new double[,] { { x }, { y } });
var newX = (int)product[0, 0];
var newY = (int)product[1, 0];
// saving stats
minX = Math.Min(minX, newX);
minY = Math.Min(minY, newY);
maxX = Math.Max(maxX, newX);
maxY = Math.Max(maxY, newY);
// applying color transformations
Color clr = bmp.GetPixel(x, y); // current color
var colorProduct = Matrices.Multiply(
colorTransMatrix,
new double[,] { { clr.A }, { clr.R }, { clr.G }, { clr.B } });
clr = Color.FromArgb(
GetValidColorComponent(colorProduct[0, 0]),
GetValidColorComponent(colorProduct[1, 0]),
GetValidColorComponent(colorProduct[2, 0]),
GetValidColorComponent(colorProduct[3, 0])
); // new color
// storing new data
points[idx] = new PointColor() {
X = newX,
Y = newY,
Color = clr
};
idx++;
}
}
// new bitmap width and height
var width = maxX - minX + 1;
var height = maxY - minY + 1;
// new image
var img = new Bitmap(width, height);
foreach (var pnt in points)
img.SetPixel(
pnt.X - minX,
pnt.Y - minY,
pnt.Color);
return img;
}
///
/// Returns color component between 0 and 255
///
private static byte GetValidColorComponent(double c) {
c = Math.Max(byte.MinValue, c);
c = Math.Min(byte.MaxValue, c);
return (byte)c;
}
///
/// Combines transformations to create single transformation matrix
///
private static double[,] CreateTransformationMatrix
(IImageTransformation[] vectorTransformations, int dimensions) {
double[,] vectorTransMatrix =
Matrices.CreateIdentityMatrix(dimensions);
// combining transformations works by multiplying them
foreach (var trans in vectorTransformations)
vectorTransMatrix =
Matrices.Multiply(vectorTransMatrix, trans.CreateTransformationMatrix());
return vectorTransMatrix;
}
我们首先定义了Apply()函数的两个重载。一个接受图像文件名和转换列表,另一个接受位图对象和转换列表以应用于该图像。
在Apply()函数中,我们将变换过滤为两组,一组处理点位置(X和Y),一组处理颜色。我们还为每个组使用了CreateTransformationMatrix()函数,将这些变换组合成一个单一的变换矩阵。
之后,我们开始扫描图像并将变换分别应用于点和颜色。请注意,我们必须确保转换后的颜色分量是字节大小的。应用转换后,我们将数据保存在一个数组中以备后用。
在扫描过程中,我们记录了我们的最小和最大X和Y值。这将有助于设置新的图像大小并根据需要移动点。一些转换(如拉伸)可能会增加或减少图像大小。
最后,我们创建了新的Bitmap对象并在移动它们后设置点数据。
创建客户端我们的客户端应用程序很简单。这是我们表格的截图,
我们来看看它背后的代码:
private string _file;
private Stopwatch _stopwatch;
public ImageTransformationsForm() {
InitializeComponent();
}
private void BrowseButton_Click(object sender, EventArgs e) {
string file = OpenFile();
if (file != null) {
this.FileTextBox.Text = file;
_file = file;
}
}
public static string OpenFile() {
OpenFileDialog dlg = new OpenFileDialog();
dlg.CheckFileExists = true;
if (dlg.ShowDialog() == DialogResult.OK)
return dlg.FileName;
return null;
}
private void ApplyButton_Click(object sender, EventArgs e) {
if (_file == null)
return;
DisposePreviousImage();
RotationImageTransformation rotation =
new RotationImageTransformation((double)this.AngleNumericUpDown.Value);
StretchImageTransformation stretch =
new StretchImageTransformation(
(double)this.HorizStretchNumericUpDown.Value / 100,
(double)this.VertStretchNumericUpDown.Value / 100);
FlipImageTransformation flip =
new FlipImageTransformation(this.FlipHorizontalCheckBox.Checked, this.FlipVerticalCheckBox.Checked);
DensityImageTransformation density =
new DensityImageTransformation(
(double)this.AlphaNumericUpDown.Value / 100,
(double)this.RedNumericUpDown.Value / 100,
(double)this.GreenNumericUpDown.Value / 100,
(double)this.BlueNumericUpDown.Value / 100
);
StartStopwatch();
var bmp = ImageTransformer.Apply(_file,
new IImageTransformation[] { rotation, stretch, flip, density });
StopStopwatch();
this.ImagePictureBox.Image = bmp;
}
private void StartStopwatch() {
if (_stopwatch == null)
_stopwatch = new Stopwatch();
else
_stopwatch.Reset();
_stopwatch.Start();
}
private void StopStopwatch() {
_stopwatch.Stop();
this.ExecutionTimeLabel.Text = $"Total execution time is {_stopwatch.ElapsedMilliseconds} milliseconds";
}
private void DisposePreviousImage() {
if (this.ImagePictureBox.Image != null) {
var tmpImg = this.ImagePictureBox.Image;
this.ImagePictureBox.Image = null;
tmpImg.Dispose();
}
}
代码很简单。唯一要提到的是,在一次性对象上调用Dispose()以确保最佳性能一直是一个很好的做法。
性能说明在我们的核心Multiply()方法中,我们提到调用Array.GetLength()会产生巨大的性能影响。我试图检查Array.GetLength()背后的逻辑,但没有成功。该方法是本地实现的,我无法使用常见的反汇编工具查看其代码。但是,通过对这两个场景进行基准测试(对Array.GetLength()进行大量调用的代码和对同一函数仅进行一次调用的另一段代码),我发现单次调用的代码比另一个快2倍。
另一种提高Multiply()方法性能的方法是使用不安全代码。通过直接访问数组内容,您可以获得卓越的处理性能。
这是我们新的和更新的不安全 代码:
public static double[,] MultiplyUnsafe(double[,] matrix1, double[,] matrix2) {
// cahing matrix lengths for better performance
var matrix1Rows = matrix1.GetLength(0);
var matrix1Cols = matrix1.GetLength(1);
var matrix2Rows = matrix2.GetLength(0);
var matrix2Cols = matrix2.GetLength(1);
// checking if product is defined
if (matrix1Cols != matrix2Rows)
throw new InvalidOperationException
("Product is undefined. n columns of first matrix must equal to n rows of second matrix");
// creating the final product matrix
double[,] product = new double[matrix1Rows, matrix2Cols];
unsafe
{
// fixing pointers to matrices
fixed (
double* pProduct = product,
pMatrix1 = matrix1,
pMatrix2 = matrix2) {
int i = 0;
// looping through matrix 1 rows
for (int matrix1_row = 0; matrix1_row < matrix1Rows; matrix1_row++) {
// for each matrix 1 row, loop through matrix 2 columns
for (int matrix2_col = 0; matrix2_col < matrix2Cols; matrix2_col++) {
// loop through matrix 1 columns to calculate the dot product
for (int matrix1_col = 0; matrix1_col < matrix1Cols; matrix1_col++) {
var val1 = *(pMatrix1 + (matrix1Rows * matrix1_row) + matrix1_col);
var val2 = *(pMatrix2 + (matrix2Cols * matrix1_col) + matrix2_col);
*(pProduct + i) += val1 * val2;
}
i++;
}
}
}
}
return product;
}
除非您从“项目设置”页面的“构建”选项卡中启用,否则不会编译不安全代码。
下图展示了1000x1000矩阵自身相乘时三种Multiply()场景的区别。测试在我 即将死亡的Core i5-2430M@2.4GHz 6GB RAM 1GB Intel Graphics笔记本电脑上运行。
我不会介绍客户端或Apply()方法中的任何性能改进,因为这不是本文的核心重点。
这是我对矩阵乘法的实现。希望对您有所帮助!
https://www.codeproject.com/Articles/5298657/Matrix-Multiplication-in-Csharp-Applying-Transform