Files
ARM64/MarketData/MarketDataLib/CNNProcessing/ImageHelper.cs
2025-07-02 11:11:35 -04:00

675 lines
23 KiB
C#

using SkiaSharp;
namespace MarketData.CNNProcessing
{
public class ImageHelper : IDisposable
{
private SKBitmap bitmap = null;
private PointMapping pointMapping;
public ImageHelper()
{
}
/// <summary>
/// Copy constructor
/// </summary>
/// <param name="imageHelper"></param>
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);
}
/// <summary>
/// Load image from file
/// </summary>
/// <param name="pathFileName"></param>
/// <returns></returns>
public bool LoadImage(string pathFileName)
{
using FileStream stream = new FileStream(pathFileName, FileMode.Open);
return LoadImage(stream);
}
/// <summary>
/// Load image from stream retaining the color space and pixel depth of the source image
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="newWidth"></param>
/// <returns></returns>
///
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);
}
/// <summary>
/// Resize image to given width and height. The documenation on CNN's indicates that resizing will work better than maintaining apsect
/// ration with padding
/// </summary>
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;
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="pointMapping"></param>
public void CreateImage(int width, int height,PointMapping pointMapping)
{
DisposeAll();
this.pointMapping=pointMapping;
bitmap=new SKBitmap(width,height,SKColorType.Rgba8888, SKAlphaType.Premul);
Validate();
}
/// <summary>
/// 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
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
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();
}
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
public Stream SaveBlackAndWhiteJPG()
{
Validate();
SKBitmap bwBitmap=ToBlackAndWhite(bitmap);
Stream memoryStream=new MemoryStream();
BitmapExtensions.SaveJPG100(bwBitmap,memoryStream);
memoryStream.Position=0;
bwBitmap.Dispose();
return memoryStream;
}
/// <summary>
/// Convert bitmap to stream for use in CNNClient
/// </summary>
/// <param name="pathFileName"></param>
/// <returns></returns>
public Stream ToStream()
{
Validate();
Stream memoryStream=new MemoryStream();
BitmapExtensions.SaveJPG100(bitmap,memoryStream);
memoryStream.Position=0;
return memoryStream;
}
/// <summary>
/// Save the bitmap as gray scale jpeg to the specified file
/// </summary>
/// <param name="pathFileName"></param>
/// <returns></returns>
public bool SaveGrayScaleJPG(String pathFileName)
{
Validate();
SKBitmap bwBitmap=ToGrayScale(bitmap);
BitmapExtensions.SaveJPG100(bwBitmap,pathFileName);
bwBitmap.Dispose();
return true;
}
/// <summary>
/// Save the bitmap as black and white jpeg to the specified file
/// </summary>
/// <param name="pathFileName"></param>
/// <returns></returns>
public bool SaveBlackAndWhiteJPG(String pathFileName)
{
Validate();
SKBitmap bwBitmap=ToBlackAndWhite(bitmap);
BitmapExtensions.SaveJPG100(bwBitmap,pathFileName);
bwBitmap.Dispose();
return true;
}
/// <summary>
/// Save the bitmap to the specified file
/// </summary>
/// <param name="pathFileName"></param>
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);
}
/// <summary>
/// Save the bitmap in PNG format. Note: PNG files support alpha channels, JPG files do not
/// </summary>
/// <param name="pathFileName"></param>
/// <param name="altBitmap"></param>
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;
}
/// <summary>
/// Rotates the bitmap right by 90 degrees
/// </summary>
/// <returns></returns>
public bool RotateRight()
{
Validate();
SKBitmap rotated = Rotate(bitmap, 90);
bitmap.Dispose();
bitmap=rotated;
pointMapping = new PointMapping(Width,Height,Width,0,Height,0);
return true;
}
/// <summary>
/// Rotates the bitmap left by 90 degrees
/// </summary>
/// <returns></returns>
public bool RotateLeft()
{
Validate();
SKBitmap rotated = Rotate(bitmap, -90);
bitmap.Dispose();
bitmap=rotated;
pointMapping = new PointMapping(Width,Height,Width,0,Height,0);
return true;
}
/// <summary>
/// Rotates the bitmap to the specified angle
/// </summary>
/// <param name="bitmap"></param>
/// <param name="angle"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Adds a blur effect to the bitmap
/// </summary>
/// <param name="sigmaX"></param>
/// <returns></returns>
public bool Blur(float sigmaX)
{
Validate();
SKBitmap blurredBitmap = Blur(bitmap, sigmaX);
bitmap.Dispose();
bitmap = blurredBitmap;
return true;
}
/// <summary>
/// Applies a blur effect to the bitmap
/// </summary>
/// <param name="image"></param>
/// <param name="sigmaX"></param>
/// <param name="sigmaY"></param>
/// <returns></returns>
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;
}
/// <summary>
/// GetPixel - No point translation
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public SKColor GetPixel(int x,int y)
{
Validate();
return bitmap.GetPixel(x, y);
}
/// <summary>
/// SetPixel - No point translation
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="color"></param>
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;
}
}
/// <summary>
/// Fills the rectangle with the specified color
/// </summary>
/// <param name="color"></param>
public void Fill(SKColor color)
{
Validate();
SKRectI rect = new SKRectI(0, 0, Width, Height); // x, y, width, height
bitmap.Erase(color, rect);
}
/// <summary>
/// Make the bitmap transparent. Note: PNG format preserves alpha channel while JPG format does not.. so save as PNG if using transparency
/// </summary>
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;
}
/// <summary>
/// DrawPoint - With translation
/// </summary>
/// <param name="color"></param>
/// <param name="drawPoint"></param>
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);
}
/// <summary>
/// DrawPoint - with given strokeWidth and translation
/// </summary>
/// <param name="color"></param>
/// <param name="strokeWidth"></param>
/// <param name="drawPoint"></param>
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);
}
/// <summary>
/// Add noise to the image
/// </summary>
/// <param name="color"></param>
/// <param name="percentDecimal"></param>
public void AddNoise(SKColor color,double percentDecimal)
{
Random random=new Random();
int amount=(int)(percentDecimal*(Width*Height));
for(int index=0;index<amount;index++)
{
int x=random.Next(Width-1);
int y=random.Next(Height-1);
SetPixel(x, y,color);
}
}
/// <summary>
/// Draw a line on the bitmap
/// </summary>
/// <param name="color"></param>
/// <param name="strokeWidth"></param>
/// <param name="srcPoint"></param>
/// <param name="dstPoint"></param>
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);
}
/// <summary>
/// Draw text on the bitmap
/// </summary>
/// <param name="color"></param>
/// <param name="strokeWidth"></param>
/// <param name="srcPoint"></param>
/// <param name="dstPoint"></param>
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);
}
/// <summary>
/// Create a rectangle with the specified text
/// </summary>
/// <param name="text"></param>
/// <param name="textColor"></param>
/// <param name="fillColor"></param>
/// <param name="align"></param>
/// <param name="font"></param>
/// <param name="paintStyle"></param>
/// <param name="strokeWidth"></param>
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);
}
/// <summary>
/// Gets the length of the text
/// </summary>
/// <param name="text"></param>
/// <param name="font"></param>
/// <param name="paintStyle"></param>
/// <param name="strokeWidth"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Draws the path along the line segments
/// </summary>
/// <param name="color"></param>
/// <param name="strokeWidth"></param>
/// <param name="lineSegments"></param>
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);
}
}
/// <summary>
/// Draws a circle on the bitmap
/// </summary>
/// <param name="color"></param>
/// <param name="center"></param>
/// <param name="radius"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Draw a triangle centered inside the bounded area defined by the width and height of the bitmap.
/// </summary>
/// <param name="paintStroke">The paint stroke to to use to outline the triangle</param>
/// <param name="paintFill">The paint stroke to use to fill the triangle</param>
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);
}
/// <summary>
/// Draw a triangle centered at the specified 'origin' and being 'length' width at it's base
/// </summary>
/// <param name="origin">The point origin</param>
/// <param name="length">The length of the base of the triangle</param>
/// <param name="paintStroke">The paint stroke to to use to outline the triangle</param>
/// <param name="paintFill">The paint stroke to use to fill the triangle</param>
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);
}
}
}