I'm trying to implement an Image Edge Detection into a WPF program.
I already have it working, but the converting of the image is quite slow.
The code is not using the slow GetPixel and SetPixel functions. But instead I'm looping through the image in some unsafe code so that I can directly access the value's using a pointer.
Before starting the Edge detection I'm also converting the image to a greyscale image to improve the edge detection speed.
But still it takes the program around 1600ms to convert an image with a size of 1920x1440 pixels, which I think could be much faster.
This is the original image:
Which is converted to this (Snapshot of the application):
This is how I'm converting the image, I'm wondering what I can do to get to some other speed improvements?
Loading the image and create a Greyscale WriteableBitmap:
private void imageData_Loaded(object sender, RoutedEventArgs e)
{
if (imageData.Source != null)
{
BitmapSource BitmapSrc = new FormatConvertedBitmap(imageData.Source as BitmapSource, PixelFormats.Gray8 /* Convert to greyscale image */, null, 0);
writeableOriginalBitmap = new WriteableBitmap(BitmapSrc);
writeableBitmap = writeableOriginalBitmap.Clone();
imageData.Source = writeableBitmap;
EdgeDetection();
}
}
Converting the Image:
private const int TOLERANCE = 20;
private void EdgeDetection()
{
DateTime startTime = DateTime.Now; //Save starting time
writeableOriginalBitmap.Lock();
writeableBitmap.Lock();
unsafe
{
byte* pBuffer = (byte*)writeableBitmap.BackBuffer.ToPointer();
byte* pOriginalBuffer = (byte*)writeableOriginalBitmap.BackBuffer.ToPointer();
for (int row = 0; row < writeableOriginalBitmap.PixelHeight; row++)
{
for (int column = 0; column < writeableOriginalBitmap.PixelWidth; column++)
{
byte edgeColor = getEdgeColor(column, row, pOriginalBuffer); //Get pixel color based on edge value
pBuffer[column + (row * writeableBitmap.BackBufferStride)] = (byte)(255 - edgeColor);
}
}
}
//Refresh image
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight));
writeableBitmap.Unlock();
writeableOriginalBitmap.Unlock();
//Calculate converting time
TimeSpan diff = DateTime.Now - startTime;
Debug.WriteLine("Loading Time: " + (int)diff.TotalMilliseconds);
}
private unsafe byte getEdgeColor(int xPos, int yPos, byte* pOriginalBuffer)
{
byte Color;
byte maxColor = 0;
byte minColor = 255;
int difference;
//Calculate max and min value of surrounding pixels
for (int y = yPos - 1; y <= yPos + 1; y++)
{
for (int x = xPos - 1; x <= xPos + 1; x++)
{
if (x >= 0 && x < writeableOriginalBitmap.PixelWidth && y >= 0 && y < writeableOriginalBitmap.PixelHeight)
{
Color = pOriginalBuffer[x + (y * writeableOriginalBitmap.BackBufferStride)];
if (Color > maxColor) //If current pixel has higher value as previous max pixel
maxColor = Color; //Save current pixel value as max
if (Color < minColor) //If current pixel has lower value as previous min pixel
minColor = Color; //Save current pixel value as min
}
}
}
//Difference of minimum and maximum pixel with tollerance
difference = maxColor - minColor - TOLERANCE;
if (difference < 0)
difference = 0;
return (byte)difference;
}
Console Output:
Loading Time: 1599
The following code runs your algorithm on a byte array instead of the BackBuffer of a WriteableBitmap. It completes in less than 300 ms with a 1900x1200 image on my PC.
private static BitmapSource EdgeDetection(BitmapSource source)
{
var stopwatch = Stopwatch.StartNew();
var bitmap = new FormatConvertedBitmap(source, PixelFormats.Gray8, null, 0);
var width = bitmap.PixelWidth;
var height = bitmap.PixelHeight;
var originalBuffer = new byte[width * height];
var buffer = new byte[width * height];
bitmap.CopyPixels(originalBuffer, width, 0);
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
byte edgeColor = GetEdgeColor(originalBuffer, width, height, x, y);
buffer[width * y + x] = (byte)(255 - edgeColor);
}
}
Debug.WriteLine(stopwatch.ElapsedMilliseconds);
return BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, buffer, width);
}
private static byte GetEdgeColor(byte[] buffer, int width, int height, int x, int y)
{
const int tolerance = 20;
byte minColor = 255;
byte maxColor = 0;
var xStart = Math.Max(0, x - 1);
var xEnd = Math.Min(width - 1, x + 1);
var yStart = Math.Max(0, y - 1);
var yEnd = Math.Min(height - 1, y + 1);
for (var j = yStart; j <= yEnd; j++)
{
for (var i = xStart; i <= xEnd; i++)
{
var color = buffer[width * j + i];
minColor = Math.Min(minColor, color);
maxColor = Math.Max(maxColor, color);
}
}
return (byte)Math.Max(0, maxColor - minColor - tolerance);
}
Related
The main goal is to get all pixels from the bitmap very fast, and now this is done in 30fps.
I save the bitmap to byte[] array which has length: 4196406(4mb).
This is the code I use which give me ~5fps:
using (var data = new MemoryStream())
{
bitmap.Save(data, ImageFormat.Bmp);
ScreenRefreshed?.Invoke(this, data.ToArray());
_init = true;
}
Color[,] Pixels = new Color[Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height];
Pixels = GetPixels(data);
public Color[,] GetPixels(byte[] data)
{
int width = Screen.PrimaryScreen.Bounds.Width;
int height = Screen.PrimaryScreen.Bounds.Height;
Int32 stride = width * 4;
Int32 curRowOffs = (((width * height * 4) + 54) - 1) - stride;
Color[,] buffer = new Color[width, height];
for (uint y = 0; y < height; y++)
{
uint index = (uint)curRowOffs;
for (uint x = 0; x < width; x++)
{
if (index >= 0)
{
var b = data[index];
var g = data[index + 1];
var r = data[index + 2];
var a = data[index + 3];
buffer[x, y] = Color.FromArgb(a,r,g,b); //This line is reducing the fps
}
index += 4;
}
curRowOffs -= stride;
}
return buffer;
}
How to assign the values of array to another one
and loop through the second array keeping the same speed(30fps)?
This line: buffer[x, y] = Color.FromArgb(a,r,g,b); - reduces fps from 30 to 5
I have a picture containing text :
I made a method to detect text rows. This method return the 4 corners for the text zone (always sorted) :
I want to modify the bitmap to draw a rectangle (with transparence) from theses 4 corners. Something like this :
I have my image in gray scale. I created a function to draw a rectangle, but I only achieve to draw a right rectangle :
public static void SaveDrawRectangle(int width, int height, Byte[] matrix, int dpi, System.Drawing.Point[] corners, string path)
{
System.Windows.Media.Imaging.WriteableBitmap wbm = new System.Windows.Media.Imaging.WriteableBitmap(width, height, dpi, dpi, System.Windows.Media.PixelFormats.Bgra32, null);
uint[] pixels = new uint[width * height];
for (int Y = 0; Y < height; Y++)
{
for (int X = 0; X < width; X++)
{
byte pixel = matrix[Y * width + X];
int red = pixel;
int green = pixel;
int blue = pixel;
int alpha = 255;
if (X >= corners[0].X && X <= corners[1].X &&
Y >= corners[0].Y && Y <= corners[3].Y)
{
red = 255;
alpha = 255;
}
pixels[Y * width + X] = (uint)((alpha << 24) + (red << 16) + (green << 8) + blue);
}
}
wbm.WritePixels(new System.Windows.Int32Rect(0, 0, width, height), pixels, width * 4, 0);
using (FileStream stream5 = new FileStream(path, FileMode.Create))
{
PngBitmapEncoder encoder5 = new PngBitmapEncoder();
encoder5.Frames.Add(BitmapFrame.Create(wbm));
encoder5.Save(stream5);
}
}
How can I draw a rectangle from 4 corners ?
I modify my condition by replacing with that code:
public static void SaveDrawRectangle(int width, int height, Byte[] matrix, int dpi, List<Point> corners, string path)
{
System.Windows.Media.Imaging.WriteableBitmap wbm = new System.Windows.Media.Imaging.WriteableBitmap(width, height, dpi, dpi, System.Windows.Media.PixelFormats.Bgra32, null);
uint[] pixels = new uint[width * height];
for (int Y = 0; Y < height; Y++)
{
for (int X = 0; X < width; X++)
{
byte pixel = matrix[Y * width + X];
int red = pixel;
int green = pixel;
int blue = pixel;
int alpha = 255;
if (IsInRectangle(X, Y, corners))
{
red = 255;
}
pixels[Y * width + X] = (uint)((alpha << 24) + (red << 16) + (green << 8) + blue);
}
}
wbm.WritePixels(new System.Windows.Int32Rect(0, 0, width, height), pixels, width * 4, 0);
using (FileStream stream5 = new FileStream(path, FileMode.Create))
{
PngBitmapEncoder encoder5 = new PngBitmapEncoder();
encoder5.Frames.Add(BitmapFrame.Create(wbm));
encoder5.Save(stream5);
}
}
public static bool IsInRectangle(int X, int Y, List<Point> corners)
{
Point p1, p2;
bool inside = false;
if (corners.Count < 3)
{
return inside;
}
var oldPoint = new Point(
corners[corners.Count - 1].X, corners[corners.Count - 1].Y);
for (int i = 0; i < corners.Count; i++)
{
var newPoint = new Point(corners[i].X, corners[i].Y);
if (newPoint.X > oldPoint.X)
{
p1 = oldPoint;
p2 = newPoint;
}
else
{
p1 = newPoint;
p2 = oldPoint;
}
if ((newPoint.X < X) == (X <= oldPoint.X)
&& (Y - (long)p1.Y) * (p2.X - p1.X)
< (p2.Y - (long)p1.Y) * (X - p1.X))
{
inside = !inside;
}
oldPoint = newPoint;
}
return inside;
}
It works but have 2 failings :
generated images are very big (base image take 6 Mo and after drawing 25 Mo)
generation take several time (my images are 5000x7000 pixels, process take 10 seconds)
There is probably a better way, but this way is working good.
I am trying to detect light from 2 LED lights (red and blue) I did that using Bernsen thresholding technique. However, I applied that to an image. Now I want to apply that same technique but to a live video from my webcam. Is there anyway I could simply edit the code for this technique on the image to make it work on a video from the webcam? I will add below the code I used for this thresholding technique.
private ArrayList getNeighbours(int xPos, int yPos, Bitmap bitmap)
{
//This goes around the image in windows of 5
ArrayList neighboursList = new ArrayList();
int xStart, yStart, xFinish, yFinish;
int pixel;
xStart = xPos - 5;
yStart = yPos - 5;
xFinish = xPos + 5;
yFinish = yPos + 5;
for (int y = yStart; y <= yFinish; y++)
{
for (int x = xStart; x <= xFinish; x++)
{
if (x < 0 || y < 0 || x > (bitmap.Width - 1) || y > (bitmap.Height - 1))
{
continue;
}
else
{
pixel = bitmap.GetPixel(x, y).R;
neighboursList.Add(pixel);
}
}
}
return neighboursList;
}
private void button5_Click_1(object sender, EventArgs e)
{
//The input image
Bitmap image = new Bitmap(pictureBox2.Image);
progressBar1.Minimum = 0;
progressBar1.Maximum = image.Height - 1;
progressBar1.Value = 0;
Bitmap result = new Bitmap(pictureBox2.Image);
int iMin, iMax, t, c, contrastThreshold, pixel;
contrastThreshold = 180;
ArrayList list = new ArrayList();
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
list.Clear();
pixel = image.GetPixel(x, y).R;
list = getNeighbours(x, y, image);
list.Sort();
iMin = Convert.ToByte(list[0]);
iMax = Convert.ToByte(list[list.Count - 1]);
// These are the calculations to test whether the
current pixel is light or dark
t = ((iMax + iMin) / 2);
c = (iMax - iMin);
if (c < contrastThreshold)
{
pixel = ((t >= 160) ? 0 : 255);
}
else
{
pixel = ((pixel >= t) ? 0 : 255);
}
result.SetPixel(x, y, Color.FromArgb(pixel, pixel, pixel));
}
progressBar1.Value = y;
}
pictureBox3.Image =result;
}
I've been experimenting with the image bicubic resampling algorithm present in the AForge framework with the idea of introducing something similar into my image processing solution. See the original algorithm here and interpolation kernel here
Unfortunately I've hit a wall. It looks to me like somehow I am calculating the sample destination position incorrectly, probably due to the algorithm being designed for Format24bppRgb images where as I am using a Format32bppPArgb format.
Here's my code:
public Bitmap Resize(Bitmap source, int width, int height)
{
int sourceWidth = source.Width;
int sourceHeight = source.Height;
Bitmap destination = new Bitmap(width, height, PixelFormat.Format32bppPArgb);
destination.SetResolution(source.HorizontalResolution, source.VerticalResolution);
using (FastBitmap sourceBitmap = new FastBitmap(source))
{
using (FastBitmap destinationBitmap = new FastBitmap(destination))
{
double heightFactor = sourceWidth / (double)width;
double widthFactor = sourceHeight / (double)height;
// Coordinates of source points
double ox, oy, dx, dy, k1, k2;
int ox1, oy1, ox2, oy2;
// Width and height decreased by 1
int maxHeight = height - 1;
int maxWidth = width - 1;
for (int y = 0; y < height; y++)
{
// Y coordinates
oy = (y * widthFactor) - 0.5;
oy1 = (int)oy;
dy = oy - oy1;
for (int x = 0; x < width; x++)
{
// X coordinates
ox = (x * heightFactor) - 0.5f;
ox1 = (int)ox;
dx = ox - ox1;
// Destination color components
double r = 0;
double g = 0;
double b = 0;
double a = 0;
for (int n = -1; n < 3; n++)
{
// Get Y cooefficient
k1 = Interpolation.BiCubicKernel(dy - n);
oy2 = oy1 + n;
if (oy2 < 0)
{
oy2 = 0;
}
if (oy2 > maxHeight)
{
oy2 = maxHeight;
}
for (int m = -1; m < 3; m++)
{
// Get X cooefficient
k2 = k1 * Interpolation.BiCubicKernel(m - dx);
ox2 = ox1 + m;
if (ox2 < 0)
{
ox2 = 0;
}
if (ox2 > maxWidth)
{
ox2 = maxWidth;
}
Color color = sourceBitmap.GetPixel(ox2, oy2);
r += k2 * color.R;
g += k2 * color.G;
b += k2 * color.B;
a += k2 * color.A;
}
}
destinationBitmap.SetPixel(
x,
y,
Color.FromArgb(a.ToByte(), r.ToByte(), g.ToByte(), b.ToByte()));
}
}
}
}
source.Dispose();
return destination;
}
And the kernel which should represent the given equation on Wikipedia
public static double BiCubicKernel(double x)
{
if (x < 0)
{
x = -x;
}
double bicubicCoef = 0;
if (x <= 1)
{
bicubicCoef = (1.5 * x - 2.5) * x * x + 1;
}
else if (x < 2)
{
bicubicCoef = ((-0.5 * x + 2.5) * x - 4) * x + 2;
}
return bicubicCoef;
}
Here's the original image at 500px x 667px.
And the image resized to 400px x 543px.
Visually it appears that the image is over reduced and then the same pixels are repeatedly applied once we hit a particular point.
Can anyone give me some pointers here to solve this?
Note FastBitmap is a wrapper for Bitmap that uses LockBits to manipulate pixels in memory. It works well with everything else I apply it to.
Edit
As per request here's the methods involved in ToByte
public static byte ToByte(this double value)
{
return Convert.ToByte(ImageMaths.Clamp(value, 0, 255));
}
public static T Clamp<T>(T value, T min, T max) where T : IComparable<T>
{
if (value.CompareTo(min) < 0)
{
return min;
}
if (value.CompareTo(max) > 0)
{
return max;
}
return value;
}
You are limiting your ox2 and oy2 to destination image dimensions, instead of source dimensions.
Change this:
// Width and height decreased by 1
int maxHeight = height - 1;
int maxWidth = width - 1;
to this:
// Width and height decreased by 1
int maxHeight = sourceHeight - 1;
int maxWidth = sourceWidth - 1;
Well, I've met a very strange thing, which might be or might be not a souce of the problem.
I've started to try implementing convolution matrix by myself and encountered strange behaviour. I was testing code on a small image 4x4 pixels. The code is following:
var source = Bitmap.FromFile(#"C:\Users\Public\Pictures\Sample Pictures\Безымянный.png");
using (FastBitmap sourceBitmap = new FastBitmap(source))
{
for (int TY = 0; TY < 4; TY++)
{
for (int TX = 0; TX < 4; TX++)
{
Color color = sourceBitmap.GetPixel(TX, TY);
Console.Write(color.B.ToString().PadLeft(5));
}
Console.WriteLine();
}
}
Althought I'm printing out only blue channel value, it's still clearly incorrect.
On the other hand, your solution partitially works, what makes the thing I've found kind of irrelevant. One more guess I have: what is your system's DPI?
From what I have found helpfull, here are some links:
C++ implementation of bicubic interpolation on
matrix
C# implemetation of bicubic interpolation, lacking the part about rescaling
Thread on gamedev.net which has almost working solution
That's my answer so far, but I will try further.
Suppose I have a System.Drawing.Bitmap in 32bpp ARGB mode. It's a large bitmap, but it's mostly fully transparent pixels with a relatively small image somewhere in the middle.
What is a fast algorithm to detect the borders of the "real" image, so I can crop away all the transparent pixels from around it?
Alternatively, is there a function already in .Net that I can use for this?
The basic idea is to check every pixel of the image to find the top, left, right and bottom bounds of the image. To do this efficiently, don't use the GetPixel method, which is pretty slow. Use LockBits instead.
Here's the implementation I came up with:
static Bitmap TrimBitmap(Bitmap source)
{
Rectangle srcRect = default(Rectangle);
BitmapData data = null;
try
{
data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] buffer = new byte[data.Height * data.Stride];
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
int xMin = int.MaxValue;
int xMax = 0;
int yMin = int.MaxValue;
int yMax = 0;
for (int y = 0; y < data.Height; y++)
{
for (int x = 0; x < data.Width; x++)
{
byte alpha = buffer[y * data.Stride + 4 * x + 3];
if (alpha != 0)
{
if (x < xMin) xMin = x;
if (x > xMax) xMax = x;
if (y < yMin) yMin = y;
if (y > yMax) yMax = y;
}
}
}
if (xMax < xMin || yMax < yMin)
{
// Image is empty...
return null;
}
srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax);
}
finally
{
if (data != null)
source.UnlockBits(data);
}
Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height);
Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height);
using (Graphics graphics = Graphics.FromImage(dest))
{
graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);
}
return dest;
}
It can probably be optimized, but I'm not a GDI+ expert, so it's the best I can do without further research...
EDIT: actually, there's a simple way to optimize it, by not scanning some parts of the image :
scan left to right until you find a non-transparent pixel; store (x, y) into (xMin, yMin)
scan top to bottom until you find a non-transparent pixel (only for x >= xMin); store y into yMin
scan right to left until you find a non-transparent pixel (only for y >= yMin); store x into xMax
scan bottom to top until you find a non-transparent pixel (only for xMin <= x <= xMax); store y into yMax
EDIT2: here's an implementation of the approach above:
static Bitmap TrimBitmap(Bitmap source)
{
Rectangle srcRect = default(Rectangle);
BitmapData data = null;
try
{
data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] buffer = new byte[data.Height * data.Stride];
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
int xMin = int.MaxValue,
xMax = int.MinValue,
yMin = int.MaxValue,
yMax = int.MinValue;
bool foundPixel = false;
// Find xMin
for (int x = 0; x < data.Width; x++)
{
bool stop = false;
for (int y = 0; y < data.Height; y++)
{
byte alpha = buffer[y * data.Stride + 4 * x + 3];
if (alpha != 0)
{
xMin = x;
stop = true;
foundPixel = true;
break;
}
}
if (stop)
break;
}
// Image is empty...
if (!foundPixel)
return null;
// Find yMin
for (int y = 0; y < data.Height; y++)
{
bool stop = false;
for (int x = xMin; x < data.Width; x++)
{
byte alpha = buffer[y * data.Stride + 4 * x + 3];
if (alpha != 0)
{
yMin = y;
stop = true;
break;
}
}
if (stop)
break;
}
// Find xMax
for (int x = data.Width - 1; x >= xMin; x--)
{
bool stop = false;
for (int y = yMin; y < data.Height; y++)
{
byte alpha = buffer[y * data.Stride + 4 * x + 3];
if (alpha != 0)
{
xMax = x;
stop = true;
break;
}
}
if (stop)
break;
}
// Find yMax
for (int y = data.Height - 1; y >= yMin; y--)
{
bool stop = false;
for (int x = xMin; x <= xMax; x++)
{
byte alpha = buffer[y * data.Stride + 4 * x + 3];
if (alpha != 0)
{
yMax = y;
stop = true;
break;
}
}
if (stop)
break;
}
srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax);
}
finally
{
if (data != null)
source.UnlockBits(data);
}
Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height);
Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height);
using (Graphics graphics = Graphics.FromImage(dest))
{
graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);
}
return dest;
}
There won't be a significant gain if the non-transparent part is small of course, since it will still scan most of the pixels. But if it's big, only the rectangles around the non-transparent part will be scanned.
I would like to suggest a divide & conquer approach:
split the image in the middle (e.g. vertically)
check if there are non-transparent pixels on the cut line (if so, remember min/max for bounding box)
split left half again vertically
if cut line contains non-transparent pixels -> update bounding box
if not, you can probably discard the leftmost half (I don't know the pictures)
continue with the left-right half (you stated that the image is somewhere in the middle) until you find the leftmost bound of the image
do the same for the right half