-1

Image contains single document printed in white paper. Background of image can be different. Tried to get document using code from https://scanbot.io/techblog/document-edge-detection-with-opencv/ with OpenCvsharp (https://github.com/shimat/opencvsharp) by finding biggest rectangle using C# code

  using Mat src = Cv2.ImRead("myimage.jpg", ImreadModes.Color);
  var result = WarpToTopDown(src);

  static Mat WarpToTopDown(Mat src) {
    double scale = 900.0 / Math.Max(src.Rows,src.Cols);
    Mat small = new();
    Cv2.Resize(src,small,new Size((int)(src.Cols * scale),(int)(src.Rows * scale)));
    Mat gray = new();
    Cv2.CvtColor(small,gray,ColorConversionCodes.BGR2GRAY);

    Mat blurred = new();
    Cv2.GaussianBlur(gray,blurred,new Size(5,5),0);
    Mat edges = new();
    Cv2.Canny(blurred,edges,50,150);
    Cv2.Dilate(edges,edges,Cv2.GetStructuringElement(MorphShapes.Rect,new Size(3,3)));
    Cv2.FindContours(edges,out Point[][]? contours,out _,
       RetrievalModes.External,ContourApproximationModes.ApproxSimple);

    contours = [.. contours.OrderByDescending(cnt => Cv2.ContourArea(cnt))];
    Point2f[]? quad = null;
    foreach(Point[]? cnt in contours.Take(10)) {
      double peri = Cv2.ArcLength(cnt,true);
      Point[] approx = Cv2.ApproxPolyDP(cnt,0.02 * peri,true);
      if(approx.Length == 4) {
        quad = approx.Select(p => new Point2f(p.X / (float)scale,p.Y / (float)scale)).ToArray();
        break;
      }
    }

    if(quad is null) {
      thow new Exception("no document");
    }

    Point2f[] ordered = OrderQuad(quad);
    float widthA = Distance(ordered[2],ordered[3]);
    float widthB = Distance(ordered[1],ordered[0]);
    int maxW = (int)Math.Max(widthA,widthB);
    float heightA = Distance(ordered[1],ordered[2]);
    float heightB = Distance(ordered[0],ordered[3]);
    int maxH = (int)Math.Max(heightA,heightB);

    Point2f[] dst = [new Point2f(0,0),
      new Point2f(maxW - 1,0),
      new Point2f(maxW - 1,maxH - 1),
      new Point2f(0,maxH - 1)];
    Mat M = Cv2.GetPerspectiveTransform(ordered,dst);
    Mat warped = new();
    Cv2.WarpPerspective(src,warped,M,new Size(maxW,maxH));
    return warped;
  }

  static Point2f[] OrderQuad(Point2f[] pts) {
    Point2f[] ordered = new Point2f[4];
    float[] sum = [.. pts.Select(p => p.X + p.Y)];
    float[] diff = [.. pts.Select(p => p.X - p.Y)];
    ordered[0] = pts[Array.IndexOf(sum,sum.Min())]; // tl
    ordered[2] = pts[Array.IndexOf(sum,sum.Max())]; // br
    ordered[1] = pts[Array.IndexOf(diff,diff.Max())]; // tr
    ordered[3] = pts[Array.IndexOf(diff,diff.Min())]; // bl
    return ordered;
  }

  static float Distance(Point2f a,Point2f b) => (float)Math.Sqrt(Math.Pow(a.X - b.X,2) + Math.Pow(a.Y - b.Y,2));

