using SkiaSharp; namespace MarketData.CNNProcessing { public class ImageHelper : IDisposable { private SKBitmap bitmap = null; private PointMapping pointMapping; public ImageHelper() { } /// /// Copy constructor /// /// public ImageHelper(ImageHelper imageHelper) { this.bitmap=Copy(imageHelper.bitmap); pointMapping = new PointMapping(imageHelper.pointMapping); } public void Dispose() { DisposeAll(); } private void DisposeAll() { if(null!=bitmap) { bitmap.Dispose(); bitmap=null; } } private SKBitmap Copy(SKBitmap bitmap) { return bitmap.Copy(bitmap.ColorType); } /// /// Load image from file /// /// /// public bool LoadImage(string pathFileName) { using FileStream stream = new FileStream(pathFileName, FileMode.Open); return LoadImage(stream); } /// /// Load image from stream retaining the color space and pixel depth of the source image /// /// /// public bool LoadImage(Stream stream) { try { DisposeAll(); using SKImage image=SKImage.FromEncodedData(stream); SKImageInfo imageInfo = image.Info; bitmap = new SKBitmap(image.Info); if(!image.ReadPixels (imageInfo, bitmap.GetPixels (), imageInfo.RowBytes,0, 0)) { bitmap.Dispose (); bitmap = null; return false; } pointMapping = new PointMapping(Width,Height,Width,0,Height,0); return true; } catch(Exception exception) { MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); return false; } } /// /// Resize image to given width maintaining aspect ration for height. /// If aspect ration is used for CNN then you will need to introduce padding. For instance 640,640 image and then paste the aspect /// mainained image onto that. The CNN will then have to learn that black means nothing. Resizing is the recommended way to go. /// /// /// /// public bool Resize(int newWidth) { double aspectRatio=(double)bitmap.Width/(double)bitmap.Height; int newHeight=(int)(((double)newWidth)/aspectRatio); if(0!=newHeight%2)newHeight++; return Resize(newWidth,newHeight); } /// /// Resize image to given width and height. The documenation on CNN's indicates that resizing will work better than maintaining apsect /// ration with padding /// public bool Resize(int newWidth, int newHeight) { try { Validate(); SKImageInfo imageInfo = new SKImageInfo(newWidth, newHeight); SKBitmap newBitmap = bitmap.Resize(imageInfo,SKSamplingOptions.Default); bitmap.Dispose(); bitmap = newBitmap; pointMapping = new PointMapping(Width,Height,Width,0,Height,0); return true; } catch(Exception exception) { MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); return false; } } /// /// Create a new 32 bit bitmap with 8 bits per channel which includes 8 bits for the alpha channel where 0 = fully transparent and 255 = fully opaque /// /// /// /// public void CreateImage(int width, int height,PointMapping pointMapping) { DisposeAll(); this.pointMapping=pointMapping; bitmap=new SKBitmap(width,height,SKColorType.Rgba8888, SKAlphaType.Premul); Validate(); } /// /// Create a new 32 bit bitmap with 8 bits per channel which includes 8 bits for the alpha channel where 0 = fully transparent and 255 = fully opaque /// /// /// public void CreateImage(int width, int height) { DisposeAll(); this.pointMapping = new PointMapping(width, height, width-1, 0, height-1, 0, 0,0); bitmap=new SKBitmap(width,height,SKColorType.Rgba8888, SKAlphaType.Premul); Validate(); } /// /// Save the bitmap as black and white jpeg and return the stream /// This is the method that is currently being used in the CNNClient.Predict to send the image stream to Flask /// /// public Stream SaveBlackAndWhiteJPG() { Validate(); SKBitmap bwBitmap=ToBlackAndWhite(bitmap); Stream memoryStream=new MemoryStream(); BitmapExtensions.SaveJPG100(bwBitmap,memoryStream); memoryStream.Position=0; bwBitmap.Dispose(); return memoryStream; } /// /// Convert bitmap to stream for use in CNNClient /// /// /// public Stream ToStream() { Validate(); Stream memoryStream=new MemoryStream(); BitmapExtensions.SaveJPG100(bitmap,memoryStream); memoryStream.Position=0; return memoryStream; } /// /// Save the bitmap as gray scale jpeg to the specified file /// /// /// public bool SaveGrayScaleJPG(String pathFileName) { Validate(); SKBitmap bwBitmap=ToGrayScale(bitmap); BitmapExtensions.SaveJPG100(bwBitmap,pathFileName); bwBitmap.Dispose(); return true; } /// /// Save the bitmap as black and white jpeg to the specified file /// /// /// public bool SaveBlackAndWhiteJPG(String pathFileName) { Validate(); SKBitmap bwBitmap=ToBlackAndWhite(bitmap); BitmapExtensions.SaveJPG100(bwBitmap,pathFileName); bwBitmap.Dispose(); return true; } /// /// Save the bitmap to the specified file /// /// public void Save(String pathFileName) { Save(pathFileName,null); } private void Save(String pathFileName,SKBitmap altBitmap=null) { Validate(); if(null==altBitmap)BitmapExtensions.SaveJPG100(bitmap,pathFileName); else BitmapExtensions.SaveJPG100(altBitmap,pathFileName); } /// /// Save the bitmap in PNG format. Note: PNG files support alpha channels, JPG files do not /// /// /// public void SavePng(String pathFileName,SKBitmap altBitmap=null) { Validate(); if(null==altBitmap)BitmapExtensions.SavePNG100(bitmap,pathFileName); else BitmapExtensions.SavePNG100(altBitmap,pathFileName); } // Convert to 8 bits per pixel black and white private SKBitmap ToBlackAndWhite(SKBitmap bitmap) { float n = 1.0f/3.0f; SKColorFilter bwColorFilter = SKColorFilter.CreateColorMatrix(new float[] { n, n, n, 0, 0, n, n, n, 0, 0, n, n, n, 0, 0, 0, 0, 0, 1, 0 }); SKBitmap dstBitmap = new SKBitmap(bitmap.Width, bitmap.Height,SKColorType.Gray8, SKAlphaType.Premul); using SKCanvas canvas = new SKCanvas(dstBitmap); using SKPaint paint = new SKPaint(); paint.ColorFilter = bwColorFilter; canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint); return dstBitmap; } /// /// Rotates the bitmap right by 90 degrees /// /// public bool RotateRight() { Validate(); SKBitmap rotated = Rotate(bitmap, 90); bitmap.Dispose(); bitmap=rotated; pointMapping = new PointMapping(Width,Height,Width,0,Height,0); return true; } /// /// Rotates the bitmap left by 90 degrees /// /// public bool RotateLeft() { Validate(); SKBitmap rotated = Rotate(bitmap, -90); bitmap.Dispose(); bitmap=rotated; pointMapping = new PointMapping(Width,Height,Width,0,Height,0); return true; } /// /// Rotates the bitmap to the specified angle /// /// /// /// private SKBitmap Rotate(SKBitmap bitmap, double angle) { double radians = Math.PI * angle / 180; float sine = (float)Math.Abs(Math.Sin(radians)); float cosine = (float)Math.Abs(Math.Cos(radians)); int originalWidth = bitmap.Width; int originalHeight = bitmap.Height; int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight); int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth); SKBitmap rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight); using (SKCanvas surface = new SKCanvas(rotatedBitmap)) { surface.Clear(); surface.Translate(rotatedWidth / 2, rotatedHeight / 2); surface.RotateDegrees((float)angle); surface.Translate(-originalWidth / 2, -originalHeight / 2); surface.DrawBitmap(bitmap, new SKPoint()); } return rotatedBitmap; } public void ToGrayScale() { Validate(); SKBitmap grayScaleBitmap = ToGrayScale(bitmap); bitmap.Dispose(); bitmap=grayScaleBitmap; } // Convert to 8 bits per pixel black and white private SKBitmap ToGrayScale(SKBitmap bitmap) { SKColorFilter grayScaleColorFilter = SKColorFilter.CreateColorMatrix(new float[] { 0.2126f, 0.7152f, 0.0722f, 0, 0, 0.2126f, 0.7152f, 0.0722f, 0, 0, 0.2126f, 0.7152f, 0.0722f, 0, 0, 0, 0, 0, 1, 0 }); SKBitmap dstBitmap = new SKBitmap(bitmap.Width, bitmap.Height,SKColorType.Gray8, SKAlphaType.Premul); using SKCanvas canvas = new SKCanvas(dstBitmap); using SKPaint paint = new SKPaint(); paint.ColorFilter = grayScaleColorFilter; canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint); return dstBitmap; } /// /// Adds a blur effect to the bitmap /// /// /// public bool Blur(float sigmaX) { Validate(); SKBitmap blurredBitmap = Blur(bitmap, sigmaX); bitmap.Dispose(); bitmap = blurredBitmap; return true; } /// /// Applies a blur effect to the bitmap /// /// /// /// /// private SKBitmap Blur(SKBitmap image, float sigmaX=5.0f, float sigmaY=5.0f) { SKImageFilter imageFilter = SKImageFilter.CreateBlur(sigmaX, sigmaY); SKBitmap blurredBitmap = new SKBitmap(image.Width, image.Height); using SKCanvas canvas = new SKCanvas(blurredBitmap); using SKPaint paint = new SKPaint() { ImageFilter = imageFilter }; canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint); return blurredBitmap; } /// /// GetPixel - No point translation /// /// /// /// public SKColor GetPixel(int x,int y) { Validate(); return bitmap.GetPixel(x, y); } /// /// SetPixel - No point translation /// /// /// /// public void SetPixel(int x, int y,SKColor color) { Validate(); bitmap.SetPixel(x, y, color); } public SKPoint TranslatePoint(SKPoint point) { return pointMapping.TranslatePoint(point); } public void Validate() { if(null==bitmap)throw new InvalidDataException("The image has not been initialized"); } public int Width { get { return bitmap.Width; } } public int Height { get { return bitmap.Height; } } /// /// Fills the rectangle with the specified color /// /// public void Fill(SKColor color) { Validate(); SKRectI rect = new SKRectI(0, 0, Width, Height); // x, y, width, height bitmap.Erase(color, rect); } /// /// Make the bitmap transparent. Note: PNG format preserves alpha channel while JPG format does not.. so save as PNG if using transparency /// public void Transparent(SKColor color) { Validate(); int pixelCount = Width * Height; SKColor[] colors = new SKColor[pixelCount]; SKColor transparent = new SKColor(color.Red, color.Green, color.Blue, 0); for (int index = 0; index < pixelCount; index++) { colors[index] = transparent; } bitmap.Pixels = colors; } /// /// DrawPoint - With translation /// /// /// public void DrawPoint(SKColor color, SKPoint drawPoint) { Validate(); using SKCanvas canvas = new SKCanvas(bitmap); canvas.Clear(); SKPoint txPoint = pointMapping.MapPoint(drawPoint); canvas.DrawPoint(drawPoint, color); } /// /// DrawPoint - with given strokeWidth and translation /// /// /// /// public void DrawPoint(SKColor color, float strokeWidth, SKPoint drawPoint) { Validate(); SKPoint txPoint = pointMapping.MapPoint(drawPoint); using SKCanvas canvas = new SKCanvas(bitmap); using SKPaint paint = new SKPaint(); paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = strokeWidth; // Set the desired stroke width paint.Color = SKColors.Black; SKPoint[] points = new SKPoint[] { txPoint}; canvas.DrawPoints(SKPointMode.Points, points, paint); } /// /// Add noise to the image /// /// /// public void AddNoise(SKColor color,double percentDecimal) { Random random=new Random(); int amount=(int)(percentDecimal*(Width*Height)); for(int index=0;index /// Draw a line on the bitmap /// /// /// /// /// public void DrawLine(SKColor color, float strokeWidth, SKPoint srcPoint, SKPoint dstPoint) { Validate(); SKPoint txSrcPoint = pointMapping.MapPoint(srcPoint); SKPoint txDstPoint = pointMapping.MapPoint(dstPoint); using SKCanvas canvas = new SKCanvas(bitmap); using SKPaint paint = new SKPaint(); paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = strokeWidth; // Set the desired stroke width paint.Color = color; canvas.DrawLine(txSrcPoint, txDstPoint, paint); } /// /// Draw text on the bitmap /// /// /// /// /// public void DrawText(String text, SKPoint srcPoint, SKColor color, SKTextAlign align, SKFont font,SKPaintStyle paintStyle=SKPaintStyle.Fill,float strokeWidth = 1) { Validate(); SKPoint txSrcPoint = pointMapping.MapPoint(srcPoint); using SKCanvas canvas = new SKCanvas(bitmap); using SKPaint paint = new SKPaint(); paint.Style = paintStyle; paint.StrokeWidth = strokeWidth; // Set the desired stroke width paint.Color = color; canvas.DrawText(text,srcPoint, align, font, paint); } /// /// Create a rectangle with the specified text /// /// /// /// /// /// /// /// public void CreateBoundedText(String text, SKColor textColor, SKColor fillColor, SKTextAlign align, SKFont font, SKPaintStyle paintStyle = SKPaintStyle.Fill, float strokeWidth = 1) { SKPoint srcPoint = new SKPoint(2, (int)font.Size + 4); int width = GetTextLength(text, font) + 4; int height = (int)font.Size * 2; CreateImage(width, height); Fill(fillColor); DrawLine(SKColors.Black, 1, new SKPoint(0, 0), new SKPoint(width - 1, 0)); // bottom left to right DrawLine(SKColors.Black, 1, new SKPoint(width - 1, 0), new SKPoint(width - 1, height - 1)); // up lefthand side DrawLine(SKColors.Black, 1, new SKPoint(0, height - 1), new SKPoint(width - 1, height - 1)); // top left to right DrawLine(SKColors.Black, 1, new SKPoint(0, height - 1), new SKPoint(0, 0)); // left hand side top to bottom DrawText(text, srcPoint, textColor, align, font); } /// /// Gets the length of the text /// /// /// /// /// /// public int GetTextLength(String text, SKFont font, SKPaintStyle paintStyle = SKPaintStyle.Fill, float strokeWidth = 1) { using SKPaint paint = new SKPaint(); paint.Style = paintStyle; paint.StrokeWidth = strokeWidth; // Set the desired stroke width paint.Color = SKColors.Transparent; SKRect rect = new SKRect(0, 0, 0, 0); float textLength = font.MeasureText(text, out rect, paint); return (int)textLength; } /// /// Draws the path along the line segments /// /// /// /// public void DrawPath(SKColor color, float strokeWidth, LineSegments lineSegments) { Validate(); using SKCanvas canvas = new SKCanvas(bitmap); using SKPaint paint = new SKPaint(); paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = strokeWidth; // Set the desired stroke width paint.Color = SKColors.Black; foreach (LineSegment lineSegment in lineSegments) { SKPoint txSrcPoint = pointMapping.MapPoint(lineSegment.P1); SKPoint txDstPoint = pointMapping.MapPoint(lineSegment.P2); canvas.DrawLine(txSrcPoint, txDstPoint, paint); } } /// /// Draws a circle on the bitmap /// /// /// /// /// public bool DrawCircle(SKColor color, SKPoint center, float radius = 1.00f) { Validate(); SKPoint txPointCenter = pointMapping.MapPoint(center); using SKPaint paint = new SKPaint(); paint.Color = color; paint.Style = SKPaintStyle.Fill; using SKCanvas canvas = new SKCanvas(bitmap); canvas.DrawCircle(txPointCenter, radius, paint); return true; } /// /// Draw a triangle centered inside the bounded area defined by the width and height of the bitmap. /// /// The paint stroke to to use to outline the triangle /// The paint stroke to use to fill the triangle public void DrawTriangle(SKPaint paintStroke, SKPaint paintFill) { Validate(); using SKPath path = new SKPath(); float hLength = (float)Width / 2f; SKPoint txOrigin = pointMapping.MapPoint(new SKPoint(hLength,Height)); using SKCanvas canvas = new SKCanvas(bitmap); path.MoveTo(txOrigin.X, txOrigin.Y); path.LineTo(txOrigin.X - hLength, txOrigin.Y + Height - 1f); path.LineTo(txOrigin.X + hLength - 1f, txOrigin.Y + Height - 1f); path.LineTo(txOrigin.X, txOrigin.Y); path.Close(); canvas.DrawPath(path, paintStroke); canvas.DrawPath(path, paintFill); } /// /// Draw a triangle centered at the specified 'origin' and being 'length' width at it's base /// /// The point origin /// The length of the base of the triangle /// The paint stroke to to use to outline the triangle /// The paint stroke to use to fill the triangle public void DrawTriangle(SKPoint origin, int length, SKPaint paintStroke, SKPaint paintFill) { Validate(); SKPath path = new SKPath(); SKPoint txOrigin = pointMapping.MapPoint(origin); float lengthF = (float)length; float hLength = lengthF / 2f; float height = (float)Math.Sqrt(Math.Pow(lengthF, 2) - Math.Pow(hLength, 2)); height/=1.15f; // reduce the height by 15% path.MoveTo(txOrigin.X, txOrigin.Y); path.LineTo(txOrigin.X - hLength, txOrigin.Y + height); path.LineTo(txOrigin.X + hLength, txOrigin.Y + height); path.LineTo(txOrigin.X, txOrigin.Y); path.Close(); using SKCanvas canvas = new SKCanvas(bitmap); canvas.DrawPath(path, paintStroke); canvas.DrawPath(path, paintFill); } } }