5

I am asking this question as the other one is two years old and not answered accurately.

I'm looking to replicate the PhotoShop effect mentioned in this article in C#. Adobe call it a Color halftone, I think it looks like some sort of rotated CMYK halftone thingy. Either way I don't know how I would do it.

Current code sample is below.

Any ideas?

Color halftone effect

P.S.

This isn't homework. I'm looking to upgrade the comic book effect I have in my OSS project ImageProcessor.

ImageProcessor comic effect

Progress Update.

So here's some code to show what I have done so far...

I can convert to and from CMYK to RGB fairly easily and accurately enough for my needs and also print out a patterned series of ellipses based on the the intensity of each colour component at a series of points.

What I am stuck at just now is rotating the graphics object for each colour so that the points are laid at the angles specified in the code. Can anyone give me some pointers as how to go about that?

public Image ProcessImage(ImageFactory factory)
{
    Bitmap newImage = null;
    Image image = factory.Image;

    try
    {
        int width = image.Width;
        int height = image.Height;

        // These need to be used.
        float cyanAngle = 105f;
        float magentaAngle = 75f;
        float yellowAngle = 90f;
        float keylineAngle = 15f;

        newImage = new Bitmap(width, height);
        newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

        using (Graphics graphics = Graphics.FromImage(newImage))
        {
            // Reduce the jagged edges.
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
            graphics.CompositingQuality = CompositingQuality.HighQuality;

            graphics.Clear(Color.White);

            using (FastBitmap sourceBitmap = new FastBitmap(image))
            {
                for (int y = 0; y < height; y += 4)
                {
                    for (int x = 0; x < width; x += 4)
                    {
                        Color color = sourceBitmap.GetPixel(x, y);

                        if (color != Color.White)
                        {
                            CmykColor cmykColor = color;
                            float cyanBrushRadius = (cmykColor.C / 100) * 3;
                            graphics.FillEllipse(Brushes.Cyan, x, y, cyanBrushRadius, cyanBrushRadius);

                            float magentaBrushRadius = (cmykColor.M / 100) * 3;
                            graphics.FillEllipse(Brushes.Magenta, x, y, magentaBrushRadius, magentaBrushRadius);

                            float yellowBrushRadius = (cmykColor.Y / 100) * 3;
                            graphics.FillEllipse(Brushes.Yellow, x, y, yellowBrushRadius, yellowBrushRadius);

                            float blackBrushRadius = (cmykColor.K / 100) * 3;
                            graphics.FillEllipse(Brushes.Black, x, y, blackBrushRadius, blackBrushRadius);
                        }
                    }
                }
            }
        }

        image.Dispose();
        image = newImage;
    }
    catch (Exception ex)
    {
        if (newImage != null)
        {
            newImage.Dispose();
        }

        throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex);
    }

    return image;
}
Input Image

input image

Current Output

As you can see since the drawn ellipses are not angled colour output is incorrect.

current output

James South
  • 10,147
  • 4
  • 59
  • 115

1 Answers1

0

So here's a working solution. It ain't pretty, it ain't fast (2 seconds on my laptop) but the output is good. It doesn't exactly match Photoshop's output though I think they are performing some additional work.

Slight moiré patterns sometimes appear on different test images but descreening is out of scope for the current question.

The code performs the following steps.

  1. Loop through the pixels of the image at a given interval
  2. For each colour component, CMYK draw an ellipse at a given point which is calculated by rotating the current point by the set angle. The dimensions of this ellipse are determined by the level of each colour component at each point.
  3. Create a new image by looping though the pixel points and adding the CMYK colour component values at each point to determine the correct colour to draw to the image.

Output image

halftoned image

The code

