I have x, y coordinates of all vertices of a polygon, say (1,1) (20,10) (10,30), how do I generate a mask with all pixels inside the polygon being 1 whereas outside being 0?
I know there is a function FillPolygon() in C# that looks pretty much doing the job, but it seems to me that it does not return a mask in any way.
Bitmap b = new Bitmap(30, 30);
using (Graphics g = Graphics.FromImage(b))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
g.Clear(Color.Black);
g.FillPolygon(Brushes.White, new Point[] {
new Point(5,5),
new Point(20,20),
new Point(2,15)});
}
byte[,] mask = new byte[b.Width, b.Height];
for (int y = 0; y < b.Height; y++)
for (int x = 0; x < b.Width; x++)
{
mask[x, y] = b.GetPixel(x, y).R > 0 ? 1 : 0;
}
This will of course be more performant if you use direct pixel access using LockBits instead of GetPixel.
I know this is rather old question but in case someone comes around and looks for something similar ...
If you just want to get the mask, there are much better approaches than referencing System.Drawing and actually drawing onto image in memory ...
struct Point
{
public readonly int X;
public readonly int Y;
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}
bool PointInPolygon(Point[] polygon, int x, int y)
{
if(polygon == null || polygon.Length < 3) return false;
int counter = 0;
double intersections;
Point p1 = polygon[0];
Point p2;
for (int i = 1; i <= polygon.Length; i++)
{
p2 = polygon[i % polygon.Length];
if ((y > (p1.Y < p2.Y ? p1.Y : p2.Y)) && (y <= (p1.Y > p2.Y ? p1.Y : p2.Y)) && (x <= (p1.X > p2.X ? p1.X : p2.X)) && (p1.Y != p2.Y))
{
intersections = (y - p1.Y) * (p2.X - p1.X) / (p2.Y - p1.Y) + p1.X;
if (p1.X == p2.X || x <= intersections) counter++;
}
p1 = p2;
}
return counter % 2 != 0;
}
void Main()
{
Point[] polygon = new Point[] { new Point(1,1), new Point(20,10), new Point(10,30) };
bool[,] mask = new bool[30,30];
for(int i=0;i<30;i++)
{
for(int j=0;j<30;j++)
{
mask[i,j] = PointInPolygon(polygon, i, j);
Console.Write(mask[i,j]?"*":".");
}
Console.WriteLine();
}
}
Which will output like this:
..............................
..............................
..............................
..***.........................
...*****......................
...********...................
....**********................
....**************............
.....****************.........
.....*******************......
......*********************...
......************************
.......*********************..
.......*******************....
........****************......
........**************........
.........***********..........
.........*********............
..........******..............
..........****................
..........**..................
..............................
..............................
..............................
..............................
..............................
..............................
..............................
..............................
..............................
For playng with masking, you can use a Region.
Related
I'm trying to make a circle using graphics.DrawLine() property and using BRESENHAM'S CIRCLE ALGORITHM but I'm not able to make it. Here is the method for making the circle
Pen pen = new Pen(Color.Red, 2);
Graphics graphics = this.Shape_PictureBox.CreateGraphics();
int radius = 40;
int x = 0, y = radius;
int xc = 50, yc = 50;
int d = 3 - 2 * radius;
// graphics.DrawLine(pen, xc, yc, x, y);
while (y >= x)
{
x++;
if (d > 0)
{
y--;
d = d + 4 * (x - y) + 10;
}
else
{
d = d + 4 * x + 6;
}
//drawCircle(xc, yc, x, y);
graphics.DrawLine(pen, xc, yc, x, y);
}
Well, there appears to be a bug in your algorithm implementation, as posted - but I suppose you're first and foremost asking as to why nothing is visible in the Shape_PictureBox? You should create a Bitmap buffer (think of it as a canvas) to draw to, and then assign it to the Shape_PictureBox.Image property.
IMPORTANT: Make sure to do this in the Form_Shown, not Form_Load event!
private void Child2_Shown(object sender, EventArgs e)
{
Pen pen = new Pen(Color.Red, 2);
Bitmap canvas = new Bitmap(Shape_PictureBox.Width, Shape_PictureBox.Height);
Graphics graphics = Graphics.FromImage(canvas);
int radius = 40;
int x = 0;
int y = radius;
int xc = 50;
int yc = 50;
int d = 3 - 2 * radius;
graphics.DrawLine(pen, xc, yc, x, y);
while (y >= x)
{
x++;
if (d > 0)
{
y--;
d = d + 4 * (x - y) + 10;
}
else
{
d = d + 4 * x + 6;
}
// drawCircle(xc, yc, x, y);
graphics.DrawLine(pen, xc, yc, x, y);
}
Shape_PictureBox.Image = canvas;
}
Currently, it looks like this:
You'll need to revise your implementation of Bresenham's Circle algorithm :)
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 used this code for segmentation, I'm trying to detect pixels one by one because my object is a binary, not a grayscale. when i run the program, it draws 2 object. The first object is successfully drawn (object still has a black color and a red rectangle), but the second object fails get drawn. Screenshot is here. Please help me, why does this happen?
#region Edge Detection
private void btnSegmentasi_Click(object sender, EventArgs e)
{
Segments = new List<ImageSegment>();
Bitmap bmp = (Bitmap)pb2.Image;
imageArea = new Rectangle(0, 0, pb2.Image.Width - 1, pb2.Image.Height - 1);
for (int y = 0; y < pb2.Image.Height; y++)
{
for (int x = 0; x < pb2.Image.Width; x++)
{
bool skip = false;
foreach (ImageSegment segment in Segments)
{
if (pointIsInRect(x, y, segment.Rect))
{
skip = true;
break;
}
}
if (skip) continue;
Color warna = bmp.GetPixel(x, y);
if (warna.G == 0)
startEdgeDetection(x, y, ref bmp);
}
}
DGVProses.DataSource = Segments;
if (Segments.Count > 0)
{
Graphics g = pb2.CreateGraphics();
Rectangle[] rects = (from theSegment in Segments select theSegment.Rect).ToArray();
g.DrawRectangles(new Pen(Brushes.Red), rects);
g.Dispose();
}
}
private void startEdgeDetection(int x, int y, ref Bitmap bmp)
{
Point startPoint = new Point(x, y);
Point currPoint = new Point(x, y);
int sudut = 180;
int xMin = x, yMin = y, xMax = x, yMax = y;
do
{
sudut -= 45;
Point offset = angleToPoint(ref sudut);
Point trialPoint = new Point(currPoint.X + offset.X, currPoint.Y + offset.Y);
if (!pointIsInRect(trialPoint.X, trialPoint.Y, imageArea))
continue;
Color theColor = bmp.GetPixel(trialPoint.X, trialPoint.Y);
if (theColor.G == 0)
{
currPoint = trialPoint;
sudut -= 180;
if (currPoint.X > xMax)
xMax = currPoint.X;
else if (currPoint.X < xMin)
xMin = currPoint.X;
if (currPoint.Y > yMax)
yMax = currPoint.Y;
else if (currPoint.Y < yMin)
yMin = currPoint.Y;
if (sudut < 0)
sudut += 360;
if (currPoint == startPoint && sudut == 180)
break;
}
}
while (!(currPoint == startPoint && sudut == 180));
Rectangle r = new Rectangle(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
Bitmap newImage = new Bitmap(r.Width + 2, r.Height + 2);
using (Graphics g = Graphics.FromImage(newImage))
{
g.FillRectangle(Brushes.White, 0, 0, newImage.Width, newImage.Height);
g.DrawImage(bmp, new Rectangle(1, 1, r.Width, r.Height), r, GraphicsUnit.Pixel);
g.Dispose();
}
Segments.Add(new ImageSegment(r, newImage));
}
private Point angleToPoint(ref int sudut)
{
if (sudut < 0)
sudut += 360;
switch (sudut)
{
case 135: return new Point(-1, -1);
case 90: return new Point(0, -1);
case 45: return new Point(1, -1);
case 0: return new Point(1, 0);
case 315: return new Point(1, 1);
case 270: return new Point(0, 1);
case 225: return new Point(-1, 1);
default: return new Point(-1, 0);
}
}
private bool pointIsInRect(int x, int y, Rectangle rect)
{
if (x < rect.X)
return false;
if (x > rect.X + rect.Width)
return false;
if (x < rect.Y)
return false;
if (x > rect.Y + rect.Height)
return false;
return true;
}
#endregion
Okay, I think I've now got a clue of how your algorithm is supposed to work. I'd guess you are running around in circles within the object. I do not really know why it does not happen for the first object, but this is another story.
When you enter startEdgeDetection you start at some point, check if it's black, move by an angle and repeat the whole procedure. You stop when the current point reaches the starting point. The crux is, that this algorithm does not guarantee to walk the whole object, but may just do the following (I do not know it is exactly like this, but pretty much):
OOOOOO
O####O
O####O
OOOOOO
OOOOOO
O*###O
O####O
OOOOOO
OOOOOO
O**##O
O####O
OOOOOO
OOOOOO
O**##O
O#*##O
OOOOOO
OOOOOO
O**##O
O**##O
OOOOOO
O = pixels filled with white
# = pixels filled with black
* = pixels you stepped through
You've reached your starting point again and the algorithm stops, but the bounding box does not contain the whole object, but just a part. If all of your objects bounding boxes have either a width or a height of 1 you fill up your whole object with bounding boxes, hence it appears red.
You'll have to fix the startEdgeDetection to avoid the described case and make sure that you really detect the edge.
I made up a simple class that finds the bounding box of an object. It should be easy to apply it to your problem.
public class BoundingBoxCalculator
{
Bitmap bitmapToCalculateBoundingBoxFor;
Point startingPoint;
Point[] neighborOffsets =
{new Point(-1,-1),
new Point(0,-1),
new Point(1,-1),
new Point(-1, 0),
new Point(1, 0),
new Point(-1,1),
new Point(0,1),
new Point(1,1)};
public BoundingBoxCalculator(Bitmap bitmapContainingObject, Point borderPoint)
{
this.bitmapToCalculateBoundingBoxFor = bitmapContainingObject;
this.startingPoint = borderPoint;
}
public Rectangle CalculateBoundingBox()
{
List<Point> edgePoints = CalculateEdge();
int minX = edgePoints.Min(p => p.X);
int maxX = edgePoints.Max(p => p.X);
int minY = edgePoints.Min(p => p.Y);
int maxY = edgePoints.Max(p => p.Y);
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
List<Point> CalculateEdge()
{
List<Point> edgePoints = new List<Point>();
Point currentPoint = startingPoint;
do
{
IEnumerable<Point> neighboringEdgePoints = GetNeighboringEdgePoints(currentPoint);
IEnumerable<Point> neighboringEdgePointsNotVisited = from p in neighboringEdgePoints where !edgePoints.Contains(p) select p;
edgePoints.Add(currentPoint);
if(neighboringEdgePointsNotVisited.Count() == 0
&& neighboringEdgePoints.Contains(startingPoint))
{
currentPoint = startingPoint;
}
else if(neighboringEdgePointsNotVisited.Count() == 1)
{
Point nextPoint = neighboringEdgePointsNotVisited.First();
currentPoint = nextPoint;
}
else if(neighboringEdgePointsNotVisited.Count() > 1)
{
Point nextPoint = GetPointWithMinDistance(currentPoint, neighboringEdgePointsNotVisited);
currentPoint = nextPoint;
}
else
{
throw new Exception();
}
} while(currentPoint != startingPoint);
return edgePoints;
}
Point GetPointWithMinDistance(Point origin, IEnumerable<Point> pointsToTest)
{
double minDistance = double.MaxValue;
Point pointWithMinDistance = new Point(0,0);
foreach(Point pointToTest in pointsToTest)
{
double currentDistance = GetPointsDistance(origin, pointToTest);
if(currentDistance < minDistance)
{
minDistance = currentDistance;
pointWithMinDistance = pointToTest;
}
}
return pointWithMinDistance;
}
double GetPointsDistance(Point p1, Point p2)
{
return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
}
IEnumerable<Point> GetNeighboringEdgePoints(Point currentPoint)
{
IEnumerable<Point> neighboringPoints = GetNeighboringPoints(currentPoint);
List<Point> neighboringEdgePoints = new List<Point>();
foreach(Point pointToTest in neighboringPoints)
{
if(GetNeighboringPoints(pointToTest).Count() < 8)
{
neighboringEdgePoints.Add(pointToTest);
}
}
return neighboringEdgePoints;
}
IEnumerable<Point> GetNeighboringPoints(Point currentPoint)
{
List<Point> neighbors = new List<Point>();
for(int offsetsCount = 0; offsetsCount < neighborOffsets.Length; offsetsCount++)
{
Point currentPointWithOffset = AddPointOffset(currentPoint, neighborOffsets[offsetsCount]);
if(IsInImage(currentPointWithOffset) &&
IsInObject(currentPointWithOffset))
{
neighbors.Add(currentPointWithOffset);
}
}
return neighbors;
}
bool IsInImage(Point pointToTest)
{
return pointToTest.X >= 0
&& pointToTest.X < bitmapToCalculateBoundingBoxFor.Width
&& pointToTest.Y >= 0
&& pointToTest.Y < bitmapToCalculateBoundingBoxFor.Height;
}
bool IsInObject(Point pointToTest)
{
Color colorInPointPosition = bitmapToCalculateBoundingBoxFor.GetPixel(pointToTest.X, pointToTest.Y);
//assume object is color is not white
return colorInPointPosition.R != 255
|| colorInPointPosition.G != 255
|| colorInPointPosition.B != 255;
}
Point AddPointOffset(Point point, Point offset)
{
return new Point(point.X + offset.X, point.Y + offset.Y);
}
}
Find an example at:
https://dotnetfiddle.net/49bnTV
I just tested it with a rectangle, but I guess it should work with any shape. Just give it a try.
I have three points, for example:
Start 194 171
Right 216 131
Left 216 203
I want to get all the points within that triangle. How would I do that efficiently?
see z3nth10n's answer for better input validation
Introduction:
The general idea was to get the triangle's edges (y-Wise) for every x in it's range, and then you have all the y's that exist within the triangle for every single x, which with simple conversion turns into all points within the triangle.
You can look at it as if you cut the triangle into stripes, each being of width 1.
So for X=0, on the line between A and B, the Y is 6, and on the line between A and C, the Y is -2, so you can see that the stripe of X=0 is between -2 and 6. Therefore, you can tell that (0, -2) (0, -1) (0, 0) ... (0, 5) (0, 6) are all in the triangle. Doing that for X's between the smallest and the largest within the triangle, and you have all the points in the triangle!
Speed:
For the triangle (0, 0) (1, 8) (4, 6) - found 16 points.
Done 1,000,000 times in 3.68 seconds.
Implementation:
public IEnumerable<Point> PointsInTriangle(Point pt1, Point pt2, Point pt3)
{
if (pt1.Y == pt2.Y && pt1.Y == pt3.Y)
{
throw new ArgumentException("The given points must form a triangle.");
}
Point tmp;
if (pt2.X < pt1.X)
{
tmp = pt1;
pt1 = pt2;
pt2 = tmp;
}
if (pt3.X < pt2.X)
{
tmp = pt2;
pt2 = pt3;
pt3 = tmp;
if (pt2.X < pt1.X)
{
tmp = pt1;
pt1 = pt2;
pt2 = tmp;
}
}
var baseFunc = CreateFunc(pt1, pt3);
var line1Func = pt1.X == pt2.X ? (x => pt2.Y) : CreateFunc(pt1, pt2);
for (var x = pt1.X; x < pt2.X; x++)
{
int maxY;
int minY = GetRange(line1Func(x), baseFunc(x), out maxY);
for (var y = minY; y <= maxY; y++)
{
yield return new Point(x, y);
}
}
var line2Func = pt2.X == pt3.X ? (x => pt2.Y) : CreateFunc(pt2, pt3);
for (var x = pt2.X; x <= pt3.X; x++)
{
int maxY;
int minY = GetRange(line2Func(x), baseFunc(x), out maxY);
for (var y = minY; y <= maxY; y++)
{
yield return new Point(x, y);
}
}
}
private int GetRange(double y1, double y2, out int maxY)
{
if (y1 < y2)
{
maxY = (int)Math.Floor(y2);
return (int)Math.Ceiling(y1);
}
maxY = (int)Math.Floor(y1);
return (int)Math.Ceiling(y2);
}
private Func<int, double> CreateFunc(Point pt1, Point pt2)
{
var y0 = pt1.Y;
if (y0 == pt2.Y)
{
return x => y0;
}
var m = (double)(pt2.Y - y0) / (pt2.X - pt1.X);
return x => m * (x - pt1.X) + y0;
}
#SimpleVar answer is well, but it lacks a good check for valid triangles. (This can cause an overflow problem).
So I do my own implementation for Unity3D:
public static IEnumerable<T> PointsInTriangle<T>(T pt1, T pt2, T pt3)
where T : IPoint
{
/*
// https://www.geeksforgeeks.org/check-whether-triangle-valid-not-sides-given/
a + b > c
a + c > b
b + c > a
*/
float a = Vector2.Distance(new Vector2(pt1.x, pt1.y), new Vector2(pt2.x, pt2.y)),
b = Vector2.Distance(new Vector2(pt2.x, pt2.y), new Vector2(pt3.x, pt3.y)),
c = Vector2.Distance(new Vector2(pt3.x, pt3.y), new Vector2(pt1.x, pt1.y));
if (a + b <= c || a + c <= b || b + c <= a)
{
Debug.LogWarning($"The given points must form a triangle. {{{pt1}, {pt2}, {pt3}}}");
yield break;
}
if (TriangleArea(pt1, pt2, pt3) <= 1)
{
Point center = GetTriangleCenter(pt1, pt2, pt3);
yield return (T)Activator.CreateInstance(typeof(T), center.x, center.y);
return;
}
T tmp;
if (pt2.x < pt1.x)
{
tmp = pt1;
pt1 = pt2;
pt2 = tmp;
}
if (pt3.x < pt2.x)
{
tmp = pt2;
pt2 = pt3;
pt3 = tmp;
if (pt2.x < pt1.x)
{
tmp = pt1;
pt1 = pt2;
pt2 = tmp;
}
}
var baseFunc = CreateFunc(pt1, pt3);
var line1Func = pt1.x == pt2.x ? (x => pt2.y) : CreateFunc(pt1, pt2);
for (var x = pt1.x; x < pt2.x; ++x)
{
int maxY;
int minY = GetRange(line1Func(x), baseFunc(x), out maxY);
for (int y = minY; y <= maxY; ++y)
yield return (T)Activator.CreateInstance(typeof(T), x, y);
}
var line2Func = pt2.x == pt3.x ? (x => pt2.y) : CreateFunc(pt2, pt3);
for (var x = pt2.x; x <= pt3.x; ++x)
{
int maxY;
int minY = GetRange(line2Func(x), baseFunc(x), out maxY);
for (int y = minY; y <= maxY; ++y)
yield return (T)Activator.CreateInstance(typeof(T), x, y);
}
}
private static int GetRange(float y1, float y2, out int maxY)
{
if (y1 < y2)
{
maxY = Mathf.FloorToInt(y2);
return Mathf.CeilToInt(y1);
}
maxY = Mathf.FloorToInt(y1);
return Mathf.CeilToInt(y2);
}
private static Func<int, float> CreateFunc<T>(T pt1, T pt2)
where T : IPoint
{
var y0 = pt1.y;
if (y0 == pt2.y)
return x => y0;
float m = (float)(pt2.y - y0) / (pt2.x - pt1.x);
return x => m * (x - pt1.x) + y0;
}
public static float TriangleArea<T>(T p1, T p2, T p3)
where T : IPoint
{
float a, b, c;
if (!CheckIfValidTriangle(p1, p2, p3, out a, out b, out c))
return 0;
return TriangleArea(a, b, c);
}
public static float TriangleArea(float a, float b, float c)
{
// Thanks to: http://james-ramsden.com/area-of-a-triangle-in-3d-c-code/
float s = (a + b + c) / 2.0f;
return Mathf.Sqrt(s * (s - a) * (s - b) * (s - c));
}
public static Point GetTriangleCenter<T>(T p0, T p1, T p2)
where T : IPoint
{
// Thanks to: https://stackoverflow.com/questions/524755/finding-center-of-2d-triangle
return new Point(p0.x + p1.x + p2.x / 3, p0.y + p1.y + p2.y / 3);
}
public static bool CheckIfValidTriangle<T>(T v1, T v2, T v3, out float a, out float b, out float c)
where T : IPoint
{
a = Vector2.Distance(new Vector2(v1.x, v1.y), new Vector2(v2.x, v2.y));
b = Vector2.Distance(new Vector2(v2.x, v2.y), new Vector2(v3.x, v3.y));
c = Vector2.Distance(new Vector2(v3.x, v3.y), new Vector2(v1.x, v1.y));
if (a + b <= c || a + c <= b || b + c <= a)
return false;
return true;
}
IPoint interface could be a good point for own Point implementations (for libs like Clipper, TessDotNet or Poly2Tri). You can change it at any time (two UnityEngine.Vector2 or System.Drawing.Point).
Hope this helps!
EDIT: I solved all bugs here:
Also I answered my own question asking this: https://stackoverflow.com/a/53734816/3286975
I'm attempting to add semi-realistic water into my tile-based, 2D platformer. The water must act somewhat lifelike, with a pressure model that runs entirely local. (IE. Can only use data from cells near it) This model is needed because of the nature of my game, where you cannot be certain that the data you need isn't inside an area that isn't in memory.
I've tried one method so far, but I could not refine it enough to work with my constraints.
For that model, each cell would be slightly compressible, depending on the amount of water in the above cell. When a cell's water content was larger than the normal capacity, the cell would try to expand upwards. This created a fairly nice simulation, abeit slow (Not lag; Changes in the water were taking a while to propagate.), at times. When I tried to implement this into my engine, I found that my limitations lacked the precision required for it to work. I can provide a more indepth explanation or a link to the original concept if you wish.
My constraints:
Only 256 discrete values for water level. (No floating point variables :( ) -- EDIT. Floats are fine.
Fixed grid size.
2D Only.
U-Bend Configurations must work.
The language that I'm using is C#, but I can probably take other languages and translate it to C#.
The question is, can anyone give me a pressure model for water, following my constraints as closely as possible?
How about a different approach?
Forget about floats, that's asking for roundoff problems in the long run. Instead, how about a unit of water?
Each cell contains a certain number of units of water. Each iteration you compare the cell with it's 4 neighbors and move say 10% (change this to alter the propagation speed) of the difference in the number of units of water. A mapping function translates the units of water into a water level.
To avoid calculation order problems use two values, one for the old units, one for the new. Calculate everything and then copy the updated values back. 2 ints = 8 bytes per cell. If you have a million cells that's still only 8mb.
If you are actually trying to simulate waves you'll need to also store the flow--4 values, 16 mb. To make a wave put some inertia to the flow--after you calculate the desired flow then move the previous flow say 10% of the way towards the desired value.
Try treating each contiguous area of water as a single area (like flood fill) and track 1) the lowest cell(s) where water can escape and 2) the highest cell(s) from which water can come, then move water from the top to the bottom. This isn't local, but I think you can treat the edges of the area you want to affect as not connected and process any subset that you want. Re-evaluate what areas are contiguous on each frame (re-flood on each frame) so that when blobs converge, they can start being treated as one.
Here's my code from a Windows Forms demo of the idea. It may need some fine tuning, but seems to work quite well in my tests:
public partial class Form1 : Form
{
byte[,] tiles;
const int rows = 50;
const int cols = 50;
public Form1()
{
SetStyle(ControlStyles.ResizeRedraw, true);
InitializeComponent();
tiles = new byte[cols, rows];
for (int i = 0; i < 10; i++)
{
tiles[20, i+20] = 1;
tiles[23, i+20] = 1;
tiles[32, i+20] = 1;
tiles[35, i+20] = 1;
tiles[i + 23, 30] = 1;
tiles[i + 23, 32] = 1;
tiles[21, i + 15] = 2;
tiles[21, i + 4] = 2;
if (i % 2 == 0) tiles[22, i] = 2;
}
tiles[20, 30] = 1;
tiles[20, 31] = 1;
tiles[20, 32] = 1;
tiles[21, 32] = 1;
tiles[22, 32] = 1;
tiles[33, 32] = 1;
tiles[34, 32] = 1;
tiles[35, 32] = 1;
tiles[35, 31] = 1;
tiles[35, 30] = 1;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (SolidBrush b = new SolidBrush(Color.White))
{
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < cols; x++)
{
switch (tiles[x, y])
{
case 0:
b.Color = Color.White;
break;
case 1:
b.Color = Color.Black;
break;
default:
b.Color = Color.Blue;
break;
}
e.Graphics.FillRectangle(b, x * ClientSize.Width / cols, y * ClientSize.Height / rows,
ClientSize.Width / cols + 1, ClientSize.Height / rows + 1);
}
}
}
}
private bool IsLiquid(int x, int y)
{
return tiles[x, y] > 1;
}
private bool IsSolid(int x, int y)
{
return tiles[x, y] == 1;
}
private bool IsEmpty(int x, int y)
{
return IsEmpty(tiles, x, y);
}
public static bool IsEmpty(byte[,] tiles, int x, int y)
{
return tiles[x, y] == 0;
}
private void ProcessTiles()
{
byte processedValue = 0xFF;
byte unprocessedValue = 0xFF;
for (int y = 0; y < rows; y ++)
for (int x = 0; x < cols; x++)
{
if (IsLiquid(x, y))
{
if (processedValue == 0xff)
{
unprocessedValue = tiles[x, y];
processedValue = (byte)(5 - tiles[x, y]);
}
if (tiles[x, y] == unprocessedValue)
{
BlobInfo blob = GetWaterAt(new Point(x, y), unprocessedValue, processedValue, new Rectangle(0, 0, 50, 50));
blob.ProcessMovement(tiles);
}
}
}
}
class BlobInfo
{
private int minY;
private int maxEscapeY;
private List<int> TopXes = new List<int>();
private List<int> BottomEscapeXes = new List<int>();
public BlobInfo(int x, int y)
{
minY = y;
maxEscapeY = -1;
TopXes.Add(x);
}
public void NoteEscapePoint(int x, int y)
{
if (maxEscapeY < 0)
{
maxEscapeY = y;
BottomEscapeXes.Clear();
}
else if (y < maxEscapeY)
return;
else if (y > maxEscapeY)
{
maxEscapeY = y;
BottomEscapeXes.Clear();
}
BottomEscapeXes.Add(x);
}
public void NoteLiquidPoint(int x, int y)
{
if (y < minY)
{
minY = y;
TopXes.Clear();
}
else if (y > minY)
return;
TopXes.Add(x);
}
public void ProcessMovement(byte[,] tiles)
{
int min = TopXes.Count < BottomEscapeXes.Count ? TopXes.Count : BottomEscapeXes.Count;
for (int i = 0; i < min; i++)
{
if (IsEmpty(tiles, BottomEscapeXes[i], maxEscapeY) && (maxEscapeY > minY))
{
tiles[BottomEscapeXes[i], maxEscapeY] = tiles[TopXes[i], minY];
tiles[TopXes[i], minY] = 0;
}
}
}
}
private BlobInfo GetWaterAt(Point start, byte unprocessedValue, byte processedValue, Rectangle bounds)
{
Stack<Point> toFill = new Stack<Point>();
BlobInfo result = new BlobInfo(start.X, start.Y);
toFill.Push(start);
do
{
Point cur = toFill.Pop();
while ((cur.X > bounds.X) && (tiles[cur.X - 1, cur.Y] == unprocessedValue))
cur.X--;
if ((cur.X > bounds.X) && IsEmpty(cur.X - 1, cur.Y))
result.NoteEscapePoint(cur.X - 1, cur.Y);
bool pushedAbove = false;
bool pushedBelow = false;
for (; ((cur.X < bounds.X + bounds.Width) && tiles[cur.X, cur.Y] == unprocessedValue); cur.X++)
{
result.NoteLiquidPoint(cur.X, cur.Y);
tiles[cur.X, cur.Y] = processedValue;
if (cur.Y > bounds.Y)
{
if (IsEmpty(cur.X, cur.Y - 1))
{
result.NoteEscapePoint(cur.X, cur.Y - 1);
}
if ((tiles[cur.X, cur.Y - 1] == unprocessedValue) && !pushedAbove)
{
pushedAbove = true;
toFill.Push(new Point(cur.X, cur.Y - 1));
}
if (tiles[cur.X, cur.Y - 1] != unprocessedValue)
pushedAbove = false;
}
if (cur.Y < bounds.Y + bounds.Height - 1)
{
if (IsEmpty(cur.X, cur.Y + 1))
{
result.NoteEscapePoint(cur.X, cur.Y + 1);
}
if ((tiles[cur.X, cur.Y + 1] == unprocessedValue) && !pushedBelow)
{
pushedBelow = true;
toFill.Push(new Point(cur.X, cur.Y + 1));
}
if (tiles[cur.X, cur.Y + 1] != unprocessedValue)
pushedBelow = false;
}
}
if ((cur.X < bounds.X + bounds.Width) && (IsEmpty(cur.X, cur.Y)))
{
result.NoteEscapePoint(cur.X, cur.Y);
}
} while (toFill.Count > 0);
return result;
}
private void timer1_Tick(object sender, EventArgs e)
{
ProcessTiles();
Invalidate();
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
int x = e.X * cols / ClientSize.Width;
int y = e.Y * rows / ClientSize.Height;
if ((x >= 0) && (x < cols) && (y >= 0) && (y < rows))
tiles[x, y] = 2;
}
}
}
From a fluid dynamics viewpoint, a reasonably popular lattice-based algorithm family is the so-called Lattice Boltzmann method. A simple implementation, ignoring all the fine detail that makes academics happy, should be relatively simple and fast and also get reasonably correct dynamics.