BoltBait's Pages - Ink Sketch

Sarcasm, just one of the services I offer.

 

Paint.NET Ink Sketch Effect

I'm still learning how to create complex effect filters for Paint.NET.  I think this is one of my best yet.

 

The Idea

I thought that it would be easy to automate the steps that I had come up with for turning a picture into an ink sketch with colored pencil shading.  It turned out that it wasn't too hard.  Rick Brewster was nice enough to send me the source code to the Pencil Sketch effect from Paint.NET 3.05.  From that, I learned how to combine preexisting effects and blending operations.

NOTE:Rick has bundled this effect with Paint.NET version 3.10 and beyond.  If you have upgraded your Paint.NET to 3.10 or higher, there is no need to download this effect DLL--you already have it!

 

The Effect DLL

You can download the precompiled effect DLL here: InkSketch.dll

Just drop this file in your \program files\Paint.NET\effects directory and you should be all set.

If you need help installing effects, read this page: Installing Effects 

 

Instructions for Use

The best way to use this is to follow these steps:

1.  Open your graphic.

2.  Click the Effects > Ink Sketch menu.

3.  Adjust the sliders for the desired effect.

 

This changes the photo (on the left) to the sketch (on the right):

 

  

Source Code

Here are the simplified steps that my code simulates:

1. Open an image
2. Duplicate the layer
3. Effects > Glow the bottom layer with radius 6 (default) and both other sliders tied together based on my Coloring slider
4. Run the Toon effect on the top layer
5. On the top layer, Adjustments > Black and White
6. Then, Adjustments > Brightness / Contrast... set the contrast to 100% and adjust the brightness based on my Ink Outline slider
7. Change the blending mode of the top layer to Darken

And, here is the code (NOTE: This is NOT a CodeLab script):

/* InkSketchEffect.cs 
Copyright (c) 2007 David Issel 
Contact info: BoltBait@hotmail.com http://www.BoltBait.com 

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is 
furnished to do so, subject to the following conditions: 

The above copyright notice and this permission notice shall be included in 
all copies or substantial portions of the Software. 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
THE SOFTWARE. 
*/ 
using System; 
using System.Collections; 
using System.Drawing; 
using PaintDotNet; 
using PaintDotNet.Effects; 

namespace InkSketch 
{ 
    public class EffectPlugin 
        : PaintDotNet.Effects.Effect 
    { 
        public static string StaticName 
        { 
            get 
            { 
                return "Ink Sketch"; 
            } 
        } 

        public static Bitmap StaticIcon 
        { 
            get 
            { 
                return new Bitmap(typeof(EffectPlugin), "EffectPluginIcon.png"); 
            } 
        } 

        public EffectPlugin() 
            : base(EffectPlugin.StaticName, EffectPlugin.StaticIcon, true) 
        { 

        } 

        private UnaryPixelOps.Desaturate desaturateOp = new UnaryPixelOps.Desaturate(); 
        private GlowEffect glowEffect = new GlowEffect(); 
        private UserBlendOps.DarkenBlendOp DarkenOp = new UserBlendOps.DarkenBlendOp(); 

        public override unsafe void Render(EffectConfigToken parameters, RenderArgs dstArgs, RenderArgs srcArgs, Rectangle[] rois, int startIndex, int length) 
        { 
            TwoAmountsConfigToken tacd = (TwoAmountsConfigToken)parameters; 

            // Glow backgound 
            ThreeAmountsConfigToken glowToken = new ThreeAmountsConfigToken(6, -(tacd.Amount2-50)*2, -(tacd.Amount2-50)*2); 
            this.glowEffect.Render(glowToken, dstArgs, srcArgs, rois, startIndex, length); 

            // Create black outlines by finding the edges of objects 

            const int size = 5; 

            int[,] conv = new int[size, size] { 
                                            {-1,  -1,  -1,  -1, -1}, 
                                            {-1,  -1,  -1,  -1, -1}, 
                                            {-1,  -1,  30,  -1, -1}, 
                                            {-1,  -1,  -1,  -1, -1}, 
                                            {-1,  -1,  -5,  -1, -1}, 
                                          }; 

            int radius = (size-1)/2; 

            for (int i = startIndex; i < startIndex + length; ++i) 
            { 
                Rectangle roi = rois[i]; 

                for (int y = roi.Top; y < roi.Bottom; ++y) 
                { 
                    int top = y - radius, bottom = y + radius; 
                    if (top < 0) top = 0; 
                    if (bottom >= dstArgs.Height) bottom = dstArgs.Height - 1; 
                    
                    ColorBgra* srcPtr = srcArgs.Surface.GetPointAddress(roi.X, roi.Y); 
                    ColorBgra* dstPtr = dstArgs.Surface.GetPointAddress(roi.X, roi.Y); 

                    for (int x = roi.Left; x < roi.Right; ++x) 
                    { 
                        int left = x - radius, right = x + radius; 
                        int c = 0, s = 0, r = 0, g = 0, b = 0; 
                        if (left < 0) left = 0; 
                        if (right >= dstArgs.Width) right = dstArgs.Width - 1; 
                        for (int v = top; v <= bottom; v++) 
                        { 
                            ColorBgra* pRow = srcArgs.Surface.GetRowAddressUnchecked(v); 
                            int j = v - y + radius; 

                            for (int u = left; u <= right; u++) 
                            { 
                                int i1 = u - x + radius; 
                                int w = conv[i1, j]; 
                                ColorBgra* pRef = pRow + u; 
                                r += pRef->R * w; 
                                g += pRef->G * w; 
                                b += pRef->B * w; 
                                s += w; 
                                c++; 
                            } 
                        } 
                        ColorBgra TopLayer = ColorBgra.FromBgr( 
                            Utility.ClampToByte(b), 
                            Utility.ClampToByte(g), 
                            Utility.ClampToByte(r)); 

                        // Desaturate 
                        TopLayer = this.desaturateOp.Apply(TopLayer); 

                        // Adjust Brightness and Contrast 
                        if (TopLayer.R > (tacd.Amount1 * 255 / 100)) 
                        { 
                            TopLayer = ColorBgra.FromBgra(255, 255, 255, TopLayer.A); 
                        } 
                        else 
                        { 
                            TopLayer = ColorBgra.FromBgra(0, 0, 0, TopLayer.A); 
                        } 

                        // Change Blend Mode to Darken 
                        ColorBgra mypixel = this.DarkenOp.Apply(TopLayer, *dstPtr); 
                        *dstPtr = mypixel; 

                        ++srcPtr; 
                        ++dstPtr; 
                    } 
                } 
            } 
        } 

        public override EffectConfigDialog CreateConfigDialog() 
        { 
            TwoAmountsConfigDialog tacd = new TwoAmountsConfigDialog(); 
            tacd.Text = StaticName; 

            tacd.Amount1Label = "Ink Outline"; 
            tacd.Amount1Minimum = 0; 
            tacd.Amount1Maximum = 100; 
            tacd.Amount1Default = 50; 

            tacd.Amount2Label = "Coloring"; 
            tacd.Amount2Minimum = 0; 
            tacd.Amount2Maximum = 100; 
            tacd.Amount2Default = 50; 

            return tacd; 
        } 

    } 
}

Thursday, April 5, 2007