public Image ProcessImage(ImageFactory factory)
{
    Bitmap cyan = null;
    Bitmap magenta = null;
    Bitmap yellow = null;
    Bitmap keyline = null;
    Bitmap newImage = null;
    Image image = factory.Image;

    try
    {
        int width = image.Width;
        int height = image.Height;

        // Angles taken from Wikipedia page.
        float cyanAngle = 15f;
        float magentaAngle = 75f;
        float yellowAngle = 0f;
        float keylineAngle = 45f;

        int diameter = 4;
        float multiplier = 4 * (float)Math.Sqrt(2);

        // Cyan color sampled from Wikipedia page.
        Brush cyanBrush = new SolidBrush(Color.FromArgb(0, 153, 239));
        Brush magentaBrush = Brushes.Magenta;
        Brush yellowBrush = Brushes.Yellow;
        Brush keylineBrush;

        // Create our images.
        cyan = new Bitmap(width, height);
        magenta = new Bitmap(width, height);
        yellow = new Bitmap(width, height);
        keyline = new Bitmap(width, height);
        newImage = new Bitmap(width, height);

        // Ensure the correct resolution is set.
        cyan.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        magenta.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        yellow.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        keyline.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

        // Check bounds against this.
        Rectangle rectangle = new Rectangle(0, 0, width, height);

        using (Graphics graphicsCyan = Graphics.FromImage(cyan))
        using (Graphics graphicsMagenta = Graphics.FromImage(magenta))
        using (Graphics graphicsYellow = Graphics.FromImage(yellow))
        using (Graphics graphicsKeyline = Graphics.FromImage(keyline))
        {
            // Ensure cleared out.
            graphicsCyan.Clear(Color.Transparent);
            graphicsMagenta.Clear(Color.Transparent);
            graphicsYellow.Clear(Color.Transparent);
            graphicsKeyline.Clear(Color.Transparent);

            // This is too slow. The graphics object can't be called within a parallel 
            // loop so we have to do it old school. :(
            using (FastBitmap sourceBitmap = new FastBitmap(image))
            {
                for (int y = -height * 2; y < height * 2; y += diameter)
                {
                    for (int x = -width * 2; x < width * 2; x += diameter)
                    {
                        Color color;
                        CmykColor cmykColor;
                        float brushWidth;

                        // Cyan
                        Point rotatedPoint = RotatePoint(new Point(x, y), new Point(0, 0), cyanAngle);
                        int angledX = rotatedPoint.X;
                        int angledY = rotatedPoint.Y;
                        if (rectangle.Contains(new Point(angledX, angledY)))
                        {
                            color = sourceBitmap.GetPixel(angledX, angledY);
                            cmykColor = color;
                            brushWidth = diameter * (cmykColor.C / 255f) * multiplier;
                            graphicsCyan.FillEllipse(cyanBrush, angledX, angledY, brushWidth, brushWidth);
                        }

                        // Magenta
                        rotatedPoint = RotatePoint(new Point(x, y), new Point(0, 0), magentaAngle);
                        angledX = rotatedPoint.X;
                        angledY = rotatedPoint.Y;
                        if (rectangle.Contains(new Point(angledX, angledY)))
                        {
                            color = sourceBitmap.GetPixel(angledX, angledY);
                            cmykColor = color;
                            brushWidth = diameter * (cmykColor.M / 255f) * multiplier;
                            graphicsMagenta.FillEllipse(magentaBrush, angledX, angledY, brushWidth, brushWidth);
                        }

                        // Yellow
                        rotatedPoint = RotatePoint(new Point(x, y), new Point(0, 0), yellowAngle);
                        angledX = rotatedPoint.X;
                        angledY = rotatedPoint.Y;
                        if (rectangle.Contains(new Point(angledX, angledY)))
                        {
                            color = sourceBitmap.GetPixel(angledX, angledY);
                            cmykColor = color;
                            brushWidth = diameter * (cmykColor.Y / 255f) * multiplier;
                            graphicsYellow.FillEllipse(yellowBrush, angledX, angledY, brushWidth, brushWidth);
                        }

                        // Keyline 
                        rotatedPoint = RotatePoint(new Point(x, y), new Point(0, 0), keylineAngle);
                        angledX = rotatedPoint.X;
                        angledY = rotatedPoint.Y;
                        if (rectangle.Contains(new Point(angledX, angledY)))
                        {
                            color = sourceBitmap.GetPixel(angledX, angledY);
                            cmykColor = color;
                            brushWidth = diameter * (cmykColor.K / 255f) * multiplier;

                            // Just using blck is too dark. 
                            keylineBrush = new SolidBrush(CmykColor.FromCmykColor(0, 0, 0, cmykColor.K));
                            graphicsKeyline.FillEllipse(keylineBrush, angledX, angledY, brushWidth, brushWidth);
                        }
                    }
                }
            }

            // Set our white background.
            using (Graphics graphics = Graphics.FromImage(newImage))
            {
                graphics.Clear(Color.White);
            }

            // Blend the colors now to mimic adaptive blending.
            using (FastBitmap cyanBitmap = new FastBitmap(cyan))
            using (FastBitmap magentaBitmap = new FastBitmap(magenta))
            using (FastBitmap yellowBitmap = new FastBitmap(yellow))
            using (FastBitmap keylineBitmap = new FastBitmap(keyline))
            using (FastBitmap destinationBitmap = new FastBitmap(newImage))
            {
                Parallel.For(
                    0,
                    height,
                    y =>
                    {
                        for (int x = 0; x < width; x++)
                        {
                            // ReSharper disable AccessToDisposedClosure
                            Color cyanPixel = cyanBitmap.GetPixel(x, y);
                            Color magentaPixel = magentaBitmap.GetPixel(x, y);
                            Color yellowPixel = yellowBitmap.GetPixel(x, y);
                            Color keylinePixel = keylineBitmap.GetPixel(x, y);

                            CmykColor blended = cyanPixel.AddAsCmykColor(magentaPixel, yellowPixel, keylinePixel);
                            destinationBitmap.SetPixel(x, y, blended);
                            // ReSharper restore AccessToDisposedClosure
                        }
                    });
            }
        }

        cyan.Dispose();
        magenta.Dispose();
        yellow.Dispose();
        keyline.Dispose();
        image.Dispose();
        image = newImage;
    }
    catch (Exception ex)
    {
        if (cyan != null)
        {
            cyan.Dispose();
        }

        if (magenta != null)
        {
            magenta.Dispose();
        }

        if (yellow != null)
        {
            yellow.Dispose();
        }

        if (keyline != null)
        {
            keyline.Dispose();
        }

        if (newImage != null)
        {
            newImage.Dispose();
        }

        throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex);
    }

    return image;
} 

