I am attempting to extract the audio content of a wav file and export the resultant waveform as an image (bmp/jpg/png).
So I have found the following code which draws a sine wave and works as expected:
string filename = #"C:\0\test.bmp";
int width = 640;
int height = 480;
Bitmap b = new Bitmap(width, height);
for (int i = 0; i < width; i++)
{
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
b.SetPixel(i, y, Color.Black);
}
b.Save(filename);
This works completely as expected, what I would like to do is replace
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
with something like
int y = converted and scaled float from monoWaveFileFloatValues
So how would I best go about doing this in the simplest manner possible?
I have 2 basic issues I need to deal with (i think)
convert float to int in a way which does not loose information, this is due to SetPixel(i, y, Color.Black); where x & y are both int
sample skipping on the x axis so the waveform fits into the defined space audio length / image width give the number of samples to average out intensity over which would be represented by a single pixel
The other options is find another method of plotting the waveform which does not rely on the method noted above. Using a chart might be a good method, but I would like to be able to render the image directly if possible
This is all to be run from a console application and I have the audio data (minus the header) already in a float array.
UPDATE 1
The following code enabled me to draw the required output using System.Windows.Forms.DataVisualization.Charting but it took about 30 seconds to process 27776 samples and whilst it does do what I need, it is far too slow to be useful. So I am still looking towards a solution which will draw the bitmap directly.
System.Windows.Forms.DataVisualization.Charting.Chart chart = new System.Windows.Forms.DataVisualization.Charting.Chart();
chart.Size = new System.Drawing.Size(640, 320);
chart.ChartAreas.Add("ChartArea1");
chart.Legends.Add("legend1");
// Plot {sin(x), 0, 2pi}
chart.Series.Add("sin");
chart.Series["sin"].LegendText = args[0];
chart.Series["sin"].ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline;
//for (double x = 0; x < 2 * Math.PI; x += 0.01)
for (int x = 0; x < audioDataLength; x ++)
{
//chart.Series["sin"].Points.AddXY(x, Math.Sin(x));
chart.Series["sin"].Points.AddXY(x, leftChannel[x]);
}
// Save sin_0_2pi.png image file
chart.SaveImage(#"c:\tmp\example.png", System.Drawing.Imaging.ImageFormat.Png);
Output shown below:
So I managed to figure it out using a code sample found here, though I made some minor changes to the way I interact with it.
public static Bitmap DrawNormalizedAudio(List<float> data, Color foreColor, Color backColor, Size imageSize, string imageFilename)
{
Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);
int BORDER_WIDTH = 0;
float width = bmp.Width - (2 * BORDER_WIDTH);
float height = bmp.Height - (2 * BORDER_WIDTH);
using (Graphics g = Graphics.FromImage(bmp))
{
g.Clear(backColor);
Pen pen = new Pen(foreColor);
float size = data.Count;
for (float iPixel = 0; iPixel < width; iPixel += 1)
{
// determine start and end points within WAV
int start = (int)(iPixel * (size / width));
int end = (int)((iPixel + 1) * (size / width));
if (end > data.Count)
end = data.Count;
float posAvg, negAvg;
averages(data, start, end, out posAvg, out negAvg);
float yMax = BORDER_WIDTH + height - ((posAvg + 1) * .5f * height);
float yMin = BORDER_WIDTH + height - ((negAvg + 1) * .5f * height);
g.DrawLine(pen, iPixel + BORDER_WIDTH, yMax, iPixel + BORDER_WIDTH, yMin);
}
}
bmp.Save(imageFilename);
bmp.Dispose();
return null;
}
private static void averages(List<float> data, int startIndex, int endIndex, out float posAvg, out float negAvg)
{
posAvg = 0.0f;
negAvg = 0.0f;
int posCount = 0, negCount = 0;
for (int i = startIndex; i < endIndex; i++)
{
if (data[i] > 0)
{
posCount++;
posAvg += data[i];
}
else
{
negCount++;
negAvg += data[i];
}
}
if (posCount > 0)
posAvg /= posCount;
if (negCount > 0)
negAvg /= negCount;
}
In order to get it working I had to do a couple of things prior to calling the method DrawNormalizedAudio you can see below what I needed to do:
Size imageSize = new Size();
imageSize.Width = 1000;
imageSize.Height = 500;
List<float> lst = leftChannel.OfType<float>().ToList(); //change float array to float list - see link below
DrawNormalizedAudio(lst, Color.Red, Color.White, imageSize, #"c:\tmp\example2.png");
* change float array to float list
The result of this is as follows, a waveform representation of a hand clap wav sample:
I am quite sure there needs to be some updates/revisions to the code, but it's a start and hopefully this will assist someone else who is trying to do the same thing I was.
If you can see any improvements that can be made, let me know.
UPDATES
NaN issue mentioned in the comments now resolved and code above updated.
Waveform Image updated to represent output fixed by removal of NaN values as noted in point 1.
UPDATE 1
Average level (not RMS) was determined by summing the max level for each sample point and dividing by the total number of samples. Examples of this can be seen below:
Silent Wav File:
Hand Clap Wav File:
Brownian, Pink & White Noise Wav File:
Here is a variation you may want to study. It scales the Graphics object so it can use the float data directly.
Note how I translate (i.e. move) the drawing area twice so I can do the drawing more conveniently!
It also uses the DrawLines method for drawing. The benefit in addition to speed is that the lines may be semi-transparent or thicker than one pixel without getting artifacts at the joints. You can see the center line shine through.
To do this I convert the float data to a List<PointF> using a little Linq magick.
I also make sure to put all GDI+ objects I create in using clause so they will get disposed of properly.
...
using System.Windows.Forms;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
..
..
class Program
{
static void Main(string[] args)
{
float[] data = initData(10000);
Size imgSize = new Size(1000, 400);
Bitmap bmp = drawGraph(data, imgSize , Color.Green, Color.Black);
bmp.Save("D:\\wave.png", ImageFormat.Png);
}
static float[] initData(int count)
{
float[] data = new float[count];
for (int i = 0; i < count; i++ )
{
data[i] = (float) ((Math.Sin(i / 12f) * 880 + Math.Sin(i / 15f) * 440
+ Math.Sin(i / 66) * 110) / Math.Pow( (i+1), 0.33f));
}
return data;
}
static Bitmap drawGraph(float[] data, Size size, Color ForeColor, Color BackColor)
{
Bitmap bmp = new System.Drawing.Bitmap(size.Width, size.Height,
PixelFormat.Format32bppArgb);
Padding borders = new Padding(20, 20, 10, 50);
Rectangle plotArea = new Rectangle(borders.Left, borders.Top,
size.Width - borders.Left - borders.Right,
size.Height - borders.Top - borders.Bottom);
using (Graphics g = Graphics.FromImage(bmp))
using (Pen pen = new Pen(Color.FromArgb(224, ForeColor),1.75f))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Silver);
using (SolidBrush brush = new SolidBrush(BackColor))
g.FillRectangle(brush, plotArea);
g.DrawRectangle(Pens.LightGoldenrodYellow, plotArea);
g.TranslateTransform(plotArea.Left, plotArea.Top);
g.DrawLine(Pens.White, 0, plotArea.Height / 2,
plotArea.Width, plotArea.Height / 2);
float dataHeight = Math.Max( data.Max(), - data.Min()) * 2;
float yScale = 1f * plotArea.Height / dataHeight;
float xScale = 1f * plotArea.Width / data.Length;
g.ScaleTransform(xScale, yScale);
g.TranslateTransform(0, dataHeight / 2);
var points = data.ToList().Select((y, x) => new { x, y })
.Select(p => new PointF(p.x, p.y)).ToList();
g.DrawLines(pen, points.ToArray());
g.ResetTransform();
g.DrawString(data.Length.ToString("###,###,###,##0") + " points plotted.",
new Font("Consolas", 14f), Brushes.Black,
plotArea.Left, plotArea.Bottom + 2f);
}
return bmp;
}
}
Related
There is an input of points with size of n like below:
S = {x1,y1,x2,y2,...,xn,yn}
I want to display scatter graph of S sequence in a picture box. So for transforming them into picture box dimensions, I have normalized them and multiplied them by width and height of picture box with respecting picture box left and top:
waveData= wave.GetWaveData();
normalizedData = GetSignedNormalized();
n = normalizedData.Count;
picW = pictureBox1.Width;
picH = pictureBox1.Height;
picL = pictureBox1.Left;
picT = pictureBox1.Top;
normalizedInPictureBox = new List<float>();
for (int i=0;i< n; i +=2)
{
float px = normalizedData[i];
float py = normalizedData[i+1];
px = px * (picW - picL);
py = py * (picH - picT) ;
normalizedInPictureBox.Add(px);
normalizedInPictureBox.Add(py);
}
Normalize Method is also:
public List<float> GetSignedNormalized()
{
List<float> data = new List<float>();
short max = waveData.Max();
int m = waveData.Count;
for(int i=0;i< m; i++)
{
data.Add((float)waveData[i] / (float)max);
}
return data;
}
Now I am thinking normalizedInPictureBox List contains vertices in the range of picture box, and here is the code for drawing them on picture box:
In the paint method of picture box:
Graphics gr = e.Graphics;
gr.Clear(Color.Black);
for(int i=0;i< n; i +=2)
{
float x = normalizedInPictureBox[i] ;
float y = normalizedInPictureBox[i+1];
gr.FillEllipse(Brushes.Green, new RectangleF(x, y, 2.25f, 2.25f));
}
But the result is shown below:
I don't Know whats going wrong here , but I think the graph should be horizontal not diagonal ,the desire result is something like this:
I know that I can transform it to center of picture box after this. but How can change my own result to the desire one?
Thanks in advance.
I don't really know why your code doesn't work correctly without having a look at the actual data and playing around with it, but having done chart drawing before, I suggest you go the full way and clearly define your axis ranges and do proper interpolating. It get's much clearer from there.
Here is what I came up with
static Bitmap DrawChart(float[] Values, int Width, int Height)
{
var n = Values.Count();
if (n % 2 == 1) throw new Exception("Invalid data");
//Split the data into lists for easy access
var x = new List<float>();
var y = new List<float>();
for (int i = 0; i < n - 1; i += 2)
{
x.Add(Values[i]);
y.Add(Values[i + 1]);
}
//Chart axis limits, change here to get custom ranges like -1,+1
var minx = x.Min();
var miny = y.Min();
var maxx = x.Max();
var maxy = y.Max();
var dxOld = maxx - minx;
var dyOld = maxy - miny;
//Rescale the y-Range to add a border at the top and bottom
miny -= dyOld * 0.2f;
maxy += dyOld * 0.2f;
var dxNew = (float)Width;
var dyNew = (float)Height;
//Draw the data
Bitmap res = new Bitmap(Width, Height);
using (var g = Graphics.FromImage(res))
{
g.Clear(Color.Black);
for (int i = 0; i < x.Count; i++)
{
//Calculate the coordinates
var px = Interpolate(x[i], minx, maxx, 0, dxNew);
var py = Interpolate(y[i], miny, maxy, 0, dyNew);
//Draw, put the ellipse center around the point
g.FillEllipse(Brushes.ForestGreen, px - 1.0f, py - 1.0f, 2.0f, 2.0f);
}
}
return res;
}
static float Interpolate(float Value, float OldMin, float OldMax, float NewMin, float NewMax)
{
//Linear interpolation
return ((NewMax - NewMin) / (OldMax - OldMin)) * (Value - OldMin) + NewMin;
}
It should be relatively self explanatory. You may consider drawing lines instead of single points, that depends on the look and feel you want to achive. Draw other chart elements to your liking.
Important: The y-Axis is actually inversed in the code above, so positive values go down, negative go up, it is scaled like the screen coordinates. You'll figure out how to fix that :-)
Example with 5000 random-y points (x is indexed):
I am writing a .Net wrapper for Tesseract Ocr and if I use a grayscale image instead of rgb image as an input file to it then results are pretty good.
So I was searching the web for C# solution to convert a Rgb image to grayscale image and I found this code.
This performs 3 operations to increase the accuracy of tesseract.
Resize the image
then convert into grayscale image and remove noise from image
Now this converted image gives almost 90% accurate results.
//Resize
public Bitmap Resize(Bitmap bmp, int newWidth, int newHeight)
{
Bitmap temp = (Bitmap)bmp;
Bitmap bmap = new Bitmap(newWidth, newHeight, temp.PixelFormat);
double nWidthFactor = (double)temp.Width / (double)newWidth;
double nHeightFactor = (double)temp.Height / (double)newHeight;
double fx, fy, nx, ny;
int cx, cy, fr_x, fr_y;
Color color1 = new Color();
Color color2 = new Color();
Color color3 = new Color();
Color color4 = new Color();
byte nRed, nGreen, nBlue;
byte bp1, bp2;
for (int x = 0; x < bmap.Width; ++x)
{
for (int y = 0; y < bmap.Height; ++y)
{
fr_x = (int)Math.Floor(x * nWidthFactor);
fr_y = (int)Math.Floor(y * nHeightFactor);
cx = fr_x + 1;
if (cx >= temp.Width)
cx = fr_x;
cy = fr_y + 1;
if (cy >= temp.Height)
cy = fr_y;
fx = x * nWidthFactor - fr_x;
fy = y * nHeightFactor - fr_y;
nx = 1.0 - fx;
ny = 1.0 - fy;
color1 = temp.GetPixel(fr_x, fr_y);
color2 = temp.GetPixel(cx, fr_y);
color3 = temp.GetPixel(fr_x, cy);
color4 = temp.GetPixel(cx, cy);
// Blue
bp1 = (byte)(nx * color1.B + fx * color2.B);
bp2 = (byte)(nx * color3.B + fx * color4.B);
nBlue = (byte)(ny * (double)(bp1) + fy * (double)(bp2));
// Green
bp1 = (byte)(nx * color1.G + fx * color2.G);
bp2 = (byte)(nx * color3.G + fx * color4.G);
nGreen = (byte)(ny * (double)(bp1) + fy * (double)(bp2));
// Red
bp1 = (byte)(nx * color1.R + fx * color2.R);
bp2 = (byte)(nx * color3.R + fx * color4.R);
nRed = (byte)(ny * (double)(bp1) + fy * (double)(bp2));
bmap.SetPixel(x, y, System.Drawing.Color.FromArgb(255, nRed, nGreen, nBlue));
}
}
//here i included the below to functions logic without the for loop to remove repetitive use of for loop but it did not work and taking the same time.
bmap = SetGrayscale(bmap);
bmap = RemoveNoise(bmap);
return bmap;
}
//SetGrayscale
public Bitmap SetGrayscale(Bitmap img)
{
Bitmap temp = (Bitmap)img;
Bitmap bmap = (Bitmap)temp.Clone();
Color c;
for (int i = 0; i < bmap.Width; i++)
{
for (int j = 0; j < bmap.Height; j++)
{
c = bmap.GetPixel(i, j);
byte gray = (byte)(.299 * c.R + .587 * c.G + .114 * c.B);
bmap.SetPixel(i, j, Color.FromArgb(gray, gray, gray));
}
}
return (Bitmap)bmap.Clone();
}
//RemoveNoise
public Bitmap RemoveNoise(Bitmap bmap)
{
for (var x = 0; x < bmap.Width; x++)
{
for (var y = 0; y < bmap.Height; y++)
{
var pixel = bmap.GetPixel(x, y);
if (pixel.R < 162 && pixel.G < 162 && pixel.B < 162)
bmap.SetPixel(x, y, Color.Black);
}
}
for (var x = 0; x < bmap.Width; x++)
{
for (var y = 0; y < bmap.Height; y++)
{
var pixel = bmap.GetPixel(x, y);
if (pixel.R > 162 && pixel.G > 162 && pixel.B > 162)
bmap.SetPixel(x, y, Color.White);
}
}
return bmap;
}
But the problem is it takes lot of time to convert it
So I included SetGrayscale(Bitmap bmap)
RemoveNoise(Bitmap bmap) function logic inside the Resize() method to remove repetitive use of for loop
but it did not solve my problem.
The Bitmap class's GetPixel() and SetPixel() methods are notoriously slow for multiple read/writes. A much faster way to access and set individual pixels in a bitmap is to lock it first.
There's a good example here on how to do that, with a nice class LockedBitmap to wrap around the stranger Marshaling code.
Essentially what it does is use the LockBits() method in the Bitmap class, passing a rectangle for the region of the bitmap you want to lock, and then copy those pixels from its unmanaged memory location to a managed one for easier access.
Here's an example on how you would use that example class with your SetGrayscale() method:
public Bitmap SetGrayscale(Bitmap img)
{
LockedBitmap lockedBmp = new LockedBitmap(img.Clone());
lockedBmp.LockBits(); // lock the bits for faster access
Color c;
for (int i = 0; i < lockedBmp.Width; i++)
{
for (int j = 0; j < lockedBmp.Height; j++)
{
c = lockedBmp.GetPixel(i, j);
byte gray = (byte)(.299 * c.R + .587 * c.G + .114 * c.B);
lockedBmp.SetPixel(i, j, Color.FromArgb(gray, gray, gray));
}
}
lockedBmp.UnlockBits(); // remember to release resources
return lockedBmp.Bitmap; // return the bitmap (you don't need to clone it again, that's already been done).
}
This wrapper class has saved me a ridiculous amount of time in bitmap processing. Once you've implemented this in all your methods, preferably only calling LockBits() once, then I'm sure your application's performance will improve tremendously.
I also see that you're cloning the images a lot. This probably doesn't take up as much time as the SetPixel()/GetPixel() thing, but its time can still be significant especially with larger images.
The easiest way would be to redraw the image onto itself using DrawImage and passing a suitable ColorMatrix. Google for ColorMatrix and gray scale and you'll find a ton of examples, this one for example: http://www.codeproject.com/Articles/3772/ColorMatrix-Basics-Simple-Image-Color-Adjustment
I'm drawing a lot of shapes on WritableBitmap with help of WritableBitmapEx in my WPF application.
Unfortunately there is no ready to go function to draw an arc on it.
How can I:
1. Draw an arc on WritableBitmap?
2. Draw an anti-aliased arc with variable thickness on WritableBitmap?
I just need to draw circular arcs.
There is possibility to draw a nice, anti-aliased arc with variable thickness (System.Windows.Media.ArcSegment) on Canvas - but with thousands of shapes the performance of Canvas is poor - that's why I'm using WritableBitmap.
If it would be needed by some algorithms I have already calculated arc parameters like:
CenterPoint, Radius, StartPoint, EndPoint, StartAngle, EndAngle, ArcLength, IsLarge or Direction
I was trying to draw it manually with code similar to this:
int number_of_points = 1000;
for(int i=0; i<=number_of_points; i++){
double progress=(double)i/number_of_points;
double theta = (StartAngle + ArcLength * progress) * Math.PI / 180.0;
draw_pixel(
Center.X + Radius * Math.Cos(theta),
Center.Y + Radius * Math.Sin(theta)
);
}
but with varying resolution of picture, varying size of arc (how to calculate optimum number_of_points?), varying thickness of arc and with anti aliasing it starts to be a little tricky.
1. Draw an arc on WritableBitmap?
After analyzing mono libgdiplus sources on github I found that they are drawing an arc using Bezier curve.
I have ported some of their functions to c#.
DrawArc extension function can be used (with help of DrawBezier from WritableBitmapEx) to draw an simple arc.
There is no anti-aliased version of DrawBezier in WritableBitmapEx so this solution answers (only) my first question:
namespace System.Windows.Media.Imaging
{
public static partial class WriteableBitmapArcExtensions
{
//port of mono libgdiplus function
//append_arcs (GpPath *path, float x, float y, float width, float height, float startAngle, float sweepAngle)
//from: https://github.com/mono/libgdiplus/blob/master/src/graphics-path.c
public static void DrawArc(this WriteableBitmap bmp, float x, float y, float width, float height, float startAngle, float sweepAngle, Color color)
{
int i;
float drawn = 0;
int increment;
float endAngle;
bool enough = false;
if (Math.Abs(sweepAngle) >= 360)
{
bmp.DrawEllipse((int)x, (int)y, (int)width, (int)height, color);
return;
}
endAngle = startAngle + sweepAngle;
increment = (endAngle < startAngle) ? -90 : 90;
/* i is the number of sub-arcs drawn, each sub-arc can be at most 90 degrees.*/
/* there can be no more then 4 subarcs, ie. 90 + 90 + 90 + (something less than 90) */
for (i = 0; i < 4; i++)
{
float current = startAngle + drawn;
float additional;
if (enough)
return;
additional = endAngle - current; /* otherwise, add the remainder */
if (Math.Abs(additional) > 90)
{
additional = increment;
}
else
{
/* a near zero value will introduce bad artefact in the drawing */
if ((additional >= -0.0001f) && (additional <= 0.0001f))
return;
enough = true;
}
bmp._DrawArc(
x, y,
width, height, /* bounding rectangle */
current, current + additional, color);
drawn += additional;
}
}
//port of mono libgdiplus function
//append_arc (GpPath *path, BOOL start, float x, float y, float width, float height, float startAngle, float endAngle)
//from: https://github.com/mono/libgdiplus/blob/master/src/graphics-path.c
private static void _DrawArc(this WriteableBitmap bmp, float x, float y, float width, float height, float startAngle, float endAngle, Color color)
{
double sin_alpha, sin_beta, cos_alpha, cos_beta;
var rx = width / 2;
var ry = height / 2;
/* center */
var cx = x + rx;
var cy = y + ry;
/* angles in radians */
var alpha = startAngle * Math.PI / 180;
var beta = endAngle * Math.PI / 180;
/* adjust angles for ellipses */
alpha = Math.Atan2(rx * Math.Sin(alpha), ry * Math.Cos(alpha));
beta = Math.Atan2(rx * Math.Sin(beta), ry * Math.Cos(beta));
if (Math.Abs(beta - alpha) > Math.PI)
{
if (beta > alpha)
beta -= 2 * Math.PI;
else
alpha -= 2 * Math.PI;
}
var delta = beta - alpha;
// http://www.stillhq.com/ctpfaq/2001/comp.text.pdf-faq-2001-04.txt (section 2.13)
var bcp = 4.0 / 3 * (1 - Math.Cos(delta / 2)) / Math.Sin(delta / 2);
sin_alpha = Math.Sin(alpha);
sin_beta = Math.Sin(beta);
cos_alpha = Math.Cos(alpha);
cos_beta = Math.Cos(beta);
/* starting point */
double sx = cx + rx * cos_alpha;
double sy = cy + ry * sin_alpha;
//DrawBezier comes from WritableBitmapEx library
bmp.DrawBezier(
(int)(sx),
(int)(sy),
(int)(cx + rx * (cos_alpha - bcp * sin_alpha)),
(int)(cy + ry * (sin_alpha + bcp * cos_alpha)),
(int)(cx + rx * (cos_beta + bcp * sin_beta)),
(int)(cy + ry * (sin_beta - bcp * cos_beta)),
(int)(cx + rx * cos_beta),
(int)(cy + ry * sin_beta),
color
);
}
}
}
I have commented an issue on WritableBitmapEx site: I would like to draw arcs - so maybe part of this code would be included in WritableBitmapEx library.
2. Draw an anti-aliased arc with variable thickness on WritableBitmap?
After reading comment from ForeverZer0 I have made some experiments with System.Drawing.Graphics and WritableBitmap. With help of getting a DrawingContext for a wpf WriteableBitmap I have done it with such code:
WritableBitmap ret = BitmapFactory.New(img_width, img_height);
ret.Lock();
var bmp = new System.Drawing.Bitmap(
ret.PixelWidth,
ret.PixelHeight,
ret.BackBufferStride,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb,
ret.BackBuffer
);
System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
g.DrawArc(...); //<-- draws an antialiased arc with variable thickness
g.Dispose();
bmp.Dispose();
ret.AddDirtyRect(new Int32Rect(0, 0, ret.PixelWidth, ret.PixelHeight));
ret.Unlock();
return ret; //<-- WritableBitmap with beautifull arc on it;
I'm trying to use the position of the mouse to calculate the scaling factor for scaling an image. Basically, the further you get away from the center of the image, the bigger it gets; and the closer to the center you get, the smaller it gets. I have some code so far but it's acting really strange and I have absolutely no more ideas. First I'll let you know, one thing I was trying to do is average out 5 distances to get a more smooth resize animation. Here's my code:
private void pictureBoxScale_MouseMove(object sender, MouseEventArgs e)
{
if (rotateScaleMode && isDraggingToScale)
{
// For Scaling
int sourceWidth = pictureBox1.Image.Width;
int sourceHeight = pictureBox1.Image.Height;
float dCurrCent = 0; // distance between the current mouse pos and the center of the image
float dPrevCent = 0; // distance between the previous mouse pos and the center of the image
System.Drawing.Point imgCenter = new System.Drawing.Point();
imgCenter.X = pictureBox1.Location.X + (sourceWidth / 2);
imgCenter.Y = pictureBox1.Location.Y + (sourceHeight / 2);
// Calculating the distance between the current mouse location and the center of the image
dCurrCent = (float)Math.Sqrt(Math.Pow(e.X - imgCenter.X, 2) + Math.Pow(e.Y - imgCenter.Y, 2));
// Calculating the distance between the previous mouse location and the center of the image
dPrevCent = (float)Math.Sqrt(Math.Pow(prevMouseLoc.X - imgCenter.X, 2) + Math.Pow(prevMouseLoc.Y - imgCenter.Y, 2));
if (smoothScaleCount < 5)
{
dCurrCentSmooth[smoothScaleCount] = dCurrCent;
dPrevCentSmooth[smoothScaleCount] = dPrevCent;
}
if (smoothScaleCount == 4)
{
float currCentSum = 0;
float prevCentSum = 0;
for (int i = 0; i < 4; i++)
{
currCentSum += dCurrCentSmooth[i];
}
for (int i = 0; i < 4; i++)
{
prevCentSum += dPrevCentSmooth[i];
}
float scaleAvg = (currCentSum / 5) / (prevCentSum / 5);
int destWidth = (int)(sourceWidth * scaleAvg);
int destHeight = (int)(sourceHeight * scaleAvg);
// If statement is for limiting the size of the image
if (destWidth > (currentRotatedImage.Width / 2) && destWidth < (currentRotatedImage.Width * 3) && destHeight > (currentRotatedImage.Height / 2) && destWidth < (currentRotatedImage.Width * 3))
{
AForge.Imaging.Filters.ResizeBilinear resizeFilter = new AForge.Imaging.Filters.ResizeBilinear(destWidth, destHeight);
pictureBox1.Image = resizeFilter.Apply((Bitmap)currentRotatedImage);
pictureBox1.Size = pictureBox1.Image.Size;
pictureBox1.Refresh();
}
smoothScaleCount = -1;
}
prevMouseLoc = e.Location;
currentScaledImage = pictureBox1.Image;
smoothScaleCount++;
}
}
EDIT: Thanks to Ben Voigt and Ray everything works well now. The only thing wrong is that with the way I'm doing it the image doesn't keep it's ratio; but I'll fix that later. Here's what I have for those who want to know:
private void pictureBoxScale_MouseMove(object sender, MouseEventArgs e)
{
if (rotateScaleMode && isDraggingToScale)
{
// For Scaling
int sourceWidth = pictureBox1.Image.Width;
int sourceHeight = pictureBox1.Image.Height;
int scale = e.X + p0.X; //p0 is the location of the mouse when the button first came down
int destWidth = (int)(sourceWidth + (scale/10)); //I divide it by 10 to make it slower
int destHeight = (int)(sourceHeight + (scale/10));
if (destWidth > 20 && destWidth < 1000 && destHeight > 20 && destWidth < 1000)
{
AForge.Imaging.Filters.ResizeBilinear resizeFilter = new AForge.Imaging.Filters.ResizeBilinear(destWidth, destHeight);
pictureBox1.Image = resizeFilter.Apply((Bitmap)currentRotatedImage);
pictureBox1.Size = pictureBox1.Image.Size;
pictureBox1.Refresh();
}
currentScaledImage = pictureBox1.Image; // This is only so I can rotate the scaled image in another part of my program
}
}
You're scaling won't be smooth if you use the center of the image. Instead, use the initial mouse down point (call it p0). Also, rather than using the distance from that point to the current drag point (e), just take the difference along one axis (e.g. exp(e.Y - p0.Y)).
It looks to me (from the scaleAvg calculation) like you're rescaling the already-scaled image. This is a really bad idea because scaling is lossy and the errors will accumulate. Instead, keep a copy of the crisp original image and scale the original directly to the current size.
Also, I would suggest using a different norm, perhaps Manhattan distance, instead of the current Cartesian distance which is a two-norm.
If you do continue using the two-norm, consider getting rid of the Math.Pow calls. They are probably such a small part of the overall scaling complexity that it doesn't matter, but multiplying by itself should be much faster than Math.Pow for squaring a number.
I am writing a small 2d game engine in C# for my own purposes, and it works fine except for the sprite collision detection. I've decided to make it a per-pixel detection (easiest for me to implement), but it is not working the way it's supposed to. The code detects a collision long before it happens. I've examined every component of the detection, but I can't find the problem.
The collision detection method:
public static bool CheckForCollision(Sprite s1, Sprite s2, bool perpixel) {
if(!perpixel) {
return s1.CollisionBox.IntersectsWith(s2.CollisionBox);
}
else {
Rectangle rect;
Image img1 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s1.Image, s1.Position, s1.Origin, s1.Rotation, out rect), s1.Scale);
int posx1 = rect.X;
int posy1 = rect.Y;
Image img2 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s2.Image, s2.Position, s2.Origin, s2.Rotation, out rect), s2.Scale);
int posx2 = rect.X;
int posy2 = rect.Y;
Rectangle abounds = new Rectangle(posx1, posy1, (int)img1.Width, (int)img1.Height);
Rectangle bbounds = new Rectangle(posx2, posy2, (int)img2.Width, (int)img2.Height);
if(Utilities.RectangleIntersects(abounds, bbounds)) {
uint[] bitsA = s1.GetPixelData(false);
uint[] bitsB = s2.GetPixelData(false);
int x1 = Math.Max(abounds.X, bbounds.X);
int x2 = Math.Min(abounds.X + abounds.Width, bbounds.X + bbounds.Width);
int y1 = Math.Max(abounds.Y, bbounds.Y);
int y2 = Math.Min(abounds.Y + abounds.Height, bbounds.Y + bbounds.Height);
for(int y = y1; y < y2; ++y) {
for(int x = x1; x < x2; ++x) {
if(((bitsA[(x - abounds.X) + (y - abounds.Y) * abounds.Width] & 0xFF000000) >> 24) > 20 &&
((bitsB[(x - bbounds.X) + (y - bbounds.Y) * bbounds.Width] & 0xFF000000) >> 24) > 20)
return true;
}
}
}
return false;
}
}
The image rotation method:
internal static Image RotateImagePoint(Image img, Vector pos, Vector orig, double rotation, out Rectangle sz) {
if(!(new Rectangle(new Point(0), img.Size).Contains(new Point((int)orig.X, (int)orig.Y))))
Console.WriteLine("Origin point is not present in image bound; unwanted cropping might occur");
rotation = (double)ra_de((double)rotation);
sz = GetRotateDimensions((int)pos.X, (int)pos.Y, img.Width, img.Height, rotation, false);
Bitmap bmp = new Bitmap(sz.Width, sz.Height);
Graphics g = Graphics.FromImage(bmp);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.RotateTransform((float)rotation);
g.TranslateTransform(sz.Width / 2, sz.Height / 2, MatrixOrder.Append);
g.DrawImage(img, (float)-orig.X, (float)-orig.Y);
g.Dispose();
return bmp;
}
internal static Rectangle GetRotateDimensions(int imgx, int imgy, int imgwidth, int imgheight, double rotation, bool Crop) {
Rectangle sz = new Rectangle();
if (Crop == true) {
// absolute trig values goes for all angles
double dera = de_ra(rotation);
double sin = Math.Abs(Math.Sin(dera));
double cos = Math.Abs(Math.Cos(dera));
// general trig rules:
// length(adjacent) = cos(theta) * length(hypotenuse)
// length(opposite) = sin(theta) * length(hypotenuse)
// applied width = lo(img height) + la(img width)
sz.Width = (int)(sin * imgheight + cos * imgwidth);
// applied height = lo(img width) + la(img height)
sz.Height = (int)(sin * imgwidth + cos * imgheight);
}
else {
// get image diagonal to fit any rotation (w & h =diagonal)
sz.X = imgx - (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0));
sz.Y = imgy - (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0));
sz.Width = (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0)) * 2;
sz.Height = sz.Width;
}
return sz;
}
Pixel getting method:
public uint[] GetPixelData(bool useBaseImage) {
Rectangle rect;
Image image;
if (useBaseImage)
image = Image;
else
image = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(Image, Position, Origin, Rotation, out rect), Scale);
BitmapData data;
try {
data = ((Bitmap)image).LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
}
catch (ArgumentException) {
data = ((Bitmap)image).LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, image.PixelFormat);
}
byte[] rawdata = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, rawdata, 0, data.Stride * image.Height);
((Bitmap)image).UnlockBits(data);
int pixelsize = 4;
if (data.PixelFormat == PixelFormat.Format24bppRgb)
pixelsize = 3;
else if (data.PixelFormat == PixelFormat.Format32bppArgb || data.PixelFormat == PixelFormat.Format32bppRgb)
pixelsize = 4;
double intdatasize = Math.Ceiling((double)rawdata.Length / pixelsize);
uint[] intdata = new uint[(int)intdatasize];
Buffer.BlockCopy(rawdata, 0, intdata, 0, rawdata.Length);
return intdata;
}
The pixel retrieval method works, and the rotation method works as well, so the only place that the code might be wrong is the collision detection code, but I really have no idea where the problem might be.
I don't think many people here will bother to scrutinize your code to figure out what exactly is wrong. But I can come with some hints to how you can find the problem.
If collision happens long before it is supposed to I suggest your bounding box check isn't working properly.
I would change the code to dump out all the data about rectangles at collision. So you can create some code that will display the situation at collision. That might be easier than looking over the numbers.
Apart from that I doubt that per pixel collision detection easier for you to implement. When you allow for rotation and scaling that quickly becomes difficult to get right. I would do polygon based collision detection instead.
I have made my own 2D engine like you but I used polygon based collision detection and that worked fine.
I think I've found your problem.
internal static Rectangle GetRotateDimensions(int imgx, int imgy, int imgwidth, int imgheight, double rotation, bool Crop) {
Rectangle sz = new Rectangle(); // <-- Default constructed rect here.
if (Crop == true) {
// absolute trig values goes for all angles
double dera = de_ra(rotation);
double sin = Math.Abs(Math.Sin(dera));
double cos = Math.Abs(Math.Cos(dera));
// general trig rules:
// length(adjacent) = cos(theta) * length(hypotenuse)
// length(opposite) = sin(theta) * length(hypotenuse)
// applied width = lo(img height) + la(img width)
sz.Width = (int)(sin * imgheight + cos * imgwidth);
// applied height = lo(img width) + la(img height)
sz.Height = (int)(sin * imgwidth + cos * imgheight);
// <-- Never gets the X & Y assigned !!!
}
Since you never assigned imgx and imgy to the X and Y coordinates of the Rectangle, every call of GetRotateDimensions will produce a Rectangle with the same location. They may be of differing sizes, but they will always be in the default X,Y position. This would cause the really early collisions that you are seeing because any time you tried to detect collisions on two sprites, GetRotateDimensions would put their bounds in the same position regardless of where they actually are.
Once you have corrected that problem, you may run into another error:
Rectangle rect;
Image img1 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s1.Image, s1.Position, s1.Origin, s1.Rotation, out rect), s1.Scale);
// <-- Should resize rect here.
int posx1 = rect.X;
int posy1 = rect.Y;
You get your boundary rect from the RotateImagePoint function, but you then resize the image. The X and Y from the rect are probably not exactly the same as that of the resized boundaries of the image. I'm guessing that you mean for the center of the image to remain in place while all points contract toward or expand from the center in the resize. If this is the case, then you need to resize rect as well as the image in order to get the correct position.
I doubt this is the actual problem, but LockBits doesn't guarantee that the bits data is aligned to the image's Width.
I.e., there may be some padding. You need to access the image using data[x + y * stride] and not data[x + y * width]. The Stride is also part of the BitmapData.