but this returns whole image. In foreach check

  if(approx.Length == 4) {

if true for first contur which is whole image and thus whole image is returned.

For image

enter image description here

Result should be

enter image description here

How to return only document? Backgrounds can be different depening on where photo from document is taken Should this done using rectangle search or should some image proprocessing appear?

If contours are visualized in screen using

Cv2.DrawContours(src,contours,-1, new Scalar(0, 255, 0),3);
return src;

Only right side of document is detected:

enter image description here

2

2 Answers 2

3

... here now follow the classes for the ChainCode, add them to your project

(Code for the WinForm in the last answer):

Make sure, you reference the following namespaces:

using System.Collections;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
    //    This class implements a chaincode finder (crack code), as an adaption of 
    //    V. Kovalevsky's crack-code development.  
    //    (PLEASE NOTE THAT THESE ARE *NOT* HTTPS CONNECTIONS! It's an old web-site.)
    //       http://www.miszalok.de/Samples/CV/ChainCode/chain_code.htm and
    //       http://www.miszalok.de/Lectures/L08_ComputerVision/CrackCode/CrackCode_d.htm (german only). See also
    //       http://www.miszalok.de/Samples/CV/ChainCode/chaincode_kovalev_e.htm
    //As the name crackcode says, we are moving on the (invisible) "cracks" in between the pixels to find the outlines of objects

    //Please note that I dont use the /unsafe switch and byte-pointers, since I dont know, whether you are allowed to use that in your code...
    public class ChainFinder
    {
        private int _threshold = 0;
        private bool _nullCells = false;
        private Point _start = new Point(0, 0);
        private int _height = 0;

        public bool AllowNullCells
        {
            get
            {
                return _nullCells;
            }
            set
            {
                _nullCells = value;
            }
        }

        public List<ChainCode>? GetOutline(Bitmap bmp, int threshold, bool grayscale, int range, bool excludeInnerOutlines, int initialValueToCheck, bool doReverse)
        {
            BitArray? fbits = null; //Array to hold the information about processed pixels
            _threshold = threshold;
            _height = bmp.Height;

            try
            {
                List<ChainCode> fList = new List<ChainCode>();

                //Please note that the bitarray is one "column" larger than the bitmap's width
                fbits = new BitArray((bmp.Width + 1) * bmp.Height, false);

                //is the condition so, that the collected coordinate's pixel/color channel values are greater than the threshold,
                //or lower (then use the reversed switch, maybe with an approppriate initial value set)
                if (doReverse)
                    FindChainCodeRev(bmp, fList, fbits, grayscale, range, excludeInnerOutlines, initialValueToCheck);
                else
                    FindChainCode(bmp, fList, fbits, grayscale, range, excludeInnerOutlines, initialValueToCheck);

                return fList;
            }
            catch /*(Exception exc)*/
            {
                if (fbits != null)
                    fbits = null;
            }

            return null;
        }

        // PLEASE NOTE THAT THIS IS *NOT* A HTTPS CONNECTION! It's an old web-site.
        // Adaption von Herrn Prof. Dr.Ing. Dr.med. Volkmar Miszalok, siehe: http://www.miszalok.de/Samples/CV/ChainCode/chain_code.htm
        private void FindChainCode(Bitmap b, List<ChainCode> fList, BitArray fbits, bool grayscale, int range, bool excludeInnerOutlines, int initialValueToCheck)
        {
            SByte[,] Negative = new SByte[,] { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, -1 } };
            SByte[,] Positive = new SByte[,] { { 0, 0 }, { -1, 0 }, { -1, -1 }, { 0, -1 } };

            Point LeftInFront = new Point();
            Point RightInFront = new Point();
            bool LeftInFrontGreaterTh;
            bool RightInFrontGreaterTh;
            int direction = 1;

            BitmapData? bmData = null;

            //if (!AvailMem.AvailMem.checkAvailRam(b.Width * b.Height * 4L))
            //    return;

            try
            {
                bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                int stride = bmData.Stride;

                //copy the BitmapBits to a byte-array for processing
                byte[]? p = new byte[(bmData.Stride * bmData.Height)];
                Marshal.Copy(bmData.Scan0, p, 0, p.Length);

                while (start_crack_search(bmData, p, fbits, grayscale, range, initialValueToCheck))
                {
                    //setup and add the first found pixel to our results list
                    ChainCode cc = new ChainCode();

                    cc.start = _start;
                    // cc.Coord.Add(_start)

                    int x = _start.X;
                    int y = _start.Y + 1;
                    direction = 1;

                    cc.Chain.Add(direction);

                    //as long as we have not reached the starting pixel again, do processing steps
                    while (x != _start.X || y != _start.Y)
                    {
                        LeftInFront.X = x + Negative[direction, 0];
                        LeftInFront.Y = y + Negative[direction, 1];
                        RightInFront.X = x + Positive[direction, 0];
                        RightInFront.Y = y + Positive[direction, 1];

                        //add the correct pixel
                        switch (direction)
                        {
                            case 0:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X - 1, LeftInFront.Y));
                                    break;
                                }

                            case 1:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X, LeftInFront.Y - 1));
                                    break;
                                }

                            case 2:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X + 1, LeftInFront.Y));
                                    break;
                                }

                            case 3:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X, LeftInFront.Y + 1));
                                    break;
                                }
                        }

                        //now do the core algorithm steps, description above
                        LeftInFrontGreaterTh = false;
                        RightInFrontGreaterTh = false;

                        if (LeftInFront.X >= 0 && LeftInFront.X < b.Width && LeftInFront.Y >= 0 && LeftInFront.Y < b.Height)
                        {
                            if (!grayscale)
                                LeftInFrontGreaterTh = p[LeftInFront.Y * stride + LeftInFront.X * 4 + 3] > _threshold;
                            else if (range > 0)
                                LeftInFrontGreaterTh = ((p[LeftInFront.Y * stride + LeftInFront.X * 4] > _threshold) && (p[LeftInFront.Y * stride + LeftInFront.X * 4] <= _threshold + range));
                            else
                                LeftInFrontGreaterTh = p[LeftInFront.Y * stride + LeftInFront.X * 4] > _threshold;
                        }

                        if (RightInFront.X >= 0 && RightInFront.X < b.Width && RightInFront.Y >= 0 && RightInFront.Y < b.Height)
                        {
                            if (!grayscale)
                                RightInFrontGreaterTh = p[RightInFront.Y * stride + RightInFront.X * 4 + 3] > _threshold;
                            else if (range > 0)
                                RightInFrontGreaterTh = ((p[RightInFront.Y * stride + RightInFront.X * 4] > _threshold) && (p[RightInFront.Y * stride + RightInFront.X * 4] <= _threshold + range));
                            else
                                RightInFrontGreaterTh = p[RightInFront.Y * stride + RightInFront.X * 4] > _threshold;
                        }

                        //set new direction (3 cases, but only 2 of them change the direction
                        //(LeftInFrontGreaterTh + !RightInFrontGreaterTh = move straight on))
                        if (RightInFrontGreaterTh && (LeftInFrontGreaterTh || _nullCells))
                            direction = (direction + 1) % 4;
                        else if (!LeftInFrontGreaterTh && (!RightInFrontGreaterTh || !_nullCells))
                            direction = (direction + 3) % 4;

                        cc.Chain.Add(direction);

                        // fbits (always record upper pixel)
                        switch (direction)
                        {
                            case 0:
                                {
                                    x += 1;
                                    cc.Area += y;
                                    break;
                                }

                            case 1:
                                {
                                    y += 1;
                                    fbits.Set((y - 1) * (b.Width + 1) + x, true);
                                    break;
                                }

                            case 2:
                                {
                                    x -= 1;
                                    cc.Area -= y;
                                    break;
                                }

                            case 3:
                                {
                                    y -= 1;
                                    fbits.Set(y * (b.Width + 1) + x, true);
                                    break;
                                }
                        }

                        //if we finally reach the starting pixel again, add a final coord and chain-direction if one of the distance-constraints below is met.
                        //This happens always due to the setup of the algorithm (adding the coord to the ChainCode for the last set direction)
                        if (x == _start.X && y == _start.Y)
                        {
                            if (Math.Abs(cc.Coord[cc.Coord.Count - 1].X - x) > 1 || Math.Abs(cc.Coord[cc.Coord.Count - 1].Y - y) > 1)
                            {
                                if (Math.Abs(cc.Coord[cc.Coord.Count - 1].X - x) > 1)
                                {
                                    cc.Coord.Add(new Point(cc.Coord[cc.Coord.Count - 1].X + 1, cc.Coord[cc.Coord.Count - 1].Y));
                                    cc.Chain.Add(0);
                                }
                                if (Math.Abs(cc.Coord[cc.Coord.Count - 1].Y - y) > 1)
                                {
                                    cc.Coord.Add(new Point(cc.Coord[cc.Coord.Count - 1].X, cc.Coord[cc.Coord.Count - 1].Y + 1));
                                    cc.Chain.Add(1);
                                }
                                break;
                            }
                        }
                    }

                    bool isInnerOutline = false;

                    if (excludeInnerOutlines)
                    {
                        if (cc.Chain[cc.Chain.Count - 1] == 0)
                        {
                            isInnerOutline = true;
                            break;
                        }
                    }

                    //add the list to the results list
                    if (!isInnerOutline)
                    {
                        cc.Coord.Add(_start);
                        fList.Add(cc);
                    }
                }

                p = null;
                b.UnlockBits(bmData);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);

                try
                {
                    if(bmData != null)
                        b.UnlockBits(bmData);
                }
                catch
                {
                }
            }
        }

        private bool start_crack_search(BitmapData bmData, byte[] p, BitArray fbits, bool grayscale, int range, int initialValueToCheck)
        {
            int left = 0;
            int stride = bmData.Stride;

            for (int y = _start.Y; y < bmData.Height; y++)
            {
                for (int x = 0; x < bmData.Width; x++)
                {
                    if (x > 0)
                    {
                        if (!grayscale)
                            left = p[y * stride + (x - 1) * 4 + 3];
                        else
                            left = p[y * stride + (x - 1) * 4];
                    }
                    else
                        left = initialValueToCheck;

                    if (!grayscale)
                    {
                        if ((left <= _threshold) && (p[y * stride + x * 4 + 3] > _threshold) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                        {
                            _start.X = x;
                            _start.Y = y;
                            fbits.Set(y * (bmData.Width + 1) + x, true);
                            //OnProgressPlus();
                            return true;
                        }
                    }
                    else if (range > 0)
                    {
                        if ((left <= _threshold) && (p[y * stride + x * 4] > _threshold) && (p[y * stride + x * 4] <= _threshold + range) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                        {
                            _start.X = x;
                            _start.Y = y;
                            fbits.Set(y * (bmData.Width + 1) + x, true);
                            //OnProgressPlus();
                            return true;
                        }
                    }
                    else if ((left <= _threshold) && (p[y * stride + x * 4] > _threshold) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                    {
                        _start.X = x;
                        _start.Y = y;
                        fbits.Set(y * (bmData.Width + 1) + x, true);
                        //OnProgressPlus();
                        return true;
                    }
                }
            }
            return false;
        }

        // PLEASE NOTE THAT THIS IS *NOT* A HTTPS CONNECTION! It's an old web-site.
        // Adaption von Herrn Prof. Dr.Ing. Dr.med. Volkmar Miszalok, siehe: http://www.miszalok.de/Samples/CV/ChainCode/chain_code.htm
        private void FindChainCodeRev(Bitmap b, List<ChainCode> fList, BitArray fbits, bool grayscale, int range, bool excludeInnerOutlines, int initialValueToCheck)
        {
            SByte[,] Negative = new SByte[,] { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, -1 } };
            SByte[,] Positive = new SByte[,] { { 0, 0 }, { -1, 0 }, { -1, -1 }, { 0, -1 } };

            Point LeftInFront = new Point();
            Point RightInFront = new Point();
            bool LeftInFrontGreaterTh;
            bool RightInFrontGreaterTh;
            int direction = 1;

            BitmapData? bmData = null;

            //if (!AvailMem.AvailMem.checkAvailRam(b.Width * b.Height * 4L))
            //    return;

            try
            {
                bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                int stride = bmData.Stride;

                byte[]? p = new byte[(bmData.Stride * bmData.Height)];
                Marshal.Copy(bmData.Scan0, p, 0, p.Length);

                while (start_crack_searchRev(bmData, p, fbits, grayscale, range, initialValueToCheck))
                {
                    ChainCode cc = new ChainCode();

                    cc.start = _start;

                    int x = _start.X;
                    int y = _start.Y + 1;
                    direction = 1;

                    cc.Chain.Add(direction);

                    while (x != _start.X || y != _start.Y)
                    {
                        LeftInFront.X = x + Negative[direction, 0];
                        LeftInFront.Y = y + Negative[direction, 1];
                        RightInFront.X = x + Positive[direction, 0];
                        RightInFront.Y = y + Positive[direction, 1];

                        switch (direction)
                        {
                            case 0:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X - 1, LeftInFront.Y));
                                    break;
                                }

                            case 1:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X, LeftInFront.Y - 1));
                                    break;
                                }

                            case 2:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X + 1, LeftInFront.Y));
                                    break;
                                }

                            case 3:
                                {
                                    cc.Coord.Add(new Point(LeftInFront.X, LeftInFront.Y + 1));
                                    break;
                                }
                        }

                        LeftInFrontGreaterTh = false;
                        RightInFrontGreaterTh = false;

                        if (LeftInFront.X >= 0 && LeftInFront.X < b.Width && LeftInFront.Y >= 0 && LeftInFront.Y < b.Height)
                        {
                            if (!grayscale)
                                LeftInFrontGreaterTh = p[LeftInFront.Y * stride + LeftInFront.X * 4 + 3] < _threshold;
                            else if (range > 0)
                                LeftInFrontGreaterTh = ((p[LeftInFront.Y * stride + LeftInFront.X * 4] < _threshold) && (p[LeftInFront.Y * stride + LeftInFront.X * 4] >= _threshold + range));
                            else
                                LeftInFrontGreaterTh = p[LeftInFront.Y * stride + LeftInFront.X * 4] < _threshold;
                        }

                        if (RightInFront.X >= 0 && RightInFront.X < b.Width && RightInFront.Y >= 0 && RightInFront.Y < b.Height)
                        {
                            if (!grayscale)
                                RightInFrontGreaterTh = p[RightInFront.Y * stride + RightInFront.X * 4 + 3] < _threshold;
                            else if (range > 0)
                                RightInFrontGreaterTh = ((p[RightInFront.Y * stride + RightInFront.X * 4] < _threshold) && (p[RightInFront.Y * stride + RightInFront.X * 4] >= _threshold + range));
                            else
                                RightInFrontGreaterTh = p[RightInFront.Y * stride + RightInFront.X * 4] < _threshold;
                        }

                        if (RightInFrontGreaterTh && (LeftInFrontGreaterTh || _nullCells))
                            direction = (direction + 1) % 4;
                        else if (!LeftInFrontGreaterTh && (!RightInFrontGreaterTh || !_nullCells))
                            direction = (direction + 3) % 4;

                        cc.Chain.Add(direction);

                        // fbits (immer oberen punkt aufzeichnen)
                        switch (direction)
                        {
                            case 0:
                                {
                                    x += 1;
                                    cc.Area += y;
                                    break;
                                }

                            case 1:
                                {
                                    y += 1;
                                    fbits.Set((y - 1) * (b.Width + 1) + x, true);
                                    break;
                                }

                            case 2:
                                {
                                    x -= 1;
                                    cc.Area -= y;
                                    break;
                                }

                            case 3:
                                {
                                    y -= 1;
                                    fbits.Set(y * (b.Width + 1) + x, true);
                                    break;
                                }
                        }

                        if (x == _start.X && y == _start.Y)
                        {
                            if (Math.Abs(cc.Coord[cc.Coord.Count - 1].X - x) > 1 || Math.Abs(cc.Coord[cc.Coord.Count - 1].Y - y) > 1)
                            {
                                if (Math.Abs(cc.Coord[cc.Coord.Count - 1].X - x) > 1)
                                {
                                    cc.Coord.Add(new Point(cc.Coord[cc.Coord.Count - 1].X + 1, cc.Coord[cc.Coord.Count - 1].Y));
                                    cc.Chain.Add(0);
                                }
                                if (Math.Abs(cc.Coord[cc.Coord.Count - 1].Y - y) > 1)
                                {
                                    cc.Coord.Add(new Point(cc.Coord[cc.Coord.Count - 1].X, cc.Coord[cc.Coord.Count - 1].Y + 1));
                                    cc.Chain.Add(1);
                                }
                                break;
                            }
                        }
                    }

                    bool isInnerOutline = false;

                    if (excludeInnerOutlines)
                    {
                        if (cc.Chain[cc.Chain.Count - 1] == 0)
                        {
                            isInnerOutline = true;
                            break;
                        }
                    }

                    if (!isInnerOutline)
                    {
                        cc.Coord.Add(_start);
                        fList.Add(cc);
                    }
                }

                p = null;
                b.UnlockBits(bmData);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);

                try
                {
                    if (bmData != null)
                        b.UnlockBits(bmData);
                }
                catch
                {
                }
            }
        }

        private bool start_crack_searchRev(BitmapData bmData, byte[] p, BitArray fbits, bool grayscale, int range, int initialValueToCheck)
        {
            int left = 0;
            int stride = bmData.Stride;

            for (int y = _start.Y; y < bmData.Height; y++)
            {
                for (int x = 0; x < bmData.Width; x++)
                {
                    if (x > 0)
                    {
                        if (!grayscale)
                            left = p[y * stride + (x - 1) * 4 + 3];
                        else
                            left = p[y * stride + (x - 1) * 4];
                    }
                    else
                        left = initialValueToCheck;

                    if (!grayscale)
                    {
                        if ((left >= _threshold) && (p[y * stride + x * 4 + 3] < _threshold) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                        {
                            _start.X = x;
                            _start.Y = y;
                            fbits.Set(y * (bmData.Width + 1) + x, true);
                            //OnProgressPlus();
                            return true;
                        }
                    }
                    else if (range > 0)
                    {
                        if ((left >= _threshold) && (p[y * stride + x * 4] < _threshold) && (p[y * stride + x * 4] >= _threshold + range) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                        {
                            _start.X = x;
                            _start.Y = y;
                            fbits.Set(y * (bmData.Width + 1) + x, true);
                            //OnProgressPlus();
                            return true;
                        }
                    }
                    else if ((left >= _threshold) && (p[y * stride + x * 4] < _threshold) && (fbits.Get(y * (bmData.Width + 1) + x) == false))
                    {
                        _start.X = x;
                        _start.Y = y;
                        fbits.Set(y * (bmData.Width + 1) + x, true);
                        //OnProgressPlus();
                        return true;
                    }
                }
            }
            return false;
        }

        public void Reset()
        {
            this._start = new Point(0, 0);
        }
    }

    public class ChainCode
    {
        public static int F { get; set; }

        public Point start
        {
            get
            {
                return m_start;
            }
            set
            {
                m_start = value;
            }
        }
        private Point m_start;

        private List<Point> _coord = new List<Point>();
        private List<int> _chain = new List<int>();

        public List<Point> Coord
        {
            get
            {
                return _coord;
            }
            set
            {
                _coord = value;
            }
        }
        public List<int> Chain
        {
            get
            {
                return _chain;
            }
            set
            {
                _chain = value;
            }
        }

        public int Area
        {
            get
            {
                return m_Area;
            }
            set
            {
                m_Area = value;
            }
        }
        private int m_Area;
        private int _id;

        public int Perimeter
        {
            get
            {
                return _chain.Count;
            }
        }

        public int ID
        {
            get
            {
                return this._id;
            }
        }

        public void SetId()
        {
            if (ChainCode.F < Int32.MaxValue)
            {
                ChainCode.F += 1;
                this._id = ChainCode.F;
            }
            else
                throw new OverflowException("The type of the field for storing the ID reports an overflow error.");
        }

        public void ResetID()
        {
            ChainCode.F = 0;
        }

        public ChainCode()
        {
        }

        public override string ToString()
        {
            return "x = " + start.X.ToString() + "; y = " + start.Y.ToString() + "; count = " + _coord.Count.ToString() + "; area = " + this.Area.ToString();
        }
    }

Regards,
Thorsten

Sign up to request clarification or add additional context in comments.

3 Comments

Application should run in Debian Linux also where System.Drawing namespace is not supported. So this code should converted to use OpenCvSharp or SkiaSharp. I got OpenCv almost working, maybe it is better to use Canny or other method from it without implementing new method. Posted related question in stackoverflow.com/questions/79777276/…
It should be quite easy to get a byte[] from a SkiaSharp bitmap. So you could then simply rewrite the parts in the ChainFinder,cs class that uses System.Drawing Bitmaps to SkiaSharp bitmaps. Then get the bytes (into a .net byte[]) and use the rest of the methods as they are. You will have to edit the start_crack_search methods too, instead of the BitmapData-object, pass the width, height and the stride of the picture as parameters to it
Tried Grabcut method for other photo but it removes background from left side only. Posted it as stackoverflow.com/questions/79778584/… Will your method remove whole background from image in this question? I can provide higher resolution image is required.
3

For OpenCV - I'm not familiar with it, but I know, that there is a FindContours-Method in OpenCV. As far as I know this Method is only for binary images.

Here's my imagination of how you could do this in OpenCV:

- Extract the blue color channel from the image.
- Threshold this by a threshold of about 128, make sure you return a binary image.
- Pass this binary image to the FindContours method of OpenCV.
- Select the correct contour from the results.
- From this contour, display [and save] the enclosed portion of the *original image*.

Considerations:

- Add some logic to automatically get the correct threshold.
- Add some logic to make the selection of the correct chainCode/Contour more reliable for different backgrounds. E.G.: Test the "rectangularity" of the, say 10, largest area ChainCodes, or look at the Gradient, or the Entropy, to decide, if the processed ChainCode is the right one. Or, if the written part always contains some same words/letters, you could correlate the picture with a testimage to get the right contour... There's a lot of options to select the correct part of the orig img, depending on e.g.: performance, or "the parameters" of the problem itself.

Here's a screenshot of a quickly setup Winforms app using gdi-plus (not OpenCV) doing this

Edit: By your request from your comment in this question comment : ...

Here's .net8.0 c# WindowsForms code with a minimal implementation of a chaincode with a threshold for copying/pasting. ChainCode in this variant developed by V. Kovalevsky. It works as follows:

As the name crackcode says, we are moving on the (invisible) "cracks" in between the pixels to find the outlines of objects  
in an image. The algorithm is as follows:  
- Find a start pixel by moving left to right and top to bottom over the image which matches the conditions.  
- Now we are at the leftTop location of the \*pixel\*, the crack left of the pixel.   
- We now set our direction to the only reasonable value "down" and move along the crack, so we truly start the  
  algorithm at the lowest "point" of the left crack, ie. the leftBottom location of the starting pixel, and we turn our  
- looking direction also down, so the pixel left in front of us is the pixel below of the start pixel.  

- Now we:  

- Look to the pixel thats left in front of us,   
    a) if it doesnt meet the conditions (search criteria is lower than the threshold),  
       we turn left (set the direction to left, turn ourselves left and move along that crack to the next junction,   
       being now at the BottomRight corner of the start pixel)  
    b) if it meets the conditions, we:  
           a) look to the pixels thats on the right in front of us,  
               aa) if it doesnt meet the conditions (search criteria is lower than the threshold),  
                   we go straight on, keeping direction "down", being now at the BottomLeft corner   
                   of the pixel below our start pixel  
               bb) if it meets the conditions, we turn right (setting the direction and ourselves "right"   
                   and move along that crack to the next junction, being now at the BottomLeft corner   
                   of the pixel on the left of our start pixel (when looking "normally" to the picture)  
- Since we always turn our looking-direction, we can now repeat the above until we are back at the point we began.  
- Since we move on the cracks, we always will come back to that point, if we only have one pixel matching the  
  conditions, we get a chain of 4 cracks, once around the pixel

//##########################################################################

What follows is a step by step walkthrough to setup the windows forms app:

  1. Open VisualStudio and create a new c# WinForms app in .net 8.0.
  2. Open the Forms1.Designer.cs file and replace the InitializeComponent - method (also the surrounding comment and pre-processor directive ("region ...")) with the complete content of the following code-block.
        #region Windows Form Designer generated code

        /// <summary>
        ///  Required method for Designer support - do not modify
        ///  the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            btnOpen = new Button();
            btnGetChains = new Button();
            splitContainer1 = new SplitContainer();
            numTh = new NumericUpDown();
            label1 = new Label();
            btnSave = new Button();
            splitContainer2 = new SplitContainer();
            pictureBox1 = new PictureBox();
            splitContainer3 = new SplitContainer();
            pictureBox2 = new PictureBox();
            label7 = new Label();
            label6 = new Label();
            cbSelSingleClick = new CheckBox();
            btnSelNone = new Button();
            btnSelAll = new Button();
            cbRestrict = new CheckBox();
            numRestrict = new NumericUpDown();
            checkedListBox1 = new CheckedListBox();
            saveFileDialog1 = new SaveFileDialog();
            OpenFileDialog1 = new OpenFileDialog();
            ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
            splitContainer1.Panel1.SuspendLayout();
            splitContainer1.Panel2.SuspendLayout();
            splitContainer1.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)numTh).BeginInit();
            ((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit();
            splitContainer2.Panel1.SuspendLayout();
            splitContainer2.Panel2.SuspendLayout();
            splitContainer2.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
            ((System.ComponentModel.ISupportInitialize)splitContainer3).BeginInit();
            splitContainer3.Panel1.SuspendLayout();
            splitContainer3.Panel2.SuspendLayout();
            splitContainer3.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)pictureBox2).BeginInit();
            ((System.ComponentModel.ISupportInitialize)numRestrict).BeginInit();
            SuspendLayout();
            // 
            // btnOpen
            // 
            btnOpen.Location = new Point(12, 15);
            btnOpen.Name = "btnOpen";
            btnOpen.Size = new Size(75, 23);
            btnOpen.TabIndex = 0;
            btnOpen.Text = "open";
            btnOpen.UseVisualStyleBackColor = true;
            btnOpen.Click += btnOpen_Click;
            // 
            // btnGetChains
            // 
            btnGetChains.Location = new Point(348, 15);
            btnGetChains.Name = "btnGetChains";
            btnGetChains.Size = new Size(75, 23);
            btnGetChains.TabIndex = 1;
            btnGetChains.Text = "GetChains";
            btnGetChains.UseVisualStyleBackColor = true;
            btnGetChains.Click += btnGetChains_Click;
            // 
            // splitContainer1
            // 
            splitContainer1.Dock = DockStyle.Fill;
            splitContainer1.Location = new Point(0, 0);
            splitContainer1.Name = "splitContainer1";
            splitContainer1.Orientation = Orientation.Horizontal;
            // 
            // splitContainer1.Panel1
            // 
            splitContainer1.Panel1.Controls.Add(numTh);
            splitContainer1.Panel1.Controls.Add(label1);
            splitContainer1.Panel1.Controls.Add(btnSave);
            splitContainer1.Panel1.Controls.Add(btnOpen);
            splitContainer1.Panel1.Controls.Add(btnGetChains);
            // 
            // splitContainer1.Panel2
            // 
            splitContainer1.Panel2.Controls.Add(splitContainer2);
            splitContainer1.Size = new Size(1008, 750);
            splitContainer1.SplitterDistance = 46;
            splitContainer1.TabIndex = 2;
            // 
            // numTh
            // 
            numTh.Location = new Point(492, 17);
            numTh.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
            numTh.Name = "numTh";
            numTh.Size = new Size(65, 23);
            numTh.TabIndex = 4;
            numTh.Value = new decimal(new int[] { 128, 0, 0, 0 });
            // 
            // label1
            // 
            label1.AutoSize = true;
            label1.Location = new Point(429, 19);
            label1.Name = "label1";
            label1.Size = new Size(57, 15);
            label1.TabIndex = 3;
            label1.Text = "threshold";
            // 
            // btnSave
            // 
            btnSave.Location = new Point(622, 15);
            btnSave.Name = "btnSave";
            btnSave.Size = new Size(75, 23);
            btnSave.TabIndex = 2;
            btnSave.Text = "save";
            btnSave.UseVisualStyleBackColor = true;
            btnSave.Click += btnSave_Click;
            // 
            // splitContainer2
            // 
            splitContainer2.Dock = DockStyle.Fill;
            splitContainer2.Location = new Point(0, 0);
            splitContainer2.Name = "splitContainer2";
            // 
            // splitContainer2.Panel1
            // 
            splitContainer2.Panel1.Controls.Add(pictureBox1);
            // 
            // splitContainer2.Panel2
            // 
            splitContainer2.Panel2.Controls.Add(splitContainer3);
            splitContainer2.Size = new Size(1008, 700);
            splitContainer2.SplitterDistance = 336;
            splitContainer2.TabIndex = 0;
            // 
            // pictureBox1
            // 
            pictureBox1.Dock = DockStyle.Fill;
            pictureBox1.Location = new Point(0, 0);
            pictureBox1.Name = "pictureBox1";
            pictureBox1.Size = new Size(336, 700);
            pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
            pictureBox1.TabIndex = 0;
            pictureBox1.TabStop = false;
            // 
            // splitContainer3
            // 
            splitContainer3.Dock = DockStyle.Fill;
            splitContainer3.Location = new Point(0, 0);
            splitContainer3.Name = "splitContainer3";
            // 
            // splitContainer3.Panel1
            // 
            splitContainer3.Panel1.Controls.Add(pictureBox2);
            // 
            // splitContainer3.Panel2
            // 
            splitContainer3.Panel2.Controls.Add(label7);
            splitContainer3.Panel2.Controls.Add(label6);
            splitContainer3.Panel2.Controls.Add(cbSelSingleClick);
            splitContainer3.Panel2.Controls.Add(btnSelNone);
            splitContainer3.Panel2.Controls.Add(btnSelAll);
            splitContainer3.Panel2.Controls.Add(cbRestrict);
            splitContainer3.Panel2.Controls.Add(numRestrict);
            splitContainer3.Panel2.Controls.Add(checkedListBox1);
            splitContainer3.Size = new Size(668, 700);
            splitContainer3.SplitterDistance = 368;
            splitContainer3.TabIndex = 0;
            // 
            // pictureBox2
            // 
            pictureBox2.Dock = DockStyle.Fill;
            pictureBox2.Location = new Point(0, 0);
            pictureBox2.Name = "pictureBox2";
            pictureBox2.Size = new Size(368, 700);
            pictureBox2.SizeMode = PictureBoxSizeMode.Zoom;
            pictureBox2.TabIndex = 0;
            pictureBox2.TabStop = false;
            // 
            // label7
            // 
            label7.AutoSize = true;
            label7.Location = new Point(143, 254);
            label7.Name = "label7";
            label7.Size = new Size(16, 15);
            label7.TabIndex = 658;
            label7.Text = "...";
            // 
            // label6
            // 
            label6.AutoSize = true;
            label6.Location = new Point(13, 254);
            label6.Name = "label6";
            label6.Size = new Size(16, 15);
            label6.TabIndex = 659;
            label6.Text = "...";
            // 
            // cbSelSingleClick
            // 
            cbSelSingleClick.AutoSize = true;
            cbSelSingleClick.Location = new Point(12, 315);
            cbSelSingleClick.Name = "cbSelSingleClick";
            cbSelSingleClick.Size = new Size(131, 19);
            cbSelSingleClick.TabIndex = 652;
            cbSelSingleClick.Text = "SelectOnSingleClick";
            cbSelSingleClick.UseVisualStyleBackColor = true;
            cbSelSingleClick.CheckedChanged += cbSelSingleClick_CheckedChanged;
            // 
            // btnSelNone
            // 
            btnSelNone.ForeColor = SystemColors.ControlText;
            btnSelNone.Location = new Point(199, 340);
            btnSelNone.Margin = new Padding(4, 3, 4, 3);
            btnSelNone.Name = "btnSelNone";
            btnSelNone.Size = new Size(88, 27);
            btnSelNone.TabIndex = 654;
            btnSelNone.Text = "UnselectAll";
            btnSelNone.UseVisualStyleBackColor = true;
            btnSelNone.Click += btnSelNone_Click;
            // 
            // btnSelAll
            // 
            btnSelAll.ForeColor = SystemColors.ControlText;
            btnSelAll.Location = new Point(12, 340);
            btnSelAll.Margin = new Padding(4, 3, 4, 3);
            btnSelAll.Name = "btnSelAll";
            btnSelAll.Size = new Size(88, 27);
            btnSelAll.TabIndex = 656;
            btnSelAll.Text = "SelectAll";
            btnSelAll.UseVisualStyleBackColor = true;
            btnSelAll.Click += btnSelAll_Click;
            // 
            // cbRestrict
            // 
            cbRestrict.AutoSize = true;
            cbRestrict.Checked = true;
            cbRestrict.CheckState = CheckState.Checked;
            cbRestrict.Location = new Point(12, 290);
            cbRestrict.Name = "cbRestrict";
            cbRestrict.Size = new Size(158, 19);
            cbRestrict.TabIndex = 653;
            cbRestrict.Text = "restrict amount chains to";
            cbRestrict.UseVisualStyleBackColor = true;
            // 
            // numRestrict
            // 
            numRestrict.Location = new Point(198, 289);
            numRestrict.Maximum = new decimal(new int[] { 32767, 0, 0, 0 });
            numRestrict.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
            numRestrict.Name = "numRestrict";
            numRestrict.Size = new Size(88, 23);
            numRestrict.TabIndex = 657;
            numRestrict.Value = new decimal(new int[] { 100, 0, 0, 0 });
            // 
            // checkedListBox1
            // 
            checkedListBox1.FormattingEnabled = true;
            checkedListBox1.Location = new Point(13, 15);
            checkedListBox1.Name = "checkedListBox1";
            checkedListBox1.Size = new Size(271, 238);
            checkedListBox1.TabIndex = 0;
            checkedListBox1.SelectedIndexChanged += checkedListBox1_SelectedIndexChanged;
            // 
            // saveFileDialog1
            // 
            saveFileDialog1.FileName = "Bild1.png";
            saveFileDialog1.Filter = "Png-Images (*.png)|*.png";
            // 
            // OpenFileDialog1
            // 
            OpenFileDialog1.FileName = "OpenFileDialog1";
            OpenFileDialog1.Filter = "Bilder (*.jpg;*.bmp;*.png)|*.jpg;*.bmp;*.png|Alle Dateien (*.*)|*.*";
            // 
            // Form1
            // 
            AutoScaleDimensions = new SizeF(7F, 15F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new Size(1008, 750);
            Controls.Add(splitContainer1);
            Name = "Form1";
            Text = "Form1";
            FormClosing += Form1_FormClosing;
            splitContainer1.Panel1.ResumeLayout(false);
            splitContainer1.Panel1.PerformLayout();
            splitContainer1.Panel2.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
            splitContainer1.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)numTh).EndInit();
            splitContainer2.Panel1.ResumeLayout(false);
            splitContainer2.Panel2.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)splitContainer2).EndInit();
            splitContainer2.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
            splitContainer3.Panel1.ResumeLayout(false);
            splitContainer3.Panel2.ResumeLayout(false);
            splitContainer3.Panel2.PerformLayout();
            ((System.ComponentModel.ISupportInitialize)splitContainer3).EndInit();
            splitContainer3.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)pictureBox2).EndInit();
            ((System.ComponentModel.ISupportInitialize)numRestrict).EndInit();
            ResumeLayout(false);
        }

        #endregion

        private Button btnOpen;
        private Button btnGetChains;
        private SplitContainer splitContainer1;
        private SplitContainer splitContainer2;
        private PictureBox pictureBox1;
        private SplitContainer splitContainer3;
        private PictureBox pictureBox2;
        private CheckedListBox checkedListBox1;
        private SaveFileDialog saveFileDialog1;
        internal OpenFileDialog OpenFileDialog1;
        private Button btnSave;
        private Label label7;
        private Label label6;
        private CheckBox cbSelSingleClick;
        private Button btnSelNone;
        private Button btnSelAll;
        private CheckBox cbRestrict;
        private NumericUpDown numRestrict;
        private NumericUpDown numTh;
        private Label label1;
  1. Open the Form1.cs code-view and replace the form1-block (public partial class Form1 : Form { ... }) with the complete content of the following code-block. Make sure, the last closing curly brace (closing the namespace-block) doesn't get lost.

