Skip to content

Création d'un GraphicsPath à partir d'un bitmap semi-transparent.

Il est essentiel de bien interpréter le code avant de l'adapter à votre projet et si vous avez quelque chose à apporter vous pouvez le laisser dans les commentaires.

Solution :

Comme vous deux desrcipt vous avez juste à trouver le premier point non transparent et par la suite déplacer le long des pixels non transparents avec un voisin transparent.

En outre, vous devrez enregistrer le point que vous avez déjà visité et combien de fois vous les avez visités ou vous finirez dans les mêmes cas dans une boucle d'invincibilité. Si le point n'a pas de voisin qui a déjà été visité vous devez revenir en arrière chaque point, dans la direction révisée, jusqu'à ce qu'un point non visité soit à nouveau disponible.

C'est tout.


//CODE SUPPRIMÉ - LE POST ÉTAIT TROP LONG


EDIT 1

Code modifié :


//CODE SUPPRIMÉ - LE MESSAGE ÉTAIT TROP LONG


EDIT 2

Maintenant, toutes les régions sont retournées :


//CODE SUPPRIMÉ - LE POST ÉTAIT TROP LONG


EDIT 3

Changements :

  • Point.EMPTY a été remplacé par Point(-1,-1), sinon un pixel non transparent
    dans le coin supérieur gauche provoque une boucle d'invincibilité.
  • Vérification de la présence d'un point limite à la frontière de l'image.

class BorderFinder {

    int stride = 0;
    int[] visited = null;
    byte[] bytes = null;
    PointData borderdata = null;
    Size size = Size.Empty;
    bool outside = false;
    Point zeropoint = new Point(-1,-1);

    public List Find(Bitmap bmp, bool outside = true) {
        this.outside = outside;
        List border = new List();
        BitmapData bmpdata = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        stride = bmpdata.Stride;
        bytes = new byte[bmp.Width * bmp.Height * 4];
        size = bmp.Size;

        Marshal.Copy(bmpdata.Scan0, bytes, 0, bytes.Length);

        // Get all Borderpoint
        borderdata = getBorderData(bytes);

        bmp.UnlockBits(bmpdata);

        List> regions = new List>();

        //Loop until no more borderpoints are available
        while (borderdata.PointCount > 0) {
            List region = new List();

            //if valid is false the region doesn't close
            bool valid = true;

            //Find the first borderpoint from where whe start crawling
            Point startpos = getFirstPoint(borderdata);

            //we need this to know if and how often we already visted the point.
            //we somtime have to visit a point a second time because we have to go backward until a unvisted point is found again
            //for example if we go int a narrow 1px hole
            visited = new int[bmp.Size.Width * bmp.Size.Height];

            region.Add(startpos);

            //Find the next possible point
            Point current = getNextPoint(startpos);

            if (current != zeropoint) {
                visited[current.Y * bmp.Width + current.X]++;
                region.Add(current);
            }

            //May occure with just one transparent pixel without neighbors
            if (current == zeropoint)
                valid = false;

            //Loop until the area closed or colsing the area wasn't poosible
            while (!current.Equals(startpos) && valid) {
                var pos = current;
                //Check if the area was aready visited
                if (visited[current.Y * bmp.Width + current.X] < 2) {
                    current = getNextPoint(pos);
                    visited[pos.Y * bmp.Width + pos.X]++;
                    //If no possible point was found, search in reversed direction
                    if (current == zeropoint)
                        current = getNextPointBackwards(pos);
                } else { //If point was already visited, search in reversed direction
                    current = getNextPointBackwards(pos);
                }

                //No possible point was found. Closing isn't possible
                if (current == zeropoint) {
                    valid = false;
                    break;
                }

                visited[current.Y * bmp.Width + current.X]++;

                region.Add(current);
            }
            //Remove point from source borderdata
            foreach (var p in region) {
                borderdata.SetPoint(p.Y * bmp.Width + p.X, false);
            }
            //Add region if closing was possible
            if (valid)
                regions.Add(region);
        }

        //Checks if Region goes the same way back and trims it in this case
        foreach (var region in regions) {
            int duplicatedpos = -1;

            bool[] duplicatecheck = new bool[size.Width * size.Height];
            int length = region.Count;
            for (int i = 0; i < length; i++) {
                var p = region[i];
                if (duplicatecheck[p.Y * size.Width + p.X]) {
                    duplicatedpos = i - 1;
                    break;
                }
                duplicatecheck[p.Y * size.Width + p.X] = true;
            }

            if (duplicatedpos == -1)
                continue;

            if (duplicatedpos != ((region.Count - 1) / 2))
                continue;

            bool reversed = true;

            for (int i = 0; i < duplicatedpos; i++) {
                if (region[duplicatedpos - i - 1] != region[duplicatedpos + i + 1]) {
                    reversed = false;
                    break;
                }
            }

            if (!reversed)
                continue;

            region.RemoveRange(duplicatedpos + 1, region.Count - duplicatedpos - 1);
        }

        List> tempregions = new List>(regions);
        regions.Clear();

        bool connected = true;
        //Connects region if possible
        while (connected) {
            connected = false;
            foreach (var region in tempregions) {
                int connectionpos = -1;
                int connectionregion = -1;
                Point pointstart = region.First();
                Point pointend = region.Last();
                for (int ir = 0; ir < regions.Count; ir++) {
                    var otherregion = regions[ir];
                    if (region == otherregion)
                        continue;

                    for (int ip = 0; ip < otherregion.Count; ip++) {
                        var p = otherregion[ip];
                        if ((isConnected(pointstart, p) && isConnected(pointend, p)) ||
                            (isConnected(pointstart, p) && isConnected(pointstart, p))) {
                            connectionregion = ir;
                            connectionpos = ip;
                        }

                        if ((isConnected(pointend, p) && isConnected(pointend, p))) {
                            region.Reverse();
                            connectionregion = ir;
                            connectionpos = ip;
                        }
                    }

                }

                if (connectionpos == -1) {
                    regions.Add(region);
                } else {
                    regions[connectionregion].InsertRange(connectionpos, region);
                }

            }

            tempregions = new List>(regions);
            regions.Clear();
        }

        List returnregions = new List();

        foreach (var region in tempregions)
            returnregions.Add(region.ToArray());

        return returnregions;
    }

