Adjusting an Image Curve with C#
Introducing my second user control for image editing.
Introduction
In image editing, a curve is a remapping of image tonality, specified as a function from the input level to the output level, used as a way to emphasize colors or other elements in a picture. Here is a user control written in C# to adjust image curves. We know C# provides a method to draw curves, but I don’t know how to get the coordinates of any point on the curve drawn using DrawCurve
.
1. How to get the point coordinate on a curve
I designed a work space with a size 255 X 255, and set up 256 points with the x-axis representing the input level (0 to 255) and the y-axis representing the output level (0 to 255). And, I used DrawLines
to get a curve (actually a polyline). The work space was transformed to a user control by the Matrix
class.
// 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();
I used the function B(t) = (1 - t)^2*p0 + 2t(1 - t)*p1 + t^2*p2, 0 < t < 1
to construct a quadratic Bezier curve with three points p0
, p1
, p2
, and got all the points that represented the image pixel input and output level on the curve:
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. How to change the curve on the user control
The curve was constructed using three points. To change the curve, these three points must be moved by the user. I had two endpoints for the quadratic Bezier curve which could be moved by moving the label controls, the small black squares on the user control.
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;
... ...
}
And, the middle point to control the quadratic Bezier curve was directly got form this user control's mouse event:
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;
... ...
}
For using this user control to adjust the image curve, I wrote a custom event, LevelChanged
:
public class LevelChangedEventArgs : EventArgs
{
private byte[] levelValue;
public LevelChangedEventArgs(byte[] LevelValue)
{
levelValue = LevelValue;
}
public byte[] LevelValue
{
get { return levelValue; }
}
}
And, it is called in the MouseUp
event for moving the control points that construct the curve. When the user changes the curve and the mouse is up, the event LevelChanged
will be triggered.
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. How to get an image pixel and change its level
I didn’t use the Getpixel
and Setpixel
methods just because they are very slow. I used the Bitmap.LockBits
method to lock the image and performed the pixel level modifications directly on the RGB data in memory.
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);
Also, I did not use "unsafe
" code and pointers. But, I tried the System.Runtime.InteropServices.Marshal.Copy
method to copy bytes to and from the location of the image in memory. It worked very well.
// 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);
I tried using two for…
loops to reach the selected pixels, but it was much slower than using one for…
loop plus a conditional if…
statement:
// 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]];
}
}
Finally, I used my own ImagePanel
to display the image in which the curve level was being changed and to select the part of the image to be adjusted.
Any suggestion is appreciated.