Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adjusting an Image Curve with C#

0.00/5 (No votes)
12 Jun 2008 1  
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here