[The classes for the ChainCode will follow in the next answer, due to character-amount-restrictions per answer]

    public partial class Form1 : Form
    {
        private bool _picChanged;

        public Form1()
        {
            InitializeComponent();
        }

        private void btnOpen_Click(object sender, EventArgs e)
        {
            if (this._picChanged)
            {
                DialogResult dlg = MessageBox.Show("Save image?", "Unsaved data", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);

                if (dlg == DialogResult.Yes)
                    btnSave.PerformClick();
                else if (dlg == DialogResult.Cancel)
                    return;
            }

            Bitmap? bmp = null;
            if (this.OpenFileDialog1.ShowDialog() == DialogResult.OK)
            {
                using (Image img = Image.FromFile(this.OpenFileDialog1.FileName))
                    bmp = new Bitmap(img);

                Image iOld = this.pictureBox1.Image;
                this.pictureBox1.Image = bmp;
                if (iOld != null)
                    iOld.Dispose();

                this.Text = this.OpenFileDialog1.FileName;
            }
        }

        private void btnSave_Click(object sender, EventArgs e)
        {
            if (this.pictureBox1.Image != null)
            {
                this.saveFileDialog1.Filter = "Png-Images (*.png)|*.png";
                this.saveFileDialog1.FileName = "Bild1.png";

                try
                {
                    if (this.saveFileDialog1.ShowDialog() == DialogResult.OK)
                    {
                        this.pictureBox2.Image.Save(this.saveFileDialog1.FileName, System.Drawing.Imaging.ImageFormat.Png);
                        _picChanged = false;
                    }
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            }
        }

        private void btnGetChains_Click(object sender, EventArgs e)
        {
            this.checkedListBox1.Items.Clear();

            int th = (int)this.numTh.Value;
            using Bitmap bmp = new Bitmap(this.pictureBox1.Image);
            ChainFinder cf = new ChainFinder();
            //please note that we pass a color-image to the function with the grayscale switch on.
            //This means that only the blue color channel will be processed in the start_crach_search function,
            //since this function assumes that the image is grayscaled before processing, and so just checks one
            //of the three color channels.
            List<ChainCode>? c = cf.GetOutline(bmp, th, true, 0, false, 0, false);

            if (c != null)
            {
                c = c.OrderByDescending(a => a.Area).ToList();

                this.checkedListBox1.Items.Clear();

                this.checkedListBox1.SuspendLayout();
                this.checkedListBox1.BeginUpdate();

                int cnt = c.Count;
                if (this.cbRestrict.Checked)
                    cnt = Math.Min(c.Count, (int)this.numRestrict.Value);

                for (int i = 0; i < cnt; i++)
                    this.checkedListBox1.Items.Add(c[i], false);

                this.checkedListBox1.EndUpdate();
                this.checkedListBox1.ResumeLayout();

                if (this.checkedListBox1.Items.Count > 0)
                {
                    this.checkedListBox1.SetItemChecked(0, true);
                    this.checkedListBox1.SelectedIndex = 0;
                }
            }
        }

        private void cbSelSingleClick_CheckedChanged(object sender, EventArgs e)
        {
            this.checkedListBox1.CheckOnClick = this.cbSelSingleClick.Checked;
        }

        private void btnSelAll_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < this.checkedListBox1.Items.Count; i++)
                this.checkedListBox1.SetItemChecked(i, true);

            checkedListBox1_SelectedIndexChanged(this.checkedListBox1, new EventArgs());
        }

        private void btnSelNone_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < this.checkedListBox1.Items.Count; i++)
                this.checkedListBox1.SetItemChecked(i, false);

            checkedListBox1_SelectedIndexChanged(this.checkedListBox1, new EventArgs());
        }

        private void checkedListBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            List<ChainCode> fList = new List<ChainCode>();
            for (int i = 0; i < this.checkedListBox1.CheckedItems.Count; i++)
                if (this.checkedListBox1.CheckedItems[i] != null)
                {
                    ChainCode? f = this.checkedListBox1.CheckedItems[i] as ChainCode;
                    if (f != null)
                        fList.Add(f);
                }

            if (this.pictureBox1.Image != null)
            {
                using Bitmap b = new Bitmap(this.pictureBox1.Image);
                Bitmap bmp = new Bitmap(b.Width, b.Height);

                ChainFinder cf = new ChainFinder();
                FillOutlineInBmp(bmp, b, fList);

                Image? iOld = this.pictureBox2.Image;
                this.pictureBox2.Image = bmp;
                if (iOld != null)
                    iOld.Dispose();
                iOld = null;

                this.pictureBox2.Refresh();
                this._picChanged = true;
            }
        }

        private void FillOutlineInBmp(Bitmap bmp, Bitmap bOrig, List<ChainCode> fList)
        {
            using Graphics gx = Graphics.FromImage(bmp);
            using TextureBrush tb = new TextureBrush(bOrig);

            for (int i = 0; i < fList.Count; i++)
            {
                using GraphicsPath gP = new GraphicsPath();
                gP.AddLines(fList[i].Coord.Select(a => new PointF(a.X, a.Y)).ToArray());

                gx.FillPath(tb, gP);
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (this.pictureBox1.Image != null)
                this.pictureBox1.Image.Dispose();  
            if (this.pictureBox2.Image != null)
                this.pictureBox2.Image.Dispose();
        }
    }

3.2. Add the code for the ChainCode (in follow-up answer)

  1. Build the app.

##########################################################################

Usage:

- Start the app and click onto the "open" button to load a picture.
- [Adjust the threshold - in the NumericUpDown Control] and click on the "GetChains" button.
- Repeat the last step until you like the result.//##########################################################################

Regards,

Thorsten

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.