첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
728x90
반응형
728x170

TestProject.zip
다운로드

▶ DistanceType.cs

namespace TestProject
{
    /// <summary>
    /// 거리 타입
    /// </summary>
    public enum DistanceType
    {
        /// <summary>
        /// 유클리드
        /// </summary>
        Euclidean,

        /// <summary>
        /// 맨하탄
        /// </summary>
        Manhattan,

        /// <summary>
        /// 체비쇼프
        /// </summary>
        Chebyshev
    }
}

 

728x90

 

▶ Pixel.cs

namespace TestProject
{
    /// <summary>
    /// 픽셀
    /// </summary>
    public class Pixel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 오프셋 X
        /// </summary>
        private int xOffset = 0;

        /// <summary>
        /// 오프셋 Y
        /// </summary>
        private int yOffset = 0;

        /// <summary>
        /// 청색
        /// </summary>
        private byte blue = 0;

        /// <summary>
        /// 녹색
        /// </summary>
        private byte green = 0;

        /// <summary>
        /// 적색
        /// </summary>
        private byte red = 0;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region X 오프셋 - XOffset

        /// <summary>
        /// X 오프셋
        /// </summary>
        public int XOffset
        {
            get
            {
                return this.xOffset;
            }
            set
            {
                this.xOffset = value;
            }
        }

        #endregion
        #region Y 오프셋 - YOffset

        /// <summary>
        /// Y 오프셋
        /// </summary>
        public int YOffset
        {
            get
            {
                return this.yOffset;
            }
            set
            {
                this.yOffset = value;
            }
        }

        #endregion
        #region 청색 - Blue

        /// <summary>
        /// 청색
        /// </summary>
        public byte Blue
        {
            get
            {
                return this.blue;
            }
            set
            {
                this.blue = value;
            }
        }

        #endregion
        #region 녹색 - Green

        /// <summary>
        /// 녹색
        /// </summary>
        public byte Green
        {
            get
            {
                return this.green;
            }
            set
            {
                this.green = value;
            }
        }

        #endregion
        #region 적색 - Red

        /// <summary>
        /// 적색
        /// </summary>
        public byte Red
        {
            get
            {
                return this.red;
            }
            set
            {
                this.red = value;
            }
        }

        #endregion
    }
}

 

300x250

 

▶ VoronoiPoint.cs

using System.Collections.Generic;

namespace TestProject
{
    /// <summary>
    /// 보로노이 포인트
    /// </summary>
    public class VoronoiPoint
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 픽셀 리스트
        /// </summary>
        private List<Pixel> pixelList = new List<Pixel>();

        /// <summary>
        /// X 오프셋
        /// </summary>
        private int xOffset = 0;

        /// <summary>
        /// Y 오프셋
        /// </summary>
        private int yOffset = 0;

        /// <summary>
        /// 청색 합계
        /// </summary>
        private int blueTotal = 0;

        /// <summary>
        /// 녹색 합계
        /// </summary>
        private int greenTotal = 0;

        /// <summary>
        /// 적색 합계
        /// </summary>
        private int redTotal = 0;

        /// <summary>
        /// 청색 평균
        /// </summary>
        private int blueAverage = 0;

        /// <summary>
        /// 녹색 평균
        /// </summary>
        private int greenAverage = 0;

        /// <summary>
        /// 적색 평균
        /// </summary>
        private int redAverage = 0;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 픽셀 리스트 - PixelList

        /// <summary>
        /// 픽셀 리스트
        /// </summary>
        public List<Pixel> PixelList
        {
            get
            {
                return this.pixelList;
            }
        }

        #endregion
        #region X 오프셋 - XOffset

        /// <summary>
        /// X 오프셋
        /// </summary>
        public int XOffset
        {
            get
            {
                return this.xOffset;
            }
            set
            {
                this.xOffset = value;
            }
        }

        #endregion
        #region Y 오프셋 - YOffset

        /// <summary>
        /// Y 오프셋
        /// </summary>
        public int YOffset
        {
            get
            {
                return this.yOffset;
            }
            set
            {
                this.yOffset = value;
            }
        }

        #endregion
        #region 청색 합계 - BlueTotal

        /// <summary>
        /// 청색 합계
        /// </summary>
        public int BlueTotal
        {
            get
            {
                return this.blueTotal;
            }
            set
            {
                this.blueTotal = value;
            }
        }

        #endregion
        #region 녹색 합계 - GreenTotal

        /// <summary>
        /// 녹색 합계
        /// </summary>
        public int GreenTotal
        {
            get
            {
                return this.greenTotal;
            }
            set
            {
                this.greenTotal = value;
            }
        }

