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.
Point[] wLevelPts = new Point[256];
Point Origin = new Point(labelX0.Left, labelY0.Bottom);
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));
mxWtoC = new Matrix(1, 0, 0, -1, 0, 0);
mxWtoC.Scale((float)(labelX2.Right - labelX0.Left) / 255f, (float)(labelY0.Bottom
- labelY2.Top) / 255f);
mxWtoC.Translate(Origin.X, Origin.Y, MatrixOrder.Append);
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;
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;
double Y = (1.0 - t) * (1.0 - t) * b[0] + 2.0 * t *
(1 - t) * b[1] + t * t * b[2];
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 };
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;
pt1 = ControlToWorkspace(new Point(x, y));
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 (e.Button == MouseButtons.Left && (pts[2].X - pts[1].X) > 2
&& r1.Contains(new Point(e.X, e.Y)))
{
isCpt1 = true;
}
if (e.Button == MouseButtons.Left && (pts[3].X - pts[2].X) > 2
&& r2.Contains(new Point(e.X, e.Y)))
{
isCpt2 = true;
}
}
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));
}
... ...
private void labelPt3_MouseUp(object sender, MouseEventArgs e)
{
... ...
getLevelbytes();
OnLevelChanged(new LevelChangedEventArgs(LevelValue));
}
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.
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
... ...
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:
for (int i = scanStart; i < scanEnd + 1; i++)
{
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.