Additional code for rotating the pixels is as follows. This can be found at Rotating a point around another point

I've left out the colour addition code for brevity.

    /// <summary>
    /// Rotates one point around another
    /// <see href="https://stackoverflow.com/questions/13695317/rotate-a-point-around-another-point"/>
    /// </summary>
    /// <param name="pointToRotate">The point to rotate.</param>
    /// <param name="centerPoint">The centre point of rotation.</param>
    /// <param name="angleInDegrees">The rotation angle in degrees.</param>
    /// <returns>Rotated point</returns>
    private static Point RotatePoint(Point pointToRotate, Point centerPoint, double angleInDegrees)
    {
        double angleInRadians = angleInDegrees * (Math.PI / 180);
        double cosTheta = Math.Cos(angleInRadians);
        double sinTheta = Math.Sin(angleInRadians);
        return new Point
        {
            X =
                (int)
                ((cosTheta * (pointToRotate.X - centerPoint.X)) -
                ((sinTheta * (pointToRotate.Y - centerPoint.Y)) + centerPoint.X)),
            Y =
                (int)
                ((sinTheta * (pointToRotate.X - centerPoint.X)) +
                ((cosTheta * (pointToRotate.Y - centerPoint.Y)) + centerPoint.Y))
        };
    }
Community
  • 1
  • 1
James South
  • 10,147
  • 4
  • 59
  • 115
  • With all due respect, I don't think you've captured the cartoon'ish quality that the Photoshop filter has. This looks more like Floyd-Steinberg dithering to <= 256 colors, with some aliasing artifacts (the sky). – 500 - Internal Server Error Feb 02 '15 at 19:17
  • I think that's down to scale of ellipses as much as anything. If you zoom into the image you can see the overlapping angled CMYK patterns with additive colour. Definitely not a Floyd-Steinberg algorithm anyway. – James South Feb 02 '15 at 19:25
  • Nothing wrong with that part, but if I compare, say, the outline of the bus in your image with the outlines of the cabs or the buildings on the reference image it seems to me that the edges are much more defined in the reference image. – 500 - Internal Server Error Feb 02 '15 at 19:29
  • Yeah... If I reduce the interval I can do away with most of that but I quite like the current output as is. I guess it's a question of tweaking. I *think* Photoshop is actually using [Ben-Day Dots](http://en.wikipedia.org/wiki/Ben-Day_dots) rather than classical halftoning – James South Feb 02 '15 at 19:33