        #endregion
        #region 적색 합계 - RedTotal

        /// <summary>
        /// 적색 합계
        /// </summary>
        public int RedTotal
        {
            get
            {
                return this.redTotal;
            }
            set
            {
                this.redTotal = value;
            }
        }

        #endregion
        #region 청색 평균 - BlueAverage

        /// <summary>
        /// 청색 평균
        /// </summary>
        public int BlueAverage
        {
            get
            {
                return this.blueAverage;
            }
        }

        #endregion
        #region 녹색 평균 - GreenAverage

        /// <summary>
        /// 녹색 평균
        /// </summary>
        public int GreenAverage
        {
            get
            {
                return this.greenAverage;
            }
        }

        #endregion
        #region 적색 평균 - RedAverage

        /// <summary>
        /// 적색 평균
        /// </summary>
        public int RedAverage
        {
            get
            {
                return this.redAverage;
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 픽셀 추가하기 - AddPixel(pixel)

        /// <summary>
        /// 픽셀 추가하기
        /// </summary>
        /// <param name="pixel">픽셀</param>
        public void AddPixel(Pixel pixel)
        {
            this.blueTotal  += pixel.Blue;
            this.greenTotal += pixel.Green;
            this.redTotal   += pixel.Red;

            this.pixelList.Add(pixel);
        }

        #endregion
        #region 평균 계산하기 - CalculateAverage()

        /// <summary>
        /// 평균 계산하기
        /// </summary>
        public void CalculateAverage()
        {
            if(this.pixelList.Count > 0)
            {
                this.blueAverage  = this.blueTotal  / this.pixelList.Count;
                this.greenAverage = this.greenTotal / this.pixelList.Count;
                this.redAverage   = this.redTotal   / this.pixelList.Count;
            }
        }

        #endregion
    }
}

 

▶ BitmapHelper.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;

namespace TestProject
{
    /// <summary>
    /// 비트맵 헬퍼
    /// </summary>
    public static class BitmapHelper
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 제곱근 딕셔너리
        /// </summary>
        private static Dictionary <int, int> _squareRootDictionary = new Dictionary<int, int>();

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 비트맵 로드하기 - LoadBitmap(filePath)

        /// <summary>
        /// 비트맵 로드하기
        /// </summary>
        /// <param name="filePath">파일 경로</param>
        /// <returns>비트맵</returns>
        public static Bitmap LoadBitmap(string filePath)
        {
            using(Bitmap bitmap = new Bitmap(filePath))
            {
                return new Bitmap(bitmap);
            }
        }

        #endregion
        #region 스테인 글라스 필터 적용하기 - ApplyStainedGlassFilter(sourceBitmap, blockSize, blockFactor, distanceType, highlightEdge, edgeThreshold, edgeColor)

        /// <summary>
        /// 스테인 글라스 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="blockSize">블럭 크기</param>
        /// <param name="blockFactor">블럭 팩터</param>
        /// <param name="distanceType">거리 타입</param>
        /// <param name="highlightEdge">가장자리 강조 여부</param>
        /// <param name="edgeThreshold">가장자리 임계치</param>
        /// <param name="edgeColor">가장자리 색상</param>
        /// <returns>비트맵</returns>
        public static Bitmap ApplyStainedGlassFilter
        (
            Bitmap       sourceBitmap,
            int          blockSize,
            double       blockFactor,
            DistanceType distanceType,
            bool         highlightEdge,
            byte         edgeThreshold,
            Color        edgeColor
        )
        {
            BitmapData sourceBitmapData = sourceBitmap.LockBits
            (
                new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb
            );

            byte[] sourceByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];
            byte[] targetByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];

            Marshal.Copy(sourceBitmapData.Scan0, sourceByteArray, 0, sourceByteArray.Length);

            sourceBitmap.UnlockBits(sourceBitmapData);

            int neighbourHoodTotal   = 0;
            int sourceOffset         = 0;
            int targetOffset         = 0;
            int currentPixelDistance = 0;
            int nearestPixelDistance = 0;
            int nearestPointIndex    = 0;

            Random random = new Random();

            List<VoronoiPoint> randomPointList = new List<VoronoiPoint>();

