使用 C# 调整图像曲线
介绍我的第二个图像编辑用户控件。
引言
在图像编辑中,曲线是对图像色调的一种重新映射,以输入级到输出级的函数形式指定,用作强调图像中的颜色或其他元素的一种方式。这里有一个用 C# 编写的用户控件,用于调整图像曲线。我们知道 C# 提供了一种绘制曲线的方法,但我不知道如何获取使用 DrawCurve
绘制的曲线上任何点的坐标。
1. 如何获取曲线上点的坐标
我设计了一个大小为 255 X 255 的工作空间,并设置了 256 个点,其中 x 轴表示输入级(0 到 255),y 轴表示输出级(0 到 255)。并且,我使用 DrawLines
获取一条曲线(实际上是一条多边形线)。工作空间通过 Matrix
类转换为用户控件。
// set up points
Point[] wLevelPts = new Point[256];
// setup work space origin
Point Origin = new Point(labelX0.Left, labelY0.Bottom);
// Work Space
Point wsPt = new Point(labelX0.Left - 1, labelY2.Top - 1);
int wsWidth = labelX2.Right - labelX0.Left + 2;
int wsHeight = labelY0.Bottom - labelY2.Top + 2;
workSpace = new Rectangle(wsPt, new Size(wsWidth, wsHeight));
// transformation from work space to control
mxWtoC = new Matrix(1, 0, 0, -1, 0, 0);//reflect across x-axis
mxWtoC.Scale((float)(labelX2.Right - labelX0.Left) / 255f, (float)(labelY0.Bottom
- labelY2.Top) / 255f);
mxWtoC.Translate(Origin.X, Origin.Y, MatrixOrder.Append);
// transformation from control to work space
mxCtoW = mxWtoC.Clone();
mxCtoW.Invert();
我使用函数 B(t) = (1 - t)^2*p0 + 2t(1 - t)*p1 + t^2*p2, 0 < t < 1
来构造一个由三个点 p0
、p1
、p2
组成的二次贝塞尔曲线,并获取了所有表示图像像素输入和输出级在曲线上的点
private void getBezierPoints(Point sPt, Point cPt, Point ePt)
{
wLevelPts[sPt.X].Y = sPt.Y;
if (ePt.X - sPt.X > 2)
{
int aa = ePt.X - sPt.X;
int k = sPt.X;
double[] a = new double[3];
double[] b = new double[3];
a[0] = sPt.X;
a[1] = cPt.X;
a[2] = ePt.X;
b[0] = sPt.Y;
b[1] = cPt.Y;
b[2] = ePt.Y;
int interpolation = 5 * aa;
double tUnit = 1.0 / interpolation;
for (int i = 1; i < interpolation + 1; i++)
{
double t = tUnit * i;
// use function B(t) to get x-coordinate
int X = (int)((1.0 - t) * (1.0 - t) * a[0] + 2.0 * t *
(1 - t) * a[1] + t * t * a[2]);
if (X > k && X < ePt.X)
{
int bb = X - k;
// use function B(t) to get y-coordinate
double Y = (1.0 - t) * (1.0 - t) * b[0] + 2.0 * t *
(1 - t) * b[1] + t * t * b[2];
// if two points not close, do interpolation
for (int j = 1; j < bb + 1; j++)
{
double c = (double)wLevelPts[k].Y * (double)(bb - j) /
(double)bb + Y * (double)j / (double)bb;
if (c < 0) c = 0;
if (c > 255) c = 255;
wLevelPts[k + j].Y = (int)c;
}
k = k + bb;
}
}
}
}
2. 如何更改用户控件上的曲线
曲线是使用三个点构造的。要更改曲线,必须由用户移动这三个点。我为二次贝塞尔曲线的两个端点设置了两个可移动的点,可以通过移动用户控件上的标签控件(小黑方块)来移动它们。
private void labelPt3_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isLblMoving = true;
((Label)sender).Tag = new Point(e.X, e.Y);
}
}
private void labelPt3_MouseMove(object sender, MouseEventArgs e)
{
Point[] lblPts = new Point[] { pt0, pt1, pt2, pt3, pt4 };
// transform points on work space to control
mxWtoC.TransformPoints(lblPts);
if (e.Button == MouseButtons.Left && isLblMoving)
{
Label pt = (Label)sender;
Point p = (Point)pt.Tag;
int x = pt.Left + e.X - p.X;
int y = pt.Top + e.Y - p.Y;
if (y < lblPts[4].Y) y = lblPts[4].Y;
if (y > lblPts[0].Y) y = lblPts[0].Y;
if (pt == labelPt1)
{
if (x > lblPts[2].X) x = lblPts[2].X;
if (x < lblPts[0].X) x = lblPts[0].X;
// get new position on work space
pt1 = ControlToWorkspace(new Point(x, y));
// get points on curve
getLevelPoints(1);
}
if (pt == labelPt2)
{
if (x < lblPts[1].X) x = lblPts[1].X;
if (x > lblPts[3].X) x = lblPts[3].X;
pt2 = ControlToWorkspace(new Point(x, y));
getLevelPoints(2);
}
if (pt == labelPt3)
{
if (x < lblPts[2].X) x = lblPts[2].X;
if (x > lblPts[4].X) x = lblPts[4].X;
pt3 = ControlToWorkspace(new Point(x, y));
getLevelPoints(3);
}
pt.Top = y - 2;
pt.Left = x - 2;
Invalidate();
}
}
private void labelPt3_MouseUp(object sender, MouseEventArgs e)
{
isLblMoving = false;
... ...
}
并且,控制二次贝塞尔曲线的中间点直接从这个用户控件的鼠标事件中获取
private void ImageCurve_MouseDown(object sender, MouseEventArgs e)
{
Point[] pts = new Point[] { pt0, pt1, pt2, pt3, pt4 };
mxWtoC.TransformPoints(pts);
Rectangle r1 = new Rectangle(pts[1].X, pts[4].Y,
pts[2].X - pts[1].X, pts[0].Y - pts[4].Y);
Rectangle r2 = new Rectangle(pts[2].X, pts[4].Y, pts[3].X -
pts[2].X, pts[0].Y - pts[4].Y);
// if between pt1 and pt2, move cPt1
if (e.Button == MouseButtons.Left && (pts[2].X - pts[1].X) > 2
&& r1.Contains(new Point(e.X, e.Y)))
{
isCpt1 = true;//move cPt1
}
// if between pt2 and pt3, move cPt2
if (e.Button == MouseButtons.Left && (pts[3].X - pts[2].X) > 2
&& r2.Contains(new Point(e.X, e.Y)))
{
isCpt2 = true;//move cPt2
}
}
private void ImageCurve_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (isCpt1)
{
cPt1 = ControlToWorkspace(new Point(e.X, e.Y));
getBezierPoints(pt1, cPt1, pt2);
}
if (isCpt2)
{
cPt2 = ControlToWorkspace(new Point(e.X, e.Y));
getBezierPoints(pt2, cPt2, pt3);
}
}
Invalidate();
}
private void ImageCurve_MouseUp(object sender, MouseEventArgs e)
{
if (isCpt1) isCpt1 = false;
if (isCpt2) isCpt2 = false;
... ...
}
为了使用这个用户控件来调整图像曲线,我编写了一个自定义事件,LevelChanged
public class LevelChangedEventArgs : EventArgs
{
private byte[] levelValue;
public LevelChangedEventArgs(byte[] LevelValue)
{
levelValue = LevelValue;
}
public byte[] LevelValue
{
get { return levelValue; }
}
}
它在移动构造曲线的控制点的 MouseUp
事件中被调用。当用户更改曲线并且鼠标释放时,LevelChanged
事件将被触发。
private void ImageCurve_MouseUp(object sender, MouseEventArgs e)
{
... ...
getLevelbytes();
OnLevelChanged(new LevelChangedEventArgs(LevelValue)); // call event
}
... ...
private void labelPt3_MouseUp(object sender, MouseEventArgs e)
{
... ...
getLevelbytes();
OnLevelChanged(new LevelChangedEventArgs(LevelValue)); // call event
}
3. 如何获取图像像素并更改其级别
我没有使用 Getpixel
和 Setpixel
方法,仅仅是因为它们速度非常慢。我使用了 Bitmap.LockBits
方法来锁定图像,并直接在内存中的 RGB 数据上执行像素级别的修改。
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);
此外,我没有使用 "unsafe
" 代码和指针。但我尝试了 System.Runtime.InteropServices.Marshal.Copy
方法来将字节复制到图像在内存中的位置以及从该位置复制字节。它工作得很好。
// Copy the RGB values into the array.
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
... ...
// Copy the RGB values back to the bitmap
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
我尝试使用两个 for…
循环来访问选定的像素,但它比使用一个 for…
循环加上一个条件 if…
语句慢得多
// I try use for... for... two loops, but it is much slower
// than one loop
for (int i = scanStart; i < scanEnd + 1; i++)//only one loop
{
int w = i % bmpData.Stride;
int p = w % 3;
if (w > bytesStart && w < bytesEnd && p == integer)
{
rgbValues[i] = Levels[rgbValues[i]];
}
}
最后,我使用了我自己的 ImagePanel
来显示正在更改曲线级别的图像,以及选择要调整的图像部分。
任何建议都将不胜感激。