    private bool isConnected(Point p0, Point p1) {

        if (p0.X == p1.X && p0.Y - 1 == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y - 1 == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y == p1.Y)
            return true;

        if (p0.X + 1 == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y + 1 == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y == p1.Y)
            return true;

        if (p0.X - 1 == p1.X && p0.Y - 1 == p1.Y)
            return true;

        return false;
    }

    private Point getNextPoint(Point pos) {
        if (pos.Y > 0) {
            int x = pos.X;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.Y > 0 && pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X < size.Width - 1 && pos.Y < size.Height - 1) {
            int x = pos.X + 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.Y < size.Height - 1) {
            int x = pos.X;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.Y < size.Height - 1 && pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        if (pos.X > 0 && pos.Y > 0) {
            int x = pos.X - 1;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
            }
        }

        return zeropoint;
    }

    private Point getNextPointBackwards(Point pos) {
        Point backpoint = zeropoint;

        int trys = 0;

        if (pos.X > 0 && pos.Y > 0) {
            int x = pos.X - 1;
            int y = pos.Y - 1;
            if (ValidPoint(x, y) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.Y < size.Height - 1 && pos.X > 0) {
            int x = pos.X - 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.Y < size.Height - 1) {
            int x = pos.X;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.X < size.Width - 1 && pos.Y < size.Height - 1) {
            int x = pos.X + 1;
            int y = pos.Y + 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.Y > 0 && pos.X < size.Width - 1) {
            int x = pos.X + 1;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        if (pos.Y > 0) {
            int x = pos.X;
            int y = pos.Y - 1;
            if ((ValidPoint(x, y)) && HasNeighbor(x, y)) {
                if (visited[y * size.Width + x] == 0) {
                    return new Point(x, y);
                }
                if (backpoint == zeropoint || trys > visited[y * size.Width + x]) {
                    backpoint = new Point(x, y);
                    trys = visited[y * size.Width + x];
                }
            }
        }

        return backpoint;
    }

    private bool ValidPoint(int x, int y) {
        return (borderdata[y * size.Width + x]);
    }

    private bool HasNeighbor(int x, int y) {
        if (y > 0) {
            if (!borderdata[(y - 1) * size.Width + x]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (x < size.Width - 1) {
            if (!borderdata[y * size.Width + (x + 1)]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (y < size.Height - 1) {
            if (!borderdata[(y + 1) * size.Width + x]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        if (x > 0) {
            if (!borderdata[y * size.Width + (x - 1)]) {
                return true;
            }
        } else if (ValidPoint(x, y)) {
            return true;
        }

        return false;
    }

    private Point getFirstPoint(PointData data) {
        Point startpos = zeropoint;
        for (int y = 0; y < size.Height; y++) {
            for (int x = 0; x < size.Width; x++) {
                if (data[y * size.Width + x]) {
                    startpos = new Point(x, y);
                    return startpos;
                }
            }
        }
        return startpos;
    }

    private PointData getBorderData(byte[] bytes) {

        PointData isborderpoint = new PointData(size.Height * size.Width);
        bool prevtrans = false;
        bool currenttrans = false;
        for (int y = 0; y < size.Height; y++) {
            prevtrans = false;
            for (int x = 0; x <= size.Width; x++) {
                if (x == size.Width) {
                    if (!prevtrans) {
                        isborderpoint.SetPoint(y * size.Width + x - 1, true);
                    }
                    continue;
                }
                currenttrans = bytes[y * stride + x * 4 + 3] == 0;
                if (x == 0 && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                if (prevtrans && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x - 1, true);
                if (!prevtrans && currenttrans && x != 0)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                prevtrans = currenttrans;
            }
        }
        for (int x = 0; x < size.Width; x++) {
            prevtrans = false;
            for (int y = 0; y <= size.Height; y++) {
                if (y == size.Height) {
                    if (!prevtrans) {
                        isborderpoint.SetPoint((y - 1) * size.Width + x, true);
                    }
                    continue;
                }
                currenttrans = bytes[y * stride + x * 4 + 3] == 0;
                if(y == 0 && !currenttrans)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                if (prevtrans && !currenttrans)
                    isborderpoint.SetPoint((y - 1) * size.Width + x, true);
                if (!prevtrans && currenttrans && y != 0)
                    isborderpoint.SetPoint(y * size.Width + x, true);
                prevtrans = currenttrans;
            }
        }
        return isborderpoint;
    }
}

class PointData {
    bool[] points = null;
    int validpoints = 0;
    public PointData(int length) {
        points = new bool[length];
    }

    public int PointCount {
        get {
            return validpoints;
        }
    }

    public void SetPoint(int pos, bool state) {
        if (points[pos] != state) {
            if (state)
                validpoints++;
            else
                validpoints--;
        }
        points[pos] = state;
    }
    public bool this[int pos] {
        get {
            return points[pos];
        }
    }

}

J'ai modifié GetOutlinePoints en ajoutant une variable d'aide qui vérifie la position à laquelle les nouveaux points doivent être ajoutés.

L'idée de l'algorithme de détection de contour dans votre code est quelque chose comme regarder l'image en se tenant à chacun de ses bords et en écrivant tous les pixels non transparents qui sont visibles. C'est correct, cependant, vous avez toujours ajouté des pixels à la fin de la collection, ce qui a causé des problèmes. J'ai ajouté une variable qui se souvient de la position du dernier pixel visible du bord précédent et du bord actuel et l'utilise pour déterminer l'indice où le nouveau pixel doit être ajouté. Je suppose que cela devrait fonctionner tant que le contour est continu, mais je suppose que c'est le cas dans votre cas.

Je l'ai testé sur plusieurs images et cela semble fonctionner correctement :

public static Point[] GetOutlinePoints(Bitmap image)
    {
        List outlinePoints = new List();

        BitmapData bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        byte[] originalBytes = new byte[image.Width * image.Height * 4];
        Marshal.Copy(bitmapData.Scan0, originalBytes, 0, originalBytes.Length);
        //find non-transparent pixels visible from the top of the image
        for (int x = 0; x < bitmapData.Width; x++)
        {
            for (int y = 0; y < bitmapData.Height; y++)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                if (alpha != 0)
                {
                    Point p = new Point(x, y);

                    if (!ContainsPoint(outlinePoints, p))
                        outlinePoints.Add(p);

                    break;
                }
            }
        }

        //helper variable for storing position of the last pixel visible from both sides 
        //or last inserted pixel
        int? lastInsertedPosition = null;
        //find non-transparent pixels visible from the right side of the image
        for (int y = 0; y < bitmapData.Height; y++)
        {
            for (int x = bitmapData.Width - 1; x >= 0; x--)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                if (alpha != 0)
                {
                    Point p = new Point(x, y);

                    if (!ContainsPoint(outlinePoints, p))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

        lastInsertedPosition = null;
        //find non-transparent pixels visible from the bottom of the image
        for (int x = bitmapData.Width - 1; x >= 0; x--)
        {
            for (int y = bitmapData.Height - 1; y >= 0; y--)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                if (alpha != 0)
                {
                    Point p = new Point(x, y);

                    if (!ContainsPoint(outlinePoints, p))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

        lastInsertedPosition = null;
        //find non-transparent pixels visible from the left side of the image
        for (int y = bitmapData.Height - 1; y >= 0; y--)
        {
            for (int x = 0; x < bitmapData.Width; x++)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                if (alpha != 0)
                {
                    Point p = new Point(x, y);

                    if (!ContainsPoint(outlinePoints, p))
                    {
                        if (lastInsertedPosition.HasValue)
                        {
                            outlinePoints.Insert(lastInsertedPosition.Value + 1, p);
                            lastInsertedPosition += 1;
                        }
                        else
                        {
                            outlinePoints.Add(p);
                        }
                    }
                    else
                    {
                        //save last common pixel from visible from more than one sides
                        lastInsertedPosition = outlinePoints.IndexOf(p);
                    }

                    break;
                }
            }
        }

        // Added to close the loop
        outlinePoints.Add(outlinePoints[0]);

        image.UnlockBits(bitmapData);

        return outlinePoints.ToArray();
    }

Mise à jour :
Cet algorithme ne fonctionnera pas correctement pour les images qui ont des parties de contour non "visibles" depuis l'un des bords. Voir les commentaires pour les solutions suggérées. Je vais essayer de poster un extrait de code plus tard.

Mise à jour II

J'ai préparé un autre algorithme comme décrit dans mes commentaires. Il se contente de ramper autour de l'objet et de sauvegarder le contour.

D'abord, il trouve le premier pixel du contour en utilisant une méthode de l'algorithme précédent. Ensuite, il parcourt les pixels voisins dans le sens des aiguilles d'une montre, trouve le premier qui est transparent, puis continue à parcourir, mais en cherchant un non-transparent. Le premier pixel non-transparent trouvé est le suivant dans le contour. La boucle continue jusqu'à faire le tour complet de l'objet et revenir au pixel de départ.

public static Point[] GetOutlinePointsNEW(Bitmap image)
    {
        List outlinePoints = new List();

        BitmapData bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        Point currentP = new Point(0, 0);
        Point firstP = new Point(0, 0);

        byte[] originalBytes = new byte[image.Width * image.Height * 4];
        Marshal.Copy(bitmapData.Scan0, originalBytes, 0, originalBytes.Length);
        //find non-transparent pixels visible from the top of the image
        for (int x = 0; x < bitmapData.Width && outlinePoints.Count == 0; x++)
        {
            for (int y = 0; y < bitmapData.Height && outlinePoints.Count == 0; y++)
            {
                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                if (alpha != 0)
                {
                    Point p = new Point(x, y);

                    outlinePoints.Add(p);
                    currentP = p;
                    firstP = p;

                    break;
                }
            }
        }

        Point[] neighbourPoints = new Point[] { new Point(-1, -1), new Point(0, -1), new Point(1, -1), 
                                                new Point(1, 0), new Point(1, 1), new Point(0, 1), 
                                                new Point(-1, 1), new Point(-1, 0) };

        //crawl around the object and look for the next pixel of the outline
        do
        {
            bool transparentNeighbourFound = false;
            bool nextPixelFound = false;
            int i;
            //searching is done in clockwise order
            for (i = 0; (i < neighbourPoints.Length * 2) && !nextPixelFound; ++i)
            {
                int neighbourPosition = i % neighbourPoints.Length;

                int x = currentP.X + neighbourPoints[neighbourPosition].X;
                int y = currentP.Y + neighbourPoints[neighbourPosition].Y;

                byte alpha = originalBytes[y * bitmapData.Stride + 4 * x + 3];

                //a transparent pixel has to be found first
                if (!transparentNeighbourFound)
                {
                    if (alpha == 0)
                    {
                        transparentNeighbourFound = true;
                    }
                }
                else //after a transparent pixel is found, a next non-transparent one is the next pixel of the outline
                {
                    if (alpha != 0)
                    {
                        Point p = new Point(x, y);

                        currentP = p;
                        outlinePoints.Add(p);
                        nextPixelFound = true;
                    }
                }
            }
        } while (currentP != firstP);

        image.UnlockBits(bitmapData);

        return outlinePoints.ToArray();
    }

Une chose à retenir est que cela fonctionne SI l'objet ne se termine PAS au bord de l'image (il doit y avoir un espace transparent entre l'objet et chacun des bords).

Cela peut être fait facilement si vous rendez l'image plus grande d'une ligne de chaque côté avant de la passer à la fonction GetOutlinePointsNEW méthode.

N'oubliez pas de communiquer ce post si cela en valait la peine.



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.