            for(int row = 0; row < sourceBitmap.Height - blockSize; row += blockSize)
            {
                for(int column = 0; column < sourceBitmap.Width - blockSize; column += blockSize)
                {
                    sourceOffset = row * sourceBitmapData.Stride + column * 4;

                    neighbourHoodTotal = 0;

                    for(int y = 0; y < blockSize; y++)
                    {
                        for(int x = 0; x < blockSize; x++)
                        {
                            targetOffset = sourceOffset + y * sourceBitmapData.Stride + x * 4;

                            neighbourHoodTotal += sourceByteArray[targetOffset    ];
                            neighbourHoodTotal += sourceByteArray[targetOffset + 1];
                            neighbourHoodTotal += sourceByteArray[targetOffset + 2];
                        }
                    }

                    random = new Random(neighbourHoodTotal);

                    VoronoiPoint randomPoint = new VoronoiPoint();

                    randomPoint.XOffset = random.Next(0, blockSize) + column;
                    randomPoint.YOffset = random.Next(0, blockSize) + row;

                    randomPointList.Add(randomPoint);
                }
            }

            int rowOffset    = 0;
            int columnOffset = 0;

            for(int bufferOffset = 0; bufferOffset < sourceByteArray.Length - 4; bufferOffset += 4)
            {
                rowOffset    =  bufferOffset / sourceBitmapData.Stride;
                columnOffset = (bufferOffset % sourceBitmapData.Stride) / 4;

                currentPixelDistance = 0;
                nearestPixelDistance = blockSize * 4;
                nearestPointIndex    = 0;

                List<VoronoiPoint> subsidaryPointList = new List<VoronoiPoint>();

                subsidaryPointList.AddRange
                (
                    from   randomPoint in randomPointList
                    where  rowOffset >= randomPoint.YOffset - blockSize * 2 &&
                           rowOffset <= randomPoint.YOffset + blockSize * 2
                    select randomPoint
                );

                for(int k = 0; k < subsidaryPointList.Count; k++)
                {
                    if(distanceType == DistanceType.Euclidean)
                    {
                        currentPixelDistance = CalculateDistanceEuclidean
                        (
                            subsidaryPointList[k].XOffset,
                            columnOffset,
                            subsidaryPointList[k].YOffset,
                            rowOffset
                        );
                    }
                    else if(distanceType == DistanceType.Manhattan)
                    {
                        currentPixelDistance = CalculateDistanceManhattan
                        (
                            subsidaryPointList[k].XOffset,
                            columnOffset,
                            subsidaryPointList[k].YOffset,
                            rowOffset
                        );
                    }
                    else if(distanceType == DistanceType.Chebyshev)
                    {
                        currentPixelDistance = CalculateDistanceChebyshev
                        (
                            subsidaryPointList[k].XOffset,
                            columnOffset,
                            subsidaryPointList[k].YOffset,
                            rowOffset
                        );
                    }

                    if(currentPixelDistance <= nearestPixelDistance)
                    {
                        nearestPixelDistance = currentPixelDistance;
                        nearestPointIndex    = k;

                        if(nearestPixelDistance <= blockSize / blockFactor)
                        {
                            break;
                        }
                    }
                }

                Pixel pixel = new Pixel ();

                pixel.XOffset = columnOffset;
                pixel.YOffset = rowOffset;
                pixel.Blue    = sourceByteArray[bufferOffset    ];
                pixel.Green   = sourceByteArray[bufferOffset + 1];
                pixel.Red     = sourceByteArray[bufferOffset + 2];

                subsidaryPointList[nearestPointIndex].AddPixel(pixel);
            }

            for(int k = 0; k < randomPointList.Count; k++)
            {
                randomPointList[k].CalculateAverage();

                for(int i = 0; i < randomPointList[k].PixelList.Count; i++)
                {
                    targetOffset = randomPointList[k].PixelList[i].YOffset *
                                   sourceBitmapData.Stride +
                                   randomPointList[k].PixelList[i].XOffset * 4;

                    targetByteArray[targetOffset    ] = (byte)randomPointList[k].BlueAverage;
                    targetByteArray[targetOffset + 1] = (byte)randomPointList[k].GreenAverage;
                    targetByteArray[targetOffset + 2] = (byte)randomPointList[k].RedAverage;
                    targetByteArray[targetOffset + 3] = 255;
                }
            }

            Bitmap targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);

            BitmapData targetBitmapData = targetBitmap.LockBits
            (
                new Rectangle(0, 0, targetBitmap.Width, targetBitmap.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb
            );

            Marshal.Copy(targetByteArray, 0, targetBitmapData.Scan0, targetByteArray.Length);

            targetBitmap.UnlockBits(targetBitmapData);

            if(highlightEdge == true )
            {
                targetBitmap = ApplyGradientBasedEdgeDetectionFilter(targetBitmap, edgeColor, edgeThreshold);
            }

            return targetBitmap;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Private

        #region 유클리드 거리 계산하기 - CalculateDistanceEuclidean(x1, x2, y1, y2)

        /// <summary>
        /// 유클리드 거리 계산하기
        /// </summary>
        /// <param name="x1">X1</param>
        /// <param name="x2">Y1</param>
        /// <param name="y1">X2</param>
        /// <param name="y2">Y2</param>
        /// <returns>거리</returns>
        private static int CalculateDistanceEuclidean(int x1, int x2, int y1, int y2)
        {
            int square = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);

            if(_squareRootDictionary.ContainsKey(square) == false)
            {
                _squareRootDictionary.Add(square, (int)Math.Sqrt(square));
            }

            return _squareRootDictionary[square];
        }

        #endregion
        #region 맨하탄 거리 계산하기 - CalculateDistanceManhattan(x1, x2, y1, y2)

        /// <summary>
        /// 맨하탄 거리 계산하기
        /// </summary>
        /// <param name="x1">X1</param>
        /// <param name="x2">Y1</param>
        /// <param name="y1">X2</param>
        /// <param name="y2">Y2</param>
        /// <returns>거리</returns>
        private static int CalculateDistanceManhattan(int x1, int x2, int y1, int y2)
        {
            return Math.Abs(x1 - x2) + Math.Abs(y1 - y2);
        }

        #endregion
        #region 체비쇼프 거리 계산하기 - CalculateDistanceChebyshev(x1, x2, y1, y2)

        /// <summary>
        /// 체비쇼프 거리 계산하기
        /// </summary>
        /// <param name="x1">X1</param>
        /// <param name="x2">Y1</param>
        /// <param name="y1">X2</param>
        /// <param name="y2">Y2</param>
        /// <returns>거리</returns>
        private static int CalculateDistanceChebyshev(int x1, int x2, int y1, int y2)
        {
            return Math.Max(Math.Abs(x1 - x2), Math.Abs(y1 - y2));
        }

        #endregion
        #region 임계치 체크하기 - CheckThreshold(sourceByteArray, offset1, offset2, gradientValue, threshold, divideBy)

        /// <summary>
        /// 임계치 체크하기
        /// </summary>
        /// <param name="sourceByteArray">소스 바이트 배열</param>
        /// <param name="offset1">오프셋 1</param>
        /// <param name="offset2">오프셋 2</param>
        /// <param name="gradientValue">그라디언트 값</param>
        /// <param name="threshold">임계치</param>
        /// <param name="divideBy">젯수</param>
        /// <returns>임계치 체크 결과</returns>
        private static bool CheckThreshold
        (
            byte[]  sourceByteArray,
            int     offset1,
            int     offset2,
            ref int gradientValue,
            byte    threshold,
            int     divideBy = 1
        )
        {
            gradientValue += Math.Abs(sourceByteArray[offset1    ] - sourceByteArray[offset2    ]) / divideBy;
            gradientValue += Math.Abs(sourceByteArray[offset1 + 1] - sourceByteArray[offset2 + 1]) / divideBy;
            gradientValue += Math.Abs(sourceByteArray[offset1 + 2] - sourceByteArray[offset2 + 2]) / divideBy;

            return (gradientValue >= threshold);
        }

        #endregion
        #region 그라디언트 기반 가장자리 탐지 필터 적용하기 - ApplyGradientBasedEdgeDetectionFilter(sourceBitmap, edgeColor, threshold)

        /// <summary>
        /// 그라디언트 기반 가장자리 탐지 필터 적용하기
        /// </summary>
        /// <param name="sourceBitmap">소스 비트맵</param>
        /// <param name="edgeColor">가장자리 색상</param>
        /// <param name="threshold">임계치</param>
        /// <returns>비트맵</returns>
        private static Bitmap ApplyGradientBasedEdgeDetectionFilter(Bitmap sourceBitmap, Color edgeColor, byte threshold = 0)
        {
            BitmapData sourceBitmapData = sourceBitmap.LockBits
            (
                new Rectangle (0, 0, sourceBitmap.Width, sourceBitmap.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb
            );

            byte[] sourceByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];
            byte[] targetByteArray = new byte[sourceBitmapData.Stride * sourceBitmapData.Height];

            Marshal.Copy(sourceBitmapData.Scan0, sourceByteArray, 0, sourceByteArray.Length);
            Marshal.Copy(sourceBitmapData.Scan0, targetByteArray, 0, targetByteArray.Length);

            sourceBitmap.UnlockBits(sourceBitmapData);

            int  sourceOffset    = 0;
            int  gradientValue   = 0;
            bool exceedThreshold = false;

            for(int offsetY = 1; offsetY < sourceBitmap.Height - 1; offsetY++)
            {
                for(int offsetX = 1; offsetX < sourceBitmap.Width - 1; offsetX++)
                {
                    sourceOffset    = offsetY * sourceBitmapData.Stride + offsetX * 4;
                    gradientValue   = 0;
                    exceedThreshold = true;

                    CheckThreshold
                    (
                        sourceByteArray,
                        sourceOffset - 4,
                        sourceOffset + 4,
                        ref gradientValue,
                        threshold,
                        2
                    );

                    exceedThreshold = CheckThreshold
                    (
                        sourceByteArray,
                        sourceOffset - sourceBitmapData.Stride,
                        sourceOffset + sourceBitmapData.Stride,
                        ref gradientValue,
                        threshold,
                        2
                    );

                    if(exceedThreshold == false)
                    {
                        gradientValue = 0;

                        exceedThreshold = CheckThreshold
                        (
                            sourceByteArray,
                            sourceOffset - 4,
                            sourceOffset + 4,
                            ref gradientValue,
                            threshold
                        );

                        if(exceedThreshold == false)
                        {
                            gradientValue = 0;

                            exceedThreshold = CheckThreshold
                            (
                                sourceByteArray,
                                sourceOffset - sourceBitmapData.Stride,
                                sourceOffset + sourceBitmapData.Stride,
                                ref gradientValue,
                                threshold
                            );

                            if(exceedThreshold == false)
                            {
                                gradientValue = 0;

                                CheckThreshold
                                (
                                    sourceByteArray,
                                    sourceOffset - 4 - sourceBitmapData.Stride,
                                    sourceOffset + 4 + sourceBitmapData.Stride,
                                    ref gradientValue,
                                    threshold,
                                    2
                                );

                                exceedThreshold = CheckThreshold
                                (
                                    sourceByteArray,
                                    sourceOffset - sourceBitmapData.Stride + 4,
                                    sourceOffset - 4 + sourceBitmapData.Stride,
                                    ref gradientValue,
                                    threshold,
                                    2
                                );

                                if(exceedThreshold == false)
                                {
                                    gradientValue = 0;

                                    exceedThreshold = CheckThreshold
                                    (
                                        sourceByteArray,
                                        sourceOffset - 4 - sourceBitmapData.Stride,
                                        sourceOffset + 4 + sourceBitmapData.Stride,
                                        ref gradientValue,
                                        threshold
                                    );

                                    if(exceedThreshold == false)
                                    {
                                        gradientValue = 0;

                                        exceedThreshold = CheckThreshold
                                        (
                                            sourceByteArray,
                                            sourceOffset - sourceBitmapData.Stride + 4,
                                            sourceOffset + sourceBitmapData.Stride - 4,
                                            ref gradientValue,
                                            threshold
                                        );
                                    }
                                }
                            }
                        }
                    }

                    if(exceedThreshold == true)
                    {
                        targetByteArray[sourceOffset    ] = edgeColor.B;
                        targetByteArray[sourceOffset + 1] = edgeColor.G;
                        targetByteArray[sourceOffset + 2] = edgeColor.R;
                    }

                    targetByteArray[sourceOffset + 3] = 255;
                }
            }

            Bitmap targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height);

            BitmapData targetBitmapData = targetBitmap.LockBits
            (
                new Rectangle(0, 0, targetBitmap.Width, targetBitmap.Height),
                ImageLockMode.WriteOnly,
                PixelFormat.Format32bppArgb
            );

            Marshal.Copy(targetByteArray, 0, targetBitmapData.Scan0, targetByteArray.Length);

            targetBitmap.UnlockBits(targetBitmapData);

            return targetBitmap;
        }

        #endregion
    }
}

 

▶ MainForm.cs

using System.Drawing;
using System.Windows.Forms;

namespace TestProject
{
    /// <summary>
    /// 메인 폼
    /// </summary>
    public partial class MainForm : Form
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MainForm()

        /// <summary>
        /// 생성자
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            Bitmap sourceBitmap = BitmapHelper.LoadBitmap("IMAGE\\sample.jpg");
            Bitmap targetBitmap = BitmapHelper.ApplyStainedGlassFilter
            (
                sourceBitmap,
                20,
                10,
                DistanceType.Euclidean,
                true,
                1,
                Color.Black
            );

            this.pictureBox.SizeMode = PictureBoxSizeMode.Zoom;
            this.pictureBox.Image    = targetBitmap;
        }

        #endregion
    }
}
728x90
반응형
그리드형(광고전용)
Posted by icodebroker

댓글을 달아 주세요