I have two Line Segments, represented by a 3D point at their beginning/end points.
Line:
class Line
{
public string Name { get; set; }
public Point3D Start { get; set; } = new Point3D();
public Point3D End { get; set; } = new Point3D();
}
The 3D points are just 3 doubles for coordinates X,Y and Z.
3DPoint:
class Point3D
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
The Question:
Can I find the distance between two 'Lines' and the endpoints of that distance 'Line'. [1
What I have:
Currently, I can successfully get the distance between the two lines with this code (Adapted From Here Using the Segment To Segment Section):
public double lineNearLine(Line l1, Line l2)
{
Vector3D uS = new Vector3D { X = l1.Start.X, Y = l1.Start.Y, Z = l1.Start.Z };
Vector3D uE = new Vector3D { X = l1.End.X, Y = l1.End.Y, Z = l1.End.Z };
Vector3D vS = new Vector3D { X = l2.Start.X, Y = l2.Start.Y, Z = l2.Start.Z };
Vector3D vE = new Vector3D { X = l2.End.X, Y = l2.End.Y, Z = l2.End.Z };
Vector3D w1 = new Vector3D { X = l1.Start.X, Y = l1.Start.Y, Z = l1.Start.Z };
Vector3D w2 = new Vector3D { X = l2.Start.X, Y = l2.Start.Y, Z = l2.Start.Z };
Vector3D u = uE - uS;
Vector3D v = vE - vS;
Vector3D w = w1 - w2;
double a = Vector3D.DotProduct(u, u);
double b = Vector3D.DotProduct(u, v);
double c = Vector3D.DotProduct(v, v);
double d = Vector3D.DotProduct(u, w);
double e = Vector3D.DotProduct(v, w);
double D = a * c - b * b;
double sc, sN, sD = D;
double tc, tN, tD = D;
if (D < 0.01)
{
sN = 0;
sD = 1;
tN = e;
tD = c;
}
else
{
sN = (b * e - c * d);
tN = (a * e - b * d);
if (sN < 0)
{
sN = 0;
tN = e;
tD = c;
}
else if (sN > sD)
{
sN = sD;
tN = e + b;
tD = c;
}
}
if (tN < 0)
{
tN = 0;
if (-d < 0)
{
sN = 0;
}
else if (-d > a)
{
sN = sD;
}
else
{
sN = -d;
sD = a;
}
}
else if (tN > tD)
{
tN = tD;
if ((-d + b) < 0)
{
sN = 0;
}
else if ((-d + b) > a)
{
sN = sD;
}
else
{
sN = (-d + b);
sD = a;
}
}
if (Math.Abs(sN) < 0.01)
{
sc = 0;
}
else
{
sc = sN / sD;
}
if (Math.Abs(tN) < 0.01)
{
tc = 0;
}
else
{
tc = tN / tD;
}
Vector3D dP = w + (sc * u) - (tc * v);
double distance1 = Math.Sqrt(Vector3D.DotProduct(dP, dP));
return distance1;
}
What I Need:
Is there any way to determine the endpoints of the displacement vector 'dP' from the code above? If not, can anyone suggest a better method for finding minimum distance and the endpoints of that distance?
Thank you for Reading, and Thanks in advance for any suggestions!
An Enormous Thank You to #Isaac van Bakel for the theory behind this Solution
Here is my code complete: Shortest distance between two lines represented by the line that connects them at that shortest distance.
Classes:
Sharp3D.Math : I use this reference for Vector3D, but really any 3D vector class will work. On top of that, the vectors aren't even required if you do the subtraction element by element.
Point3D : My Personal Point3D class. Feel free to use as much or little as you want.
class Point3D
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
public Vector3D getVector()
{
return new Vector3D { X = this.X, Y = this.Y, Z = this.Z };
}
}
Line : My Personal Line class. Feel free to use as much or little as you want.
class Line
{
public string Name { get; set; }
public Point3D Start { get; set; } = new Point3D();
public Point3D End { get; set; } = new Point3D();
public double Length
{
get
{
return Math.Sqrt(Math.Pow((End.X - Start.X), 2) + Math.Pow((End.Y - Start.Y), 2));
}
}
}
Functions:
ClampPointToLine : Clamping function I wrote to clamp a point to a line.
public Point3D ClampPointToLine(Point3D pointToClamp, Line lineToClampTo)
{
Point3D clampedPoint = new Point3D();
double minX, minY, minZ, maxX, maxY, maxZ;
if(lineToClampTo.Start.X <= lineToClampTo.End.X)
{
minX = lineToClampTo.Start.X;
maxX = lineToClampTo.End.X;
}
else
{
minX = lineToClampTo.End.X;
maxX = lineToClampTo.Start.X;
}
if (lineToClampTo.Start.Y <= lineToClampTo.End.Y)
{
minY = lineToClampTo.Start.Y;
maxY = lineToClampTo.End.Y;
}
else
{
minY = lineToClampTo.End.Y;
maxY = lineToClampTo.Start.Y;
}
if (lineToClampTo.Start.Z <= lineToClampTo.End.Z)
{
minZ = lineToClampTo.Start.Z;
maxZ = lineToClampTo.End.Z;
}
else
{
minZ = lineToClampTo.End.Z;
maxZ = lineToClampTo.Start.Z;
}
clampedPoint.X = (pointToClamp.X < minX) ? minX : (pointToClamp.X > maxX) ? maxX : pointToClamp.X;
clampedPoint.Y = (pointToClamp.Y < minY) ? minY : (pointToClamp.Y > maxY) ? maxY : pointToClamp.Y;
clampedPoint.Z = (pointToClamp.Z < minZ) ? minZ : (pointToClamp.Z > maxZ) ? maxZ : pointToClamp.Z;
return clampedPoint;
}
distanceBetweenLines : The function that returns the line that represents the shortest distance between two lines. Returns null if unsolvable.
public Line distBetweenLines(Line l1, Line l2)
{
Vector3D p1, p2, p3, p4, d1, d2;
p1 = l1.Start.getVector();
p2 = l1.End.getVector();
p3 = l2.Start.getVector();
p4 = l2.End.getVector();
d1 = p2 - p1;
d2 = p4 - p3;
double eq1nCoeff = (d1.X * d2.X) + (d1.Y * d2.Y) + (d1.Z * d2.Z);
double eq1mCoeff = (-(Math.Pow(d1.X, 2)) - (Math.Pow(d1.Y, 2)) - (Math.Pow(d1.Z, 2)));
double eq1Const = ((d1.X * p3.X) - (d1.X * p1.X) + (d1.Y * p3.Y) - (d1.Y * p1.Y) + (d1.Z * p3.Z) - (d1.Z * p1.Z));
double eq2nCoeff = ((Math.Pow(d2.X, 2)) + (Math.Pow(d2.Y, 2)) + (Math.Pow(d2.Z, 2)));
double eq2mCoeff = -(d1.X * d2.X) - (d1.Y * d2.Y) - (d1.Z * d2.Z);
double eq2Const = ((d2.X * p3.X) - (d2.X * p1.X) + (d2.Y * p3.Y) - (d2.Y * p2.Y) + (d2.Z * p3.Z) - (d2.Z * p1.Z));
double[,] M = new double[,] { { eq1nCoeff, eq1mCoeff, -eq1Const }, { eq2nCoeff, eq2mCoeff, -eq2Const } };
int rowCount = M.GetUpperBound(0) + 1;
// pivoting
for (int col = 0; col + 1 < rowCount; col++) if (M[col, col] == 0)
// check for zero coefficients
{
// find non-zero coefficient
int swapRow = col + 1;
for (; swapRow < rowCount; swapRow++) if (M[swapRow, col] != 0) break;
if (M[swapRow, col] != 0) // found a non-zero coefficient?
{
// yes, then swap it with the above
double[] tmp = new double[rowCount + 1];
for (int i = 0; i < rowCount + 1; i++)
{ tmp[i] = M[swapRow, i]; M[swapRow, i] = M[col, i]; M[col, i] = tmp[i]; }
}
else return null; // no, then the matrix has no unique solution
}
// elimination
for (int sourceRow = 0; sourceRow + 1 < rowCount; sourceRow++)
{
for (int destRow = sourceRow + 1; destRow < rowCount; destRow++)
{
double df = M[sourceRow, sourceRow];
double sf = M[destRow, sourceRow];
for (int i = 0; i < rowCount + 1; i++)
M[destRow, i] = M[destRow, i] * df - M[sourceRow, i] * sf;
}
}
// back-insertion
for (int row = rowCount - 1; row >= 0; row--)
{
double f = M[row, row];
if (f == 0) return null;
for (int i = 0; i < rowCount + 1; i++) M[row, i] /= f;
for (int destRow = 0; destRow < row; destRow++)
{ M[destRow, rowCount] -= M[destRow, row] * M[row, rowCount]; M[destRow, row] = 0; }
}
double n = M[0, 2];
double m = M[1, 2];
Point3D i1 = new Point3D { X = p1.X + (m * d1.X), Y = p1.Y + (m * d1.Y), Z = p1.Z + (m * d1.Z) };
Point3D i2 = new Point3D { X = p3.X + (n * d2.X), Y = p3.Y + (n * d2.Y), Z = p3.Z + (n * d2.Z) };
Point3D i1Clamped = ClampPointToLine(i1, l1);
Point3D i2Clamped = ClampPointToLine(i2, l2);
return new Line { Start = i1Clamped, End = i2Clamped };
}
Implementation:
Line shortestDistanceLine = distBetweenLines(l1, l2);
Results:
So far this has been accurate in my testing. Returns null if passed two identical lines. I appreciate any feedback!
The shortest distance between two skew lines (lines which don't intersect) is the distance of the line which is perpendicular to both of them.
If we have a line l1 with known points p1 and p2, and a line l2 with known points p3 and p4:
The direction vector of l1 is p2-p1, or d1.
The direction vector of l2 is p4-p3, or d2.
We therefore know that the vector we are looking for, v, is perpendicular to both of these direction vectors:
d1.v = 0 & d2.v = 0
Or, if you prefer:
d1x*vx + d1y*vy + d1z*vz = 0
And the same for d2.
Let's take the point on the lines l1, l2 where v is actually perpendicular to the direction. We'll call these two points i1 and i2 respectively.
Since i1 lies on l1, we can say that i1 = p1 + m*d1, where m is some number.
Similarly, i2 = p3 + n*d2, where n is another number.
Since v is the vector between i1 and i2 (by definition) we get that v = i2 - i1.
This gives the substitutions for the x,y,z vectors of v:
vx = i2x - i1x = (p3x + n*d2x) - (p1x + m*d1x)
and so on.
Which you can now substitute back into your dot product equation:
d1x * ( (p3x + n*d2x) - (p1x + m*d1x) ) + ... = 0
This has reduced our number of equations to 2 (the two dot product equations) with two unknowns (m and n), so you can now solve them!
Once you have m and n, you can find the coordinates by going back to the original calculation of i1 and i2.
If you only wanted the shortest distance for points on the segment between p1-p2 and p3-p4, you can clamp i1 and i2 between these ranges of coordinates, since the shortest distance will always be as close to the perpendicular as possible.
The code above return a wrong value so I take the idea from Python code (Shortest distance between two line segments) and I converted for C#. It necessary to use Numpy lib for C#:
public static Tuple<NDarray, NDarray, NDarray> closestDistanceBetweenLines(
NDarray a0,
NDarray a1,
NDarray b0,
NDarray b1,
bool clampAll = false,
bool clampA0 = false,
bool clampA1 = false,
bool clampB0 = false,
bool clampB1 = false)
{
// If clampAll=True, set all clamps to True
if (clampAll)
{
clampA0 = true;
clampA1 = true;
clampB0 = true;
clampB1 = true;
}
// Calculate denomitator
NDarray A = a1 - a0;
NDarray B = b1 - b0;
NDarray magA = np.linalg.norm(A);
NDarray magB = np.linalg.norm(B);
NDarray _A = A / magA;
NDarray _B = B / magB;
NDarray cross = np.cross(_A, _B);
double denom = Math.Pow((float)np.linalg.norm(cross), 2);
// If lines are parallel (denom=0) test if lines overlap.
// If they don't overlap then there is a closest point solution.
// If they do overlap, there are infinite closest positions, but there is a closest distance
if (denom == 0)
{
NDarray d0 = np.dot(_A, b0 - a0);
// Overlap only possible with clamping
if (clampA0 || clampA1 || clampB0 || clampB1)
{
NDarray d1 = np.dot(_A, b1 - a0);
// Is segment B before A?
if ((float)d0 <= 0F || (float)d0 >= (float)d1)
{
if (clampA0 && clampB1)
{
if ( (float)np.absolute(d0) < (float)np.absolute(d1) )
{
return Tuple.Create(a0, b0, np.linalg.norm(a0 - b0));
}
return Tuple.Create(a0, b1, np.linalg.norm(a0 - b1));
}
}
else if ((float)d0 >= (float)magA || (float)d0 <= (float)d1)
{
// Is segment B after A?
if (clampA1 && clampB0)
{
if ((float)np.absolute(d0) < (float)np.absolute(d1))
{
return Tuple.Create(a1, b0, np.linalg.norm(a1 - b0));
}
return Tuple.Create(a1, b1, np.linalg.norm(a1 - b1));
}
}
}
// Segments overlap, return distance between parallel segments;
NDarray vuoto1 = np.array(new[] { 0 });
return Tuple.Create(vuoto1, vuoto1, np.linalg.norm(d0 * _A + a0 - b0));
}
// Lines criss-cross: Calculate the projected closest points
NDarray t = b0 - a0;
var ArrFordetA = np.array(new float[,] { { (float)t[0], (float)t[1], (float)t[2] },
{ (float)_B[0], (float)_B[1], (float)_B[2] },
{ (float)cross[0], (float)cross[1], (float)cross[2] } });
NDarray detA = np.linalg.det(ArrFordetA);
var ArrFordetB = np.array(new float[,] { { (float)t[0], (float)t[1], (float)t[2] },
{ (float)_A[0], (float)_A[1], (float)_A[2] },
{ (float)cross[0], (float)cross[1], (float)cross[2] } });
NDarray detB = np.linalg.det(ArrFordetB);
var t0 = detA / denom;
var t1 = detB / denom;
var pA = a0 + _A * t0; // Projected closest point on segment A
var pB = b0 + _B * t1; // Projected closest point on segment B
// Clamp projections
if (clampA0 || clampA1 || clampB0 || clampB1)
{
if (clampA0 && (float)t0 < 0)
{
pA = a0;
}
else if (clampA1 && (float)t0 > (float)magA)
{
pA = a1;
}
if (clampB0 && (float)t1 < 0)
{
pB = b0;
}
else if (clampB1 && (float)t1 > (float)magB)
{
pB = b1;
}
// Clamp projection A
if (clampA0 && (float)t0 < 0 || clampA1 && (float)t0 > (float)magA)
{
NDarray dot = np.dot(_B, pA - b0);
if ( clampB0 && (float)dot < 0)
{
dot = (NDarray)0;
}
else if (clampB1 && (float)dot > (float)magB)
{
dot = magB;
}
pB = b0 + _B * dot;
}
// Clamp projection B
if ( clampB0 && (float)t1 < 0 || clampB1 && (float)t1 > (float)magB)
{
NDarray dot = np.dot(_A, pB - a0);
if (clampA0 && (float)dot < 0)
{
dot = (NDarray)0;
}
else if (clampA1 && (float)dot > (float)magA)
{
dot = magA;
}
pA = a0 + _A * dot;
}
}
return Tuple.Create(pA, pB, np.linalg.norm(pA - pB));
}
to call this function use:
private void button1_Click(object sender, EventArgs e)
{
NDarray a1 = np.array(new[] { 13.43, 21.77, 46.81 });
NDarray a0 = np.array(new[] { 27.83, 31.74, -26.60 });
NDarray b0 = np.array(new[] { 77.54, 7.53, 6.22 });
NDarray b1 = np.array(new[] { 26.99, 12.39, 11.18 });
Debug.WriteLine("----------------- True: -----------------");
Debug.WriteLine(closestDistanceBetweenLines(a0, a1, b0, b1, true));
Debug.WriteLine("---------------------------------------------");
Debug.WriteLine("");
Debug.WriteLine("----------------- False: -----------------");
Debug.WriteLine(closestDistanceBetweenLines(a0, a1, b0, b1, false));
Debug.WriteLine("---------------------------------------------");
Tuple<NDarray, NDarray, NDarray> RisultatoTrue = closestDistanceBetweenLines(a0, a1, b0, b1, true);
var Pa = np.array(RisultatoTrue.Item1);
Debug.WriteLine("Start point X:" + Pa[0].ToString());
Debug.WriteLine("Start point Y:" + Pa[1].ToString());
Debug.WriteLine("Start point Z:" + Pa[2].ToString());
var Pb = np.array(RisultatoTrue.Item2);
Debug.WriteLine("End point X:" + Pb[0].ToString());
Debug.WriteLine("End point Y:" + Pb[1].ToString());
Debug.WriteLine("End point Z:" + Pb[2].ToString());
var dist = np.array(RisultatoTrue.Item3);
Debug.WriteLine("Distance:" + dist.ToString());
}
Related
I am find out the Image Coutours by Halcon,then I will determine turning Points
in Contours (x,y).
The following figures
Contour Image
I reference the calculate turning points / pivot points in trajectory (path)
Use Ramer-Douglas-Peucker(RDP) algorithm and the Result
RDP Result Image
I Want to get the Result follwing figures
I Want Result Image
public class Douglas
{
public List<Point> DouglasThinningMachine(List<Point> spList,double threshold=10)
{
if (spList == null)
{
throw new ArgumentNullException(nameof(spList));
}
int max = spList.Count;
int location = 0;
Stack<int> A = new Stack<int>();
Stack<int> B = new Stack<int>();
A.Push(0);
B.Push(max - 1);
do
{
var d = FindMostDistance(spList, A.Peek(), B.Peek(), ref location);
if (d > threshold)
{
B.Push(location);
}
else
{
A.Push(location);
B.Pop();
}
} while (B.Count > 0);
List<int> listOfIndex = A.ToList();
listOfIndex.Sort();
List<Point> result = new List<Point>();
foreach (int index in listOfIndex)
{
result.Add(spList[index]);
}
return result;
}
private double FindMostDistance(List<Point> seriesPoints, int start, int end, ref int location)
{
if (end - start <= 1)
{
location = end;
return 0;
}
double result = 0;
Point startPoint = seriesPoints[start];
Point endPoint = seriesPoints[end];
for (int i = start + 1; i < end; i++)
{
var d = GetDistanceToLine(startPoint, endPoint, seriesPoints[i]);
if (d > result)
{
result = d;
location = i;
}
}
return result;
}
public double GetDistanceToLine(double p1x, double p1y, double p2x, double p2y, double refpx, double refpy)
=> Math.Abs(((p2y - p1y) * (refpx - p1x)) - ((refpy - p1y) * (p2x - p1x))) / Math.Pow(((p2y - p1y) * (p2y - p1y)) + ((p2x - p1x) * (p2x - p1x)), 0.5);
public double GetDistanceToLine(Point point1, Point point2, Point refPoint)
{
return GetDistanceToLine(point1.X, point1.Y, point2.X, point2.Y, refPoint.X, refPoint.Y);
}
}
Main Code:
List<Point> PP = new List<Point>();
for (int i = 0; i < XX.Count; i++)
{
PP.Add(new Point() { X = Convert.ToInt32(XX[i]), Y = Convert.ToInt32(yy[i]) });
}
Douglas dg = new Douglas();
var ggg = dg.DouglasThinningMachine(PP,10);
for (int i = 0; i < XX.Count; i++)
{
chart1.Series[0].Points.AddXY(XX[i], yy[i]);
}
foreach( var g in ggg)
{
chart1.Series[1].Points.AddXY(g.X,g.Y);
}
for (int i = 0; i < XX.Count; i++)
{
chart1.Series[2].Points.AddXY(XX[i], result[i]);
}
Contour Data Link
// Read *.csv file in Halcon:
open_file (Path, 'input', FileHandle)
fread_line (FileHandle, OutLine, IsEOF)
tuple_split (OutLine, ',', Substrings)
tuple_str_first_n (Substrings[|Substrings|-1], strlen(Substrings[|Substrings|-1])-2, Substring)
Substrings[|Substrings|-1] := Substring
tuple_number (Substrings, Result)
close_file (FileHandle)
// Generate contours and split them
gen_contour_polygon_xld (Contour, -Y, X)
gen_polygons_xld (Contour, Polygons, 'ramer', 5)
split_contours_xld (Polygons, Contours, 'polygon', 3, 5)
You can adjust the parameters in gen_polygons_xld and split_contours_xld to get the connections you'd like.
In my Matrix class I was able to write my own code for matrices up to 3X3
class Matrix
{
public float Determinant()
{
if (!isSquare())
return 0;
else if (_rows == 1)
return this[0, 0];
else if (_rows == 2)
{
/* |a b|
|c d|*/
float a = this[0, 0];
float b = this[0, 1];
float c = this[1, 0];
float d = this[1, 1];
return (a * d) - (b * c);
}
else
{
float sum = 0;
int i = 0;
for (int j = 0; j < _cols; j++)
{
//finding cofactor
float a = (float)Math.Pow(-1, i + j);
float b = (j % 2 == 0) ? - a * this[i,j] : a * this[i,j];
Matrix m = subMatrix(i, j);
//getting determinant by recursion
float d = m.Determinant();
sum += b * d;
}
return sum;
}
}
}
This code stops working for matrices bigger than 3X3. I've read some similar posted by other people but those don't really help me. I don't need spoon-fed code, just some explanation or maybe an article which describes what I need to do.
I draw a bunch of line graphs using LiveCharts and WPF, where the contents and the number of line charts are determined at run time. So I don't know in advance how many LineSeries will be there, and what their values will be. However, I know the good range for each LineSeries. For example, one series, let's call it S1 has a good range of 2+/-1. So anything between 1 and 3 are considered to be good. Similarly there can be another, say S2 where range is 30+/-2, so anything between 28 and 32 is good.
I would like to draw the line graph so that sections that are within range are drawn as a solid line, but if a section is outside the range, it would be a dotted/dash line. Since I have multiple LineSeries in one, I have plotted each in its own Y-axis. My XAML and code looks like this:
<Grid>
<lvc:CartesianChart Name="MyChart" Margin="4"
Series="{Binding SeriesCollection}"/>
</Grid>
Code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public SeriesCollection SeriesCollection { get; set; }
public MainWindow()
{
InitializeComponent();
PlotGraph();
}
private void PlotGraph()
{
SeriesCollection = new SeriesCollection();
var lineSeries1 = new LineSeries
{
Title = "S1",
Values = new ChartValues<double>() { 2.3, 2.0, 3.1, 1.3, 0.5, 3.8, 7.3, 2.4, 1.2, 0.1 },
DataLabels = true,
Stroke = Brushes.Green,
Fill = Brushes.Transparent,
ScalesYAt = 0
};
var lineSeries2 = new LineSeries
{
Title = "S2",
Values = new ChartValues<double>() { 32.5, 34.5, 29.5, 26.0, 25.8, 30.5, 32.1, 36.5, 32.4, 24.5 },
DataLabels = true,
Stroke = Brushes.HotPink,
Fill = Brushes.Transparent,
ScalesYAt = 1
};
SeriesCollection.Add(lineSeries1);
SeriesCollection.Add(lineSeries2);
MyChart.AxisY.Add(new Axis());
MyChart.AxisY.Add(new Axis());
DataContext = this;
}
}
I found an example here that the PointState is colored based on values, but it doesn't work for me because I draw multiple series in one. Also, my graph has thousands of points so I have disabled PointGeometry since if I enable them they will be very hard to see anyway.
Is what I want possible at all?
i have found a solution, in fact 2: either you customize livechart to your problem or you recalculate the differents points like i do below:
its just a way, an idea to answer to your question, all things are possible, but need some line of codes....
the plotgraph method
private void PlotGraph()
{
var points = new List<Point>() { new Point(0, 2.3), new Point(1, 2.0),
new Point(2, 3.1), new Point(3, 1.3),
new Point(4, 0.5), new Point(5, 3.8),
new Point(6, 7.3), new Point(7, 2.4),
new Point(8, 1.2), new Point(9, 0.1)};
var range1 = new double[] { 1d, 3d };
var otherpoints = CurvesMath.GetInterpolatedCubicSplinedCurve(points);
var pointscurve = otherpoints.Select(p => p.Y).ToArray();
SeriesCollection = new SeriesCollection();
var lineSeries1 = new LineSeries
{
Title = "S1",
Values = new ChartValues<double>(pointscurve),
DataLabels = false,
Stroke = Brushes.Transparent,
Fill = Brushes.Transparent,
ScalesYAt = 0,
PointGeometrySize = 2,
Configuration = Mappers.Xy<double>()
.X((value, index) => index)
.Y((value, index) => value)
.Stroke((value, index) => value <= range1[0] || value >= range1[1] ? Brushes.Red : Brushes.Blue)
.Fill((value, index) => value <= range1[0] || value >= range1[1] ? Brushes.Red : Brushes.Blue)
};
points = new List<Point>() { new Point(0, 32.5), new Point(1, 34.5),
new Point(2, 29.5), new Point(3, 26.0),
new Point(4, 25.8), new Point(5, 30.5),
new Point(6, 32.1), new Point(7, 36.5),
new Point(8, 32.4), new Point(9, 24.5)};
var range2 = new double[] { 28d, 32d };
otherpoints = CurvesMath.GetInterpolatedCubicSplinedCurve(points);
pointscurve = otherpoints.Select(p => p.Y).ToArray();
var lineSeries2 = new LineSeries
{
Title = "S2",
Values = new ChartValues<double>(pointscurve),
DataLabels = false,
Stroke = Brushes.Transparent,
Fill = Brushes.Transparent,
ScalesYAt = 1,
PointGeometrySize = 2,
Configuration = Mappers.Xy<double>()
.X((value, index) => index)
.Y((value, index) => value)
.Stroke((value, index) => value <= range2[0] || value >= range2[1] ? Brushes.Red : Brushes.Green)
.Fill((value, index) => value <= range2[0] || value >= range2[1] ? Brushes.Red : Brushes.Green)
};
SeriesCollection.Add(lineSeries1);
SeriesCollection.Add(lineSeries2);
MyChart.AxisY.Add(new Axis());
MyChart.AxisY.Add(new Axis());
DataContext = this;
}
the xaml file:
<Grid>
<lvc:CartesianChart Name="MyChart" Margin="4"
Series="{Binding SeriesCollection}" >
</lvc:CartesianChart>
</Grid>
the interpolation cubic spline (or bezier)
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace WpfApp2
{
public static class CurvesMath
{
private const int precision = 80;
public static List<Point> GetInterpolatedCubicSplinedCurve(IList<Point> points)
{
var output = new List<Point>();
int np = points.Count; // number of points
double[] yCoords = new double[np]; // Newton form coefficients
double[] xCoords = new double[np]; // x-coordinates of nodes
double y;
double x;
if (np > 0)
{
for (int i = 0; i < np; i++)
{
var p = points[i];
xCoords[i] = p.X;
yCoords[i] = p.Y;
}
if (np > 1)
{
double[] a = new double[np];
double x1;
double x2;
double[] h = new double[np];
for (int i = 1; i <= np - 1; i++)
{
h[i] = xCoords[i] - xCoords[i - 1];
}
if (np > 2)
{
double[] sub = new double[np - 1];
double[] diag = new double[np - 1];
double[] sup = new double[np - 1];
for (int i = 1; i <= np - 2; i++)
{
diag[i] = (h[i] + h[i + 1]) / 3;
sup[i] = h[i + 1] / 6;
sub[i] = h[i] / 6;
a[i] = (yCoords[i + 1] - yCoords[i]) / h[i + 1] - (yCoords[i] - yCoords[i - 1]) / h[i];
}
SolveTridiag(sub, diag, sup, ref a, np - 2);
}
output.Add(points.First());
for (int i = 1; i <= np - 1; i++)
{
// loop over intervals between nodes
for (int j = 1; j <= precision; j++)
{
x1 = (h[i] * j) / precision;
x2 = h[i] - x1;
y = ((-a[i - 1] / 6 * (x2 + h[i]) * x1 + yCoords[i - 1]) * x2 +
(-a[i] / 6 * (x1 + h[i]) * x2 + yCoords[i]) * x1) / h[i];
x = xCoords[i - 1] + x1;
output.Add(new Point(x, y));
}
}
}
}
return output;
}
public static double SolveCubicSpline(IList<Point> knownSamples, double z)
{
int np = knownSamples.Count;
if (np > 1)
{
if (knownSamples[0].X == z) return knownSamples[0].Y;
double[] a = new double[np];
double x1;
double x2;
double y;
double[] h = new double[np];
for (int i = 1; i <= np - 1; i++)
{
h[i] = knownSamples[i].X - knownSamples[i - 1].X;
}
if (np > 2)
{
double[] sub = new double[np - 1];
double[] diag = new double[np - 1];
double[] sup = new double[np - 1];
for (int i = 1; i <= np - 2; i++)
{
diag[i] = (h[i] + h[i + 1]) / 3;
sup[i] = h[i + 1] / 6;
sub[i] = h[i] / 6;
a[i] = (knownSamples[i + 1].Y - knownSamples[i].Y) / h[i + 1] -
(knownSamples[i].Y - knownSamples[i - 1].Y) / h[i];
}
// SolveTridiag is a support function, see Marco Roello's original code
// for more information at
// http://www.codeproject.com/useritems/SplineInterpolation.asp
SolveTridiag(sub, diag, sup, ref a, np - 2);
}
int gap = 0;
double previous = double.MinValue;
// At the end of this iteration, "gap" will contain the index of the interval
// between two known values, which contains the unknown z, and "previous" will
// contain the biggest z value among the known samples, left of the unknown z
for (int i = 0; i < knownSamples.Count; i++)
{
if (knownSamples[i].X < z && knownSamples[i].X > previous)
{
previous = knownSamples[i].X;
gap = i + 1;
}
}
x1 = z - previous;
if (gap > h.Length - 1)
return z;
x2 = h[gap] - x1;
if (gap == 0)
return 0.0;
y = ((-a[gap - 1] / 6 * (x2 + h[gap]) * x1 + knownSamples[gap - 1].Y) * x2 +
(-a[gap] / 6 * (x1 + h[gap]) * x2 + knownSamples[gap].Y) * x1) / h[gap];
return y;
}
return 0;
}
private static void SolveTridiag(double[] sub, double[] diag, double[] sup, ref double[] b, int n)
{
/* solve linear system with tridiagonal n by n matrix a
using Gaussian elimination *without* pivoting
where a(i,i-1) = sub[i] for 2<=i<=n
a(i,i) = diag[i] for 1<=i<=n
a(i,i+1) = sup[i] for 1<=i<=n-1
(the values sub[1], sup[n] are ignored)
right hand side vector b[1:n] is overwritten with solution
NOTE: 1...n is used in all arrays, 0 is unused */
int i;
/* factorization and forward substitution */
for (i = 2; i <= n; i++)
{
sub[i] = sub[i] / diag[i - 1];
diag[i] = diag[i] - sub[i] * sup[i - 1];
b[i] = b[i] - sub[i] * b[i - 1];
}
b[n] = b[n] / diag[n];
for (i = n - 1; i >= 1; i--)
{
b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i];
}
}
}
}
in the result you see all bad parts with the color RED
I am having issues with the output of the the result of a math calculation. I have a basic average of an array of double and I assign the result to a Label object, using the ToString() method. When I emulate the average, the label shows the correct value of 15.96 for example, but the same average of the same array, on my Galaxy S3 shows 159.6.
Is there anyone who know what's up and what can I do to make the S3 show the correct value?
Thank you all!
EDIT: passing the result to a label and adding the label to the grid:
double result = Math.Round(NP122.DoAverage(parameters), 2);
CustomLabel label = new CustomLabel();
label.ColNo = grid.ColumnDefinitions.IndexOf(c);
label.FontSize = 25;
label.TextColor = Color.Green;
if (result.ToString() == "NaN")
label.Text = "0";
else
label.Text = result.ToString();
label.IsVisible = true;
for (int i = 0; i < numberOfRows.Length + 2; i++) {
if(i == numberOfRows.Length +1)
Grid.SetRow(label, i);
}
Grid.SetColumn(label, grid.ColumnDefinitions.IndexOf(c));
listaRez.Add(label);
foreach (CustomLabel a in listaRez)
{
if (a.ColNo == grid.ColumnDefinitions.IndexOf(c))
{
grid.Children.Add(a);
}
}
EDIT 2: Custom function for NP122.DoAverage:
public static class NP122
{
public static double Vx, sx, Xm, kn, Xkinf, Xksup;
public static double sum;
public static double sumaProvizorie;
public static double[] valoriKn = new double[25];
public static double ValoareCaracteristicaSuperioara(double[] l)
{
Vx = 0;
sx = 0;
Xm = 0;
kn = 0;
Xkinf = 0;
Xksup = 0;
sum = 0;
sumaProvizorie = 0;
valoriKn[0] = 0;
//more here
valoriKn[24] = 0.35;
if (l.Length < 2 )
{
Xksup = 0;
Xkinf = 0;
}
else
{
Xm = (l.Sum()) / (l.Length);
for (int j = 0; j < l.Length; j++)
{
sumaProvizorie = Math.Round(Math.Pow((l[j] - Xm), 2), 2);
sum += sumaProvizorie;
}
kn = valoriKn[l.Length - 1];
double elements = (1.00 / (l.Length - 1));
double putere = sum;
sx = Math.Round(Math.Sqrt(elements * putere), 4);
Vx = sx / Xm;
Xksup = Xm * (1 + kn * Vx);
Xkinf = Xm * (1 - kn * Vx);
}
return Xksup;
I am trying to write a Zhang-Suen thinning algorithm in C# following this guideline, without processing the margins.
In the function 'zhangsuen', I am reading from the image 'imgUndo' and writting to the image 'img'. The pointers dataPtrOrigin_aux inside the for cycles are used to read the 9 pixels inside a 3x3 window such that dataPtrOrigin_aux5 is the central pixel of this window, and that window will move along the whole image, moving from left to right and from top to bottom. In each pixel, if the if the statements are verified to be true, the corresponding changes are made in the image to be written by the pointer dataPtrFinal.
Note that I stored the neighbours of the current pixel inside a 8 element array. As such, they will be stored following this order:
internal static void zhangsuen(Image<Bgr, byte> img, Image<Bgr, byte> imgUndo)
{
unsafe
{
MIplImage m = img.MIplImage; //Image to be written.
MIplImage mUndo = imgUndo.MIplImage; //Image to be read.
byte* dataPtrFinal = (byte*)m.imageData.ToPointer();
byte* dataPtrUndo = (byte*)mUndo.imageData.ToPointer();
int width = img.Width; //Width of the image.
int height = img.Height; //Height of the image.
int nChan = m.nChannels; //3 channels (R, G, B).
int wStep = m.widthStep; //Total width of the image (including padding).
int padding = wStep - nChan * width; //Padding at the end of each line.
int x, y, i;
int[] neighbours = new int[8]; //Store the value of the surrounding neighbours in this array.
int step; //Step 1 or 2.
int[] sequence = { 1, 2, 4, 7, 6, 5, 3, 0, 1 };
int blackn = 0; //Number of black neighbours.
int numtransitions = 0; //Number of transitions from white to black in the sequence specified by the array sequence.
int changed = 1; //Just so it enters the while.
bool isblack = false;
int counter = 0;
while(changed > 0)
{
changed = 0;
if (counter % 2 == 0) //We want to read all the pixels in the image before going to the next step
step = 1;
else
step = 2;
for (y = 0; y < height; y++)
{
for (x = 0; x < width; x++)
{
byte* dataPtrOrigin_aux1 = (byte*)(dataPtrUndo + (y - 1) * m.widthStep + (x - 1) * m.nChannels);
byte* dataPtrOrigin_aux2 = (byte*)(dataPtrUndo + (y - 1) * m.widthStep + (x) * m.nChannels);
byte* dataPtrOrigin_aux3 = (byte*)(dataPtrUndo + (y - 1) * m.widthStep + (x + 1) * m.nChannels);
byte* dataPtrOrigin_aux4 = (byte*)(dataPtrUndo + (y) * m.widthStep + (x - 1) * m.nChannels);
byte* dataPtrOrigin_aux5 = (byte*)(dataPtrUndo + (y) * m.widthStep + (x) * m.nChannels);
byte* dataPtrOrigin_aux6 = (byte*)(dataPtrUndo + (y) * m.widthStep + (x + 1) * m.nChannels);
byte* dataPtrOrigin_aux7 = (byte*)(dataPtrUndo + (y + 1) * m.widthStep + (x - 1) * m.nChannels);
byte* dataPtrOrigin_aux8 = (byte*)(dataPtrUndo + (y + 1) * m.widthStep + (x) * m.nChannels);
byte* dataPtrOrigin_aux9 = (byte*)(dataPtrUndo + (y + 1) * m.widthStep + (x + 1) * m.nChannels);
if (x > 0 && y > 0 && x < width - 1 && y < height - 1)
{
if (dataPtrOrigin_aux5[0] == 0)
isblack = true;
if (isblack)
{
neighbours[0] = dataPtrOrigin_aux1[0];
neighbours[1] = dataPtrOrigin_aux2[0];
neighbours[2] = dataPtrOrigin_aux3[0];
neighbours[3] = dataPtrOrigin_aux4[0];
neighbours[4] = dataPtrOrigin_aux6[0];
neighbours[5] = dataPtrOrigin_aux7[0];
neighbours[6] = dataPtrOrigin_aux8[0];
neighbours[7] = dataPtrOrigin_aux9[0];
for(i = 0; i <= 7; i++)
{
if (neighbours[i] == 0)
blackn++;
if (neighbours[sequence[i]] - neighbours[sequence[i + 1]] == 255) //número de transições de branco para preto, seguindo a ordem do vector sequence
numtransitions++;
}
if ((blackn >= 2 && blackn <= 6) && numtransitions == 1)
{
if (step == 1 && (neighbours[1] == 255 || neighbours[4] == 255 || neighbours[6] == 255) && (neighbours[4] == 255 || neighbours[6] == 255 || neighbours[3] == 255))
{
dataPtrFinal[0] = 255;
dataPtrFinal[1] = 255;
dataPtrFinal[2] = 255;
changed++;
}
if (step == 2 && (neighbours[1] == 255 || neighbours[4] == 255 || neighbours[3] == 255) && (neighbours[1] == 255 || neighbours[6] == 255 || neighbours[3] == 255))
{
dataPtrFinal[0] = 255;
dataPtrFinal[1] = 255;
dataPtrFinal[2] = 255;
changed++;
}
}
}
}
dataPtrFinal += nChan;
isblack = false;
blackn = 0;
numtransitions = 0;
}
dataPtrFinal += padding;
}
dataPtrUndo = (byte*)m.imageData.ToPointer(); //Change the image to be read to the one that has just been written.
counter++;
}
}
}
As I end reading the first image and writing the changes to the image 'img' (As soon as the cycle for (y = 0; y < height; y++) ends I want the image I have just written to be the one I will read in the next cycle so that further thinning is made. I tried to accomplish this with the line
dataPtrUndo = (byte*)m.imageData.ToPointer();
Although at some value of counter that is greater than 0 (depends on the image that is read) I get an error that says that protected memory has been tried to be written which indicates I have tried to write outside of the image limits, but I don't understand why. Is it the last attribution to dataPtrUndo that I am doing erroneously?
Here is my C# implementation of Zhang-Suen thinning algorithm
public static bool[][] ZhangSuenThinning(bool[][] s)
{
bool[][] temp = s;
bool even = true;
for (int a = 1; a < s.Length-1; a++)
{
for (int b = 1; b < s[0].Length-1; b++)
{
if (SuenThinningAlg(a, b, temp, even))
{
temp[a][b] = false;
}
even = !even;
}
}
return temp;
}
static bool SuenThinningAlg(int x, int y, bool[][] s, bool even)
{
bool p2 = s[x][y - 1];
bool p3 = s[x + 1][y - 1];
bool p4 = s[x + 1][y];
bool p5 = s[x + 1][y + 1];
bool p6 = s[x][y + 1];
bool p7 = s[x - 1][y + 1];
bool p8 = s[x - 1][y];
bool p9 = s[x - 1][y - 1];
int bp1 = NumberOfNonZeroNeighbors(x, y, s);
if (bp1 >= 2 && bp1 <= 6)//2nd condition
{
if (NumberOfZeroToOneTransitionFromP9(x, y, s) == 1)
{
if (even)
{
if (!((p2 && p4) && p8))
{
if (!((p2 && p6) && p8))
{
return true;
}
}
}
else
{
if (!((p2 && p4) && p6))
{
if (!((p4 && p6) && p8))
{
return true;
}
}
}
}
}
return false;
}
static int NumberOfZeroToOneTransitionFromP9(int x, int y, bool[][]s)
{
bool p2 = s[x][y - 1];
bool p3 = s[x + 1][y - 1];
bool p4 = s[x + 1][y];
bool p5 = s[x + 1][y + 1];
bool p6 = s[x][y + 1];
bool p7 = s[x - 1][y + 1];
bool p8 = s[x - 1][y];
bool p9 = s[x - 1][y - 1];
int A = Convert.ToInt32((p2 == false && p3 == true)) + Convert.ToInt32((p3 == false && p4 == true)) +
Convert.ToInt32((p4 == false && p5 == true)) + Convert.ToInt32((p5 == false && p6 == true)) +
Convert.ToInt32((p6 == false && p7 == true)) + Convert.ToInt32((p7 == false && p8 == true)) +
Convert.ToInt32((p8 == false && p9 == true)) + Convert.ToInt32((p9 == false && p2 == true));
return A;
}
static int NumberOfNonZeroNeighbors(int x, int y, bool[][]s)
{
int count = 0;
if (s[x-1][y])
count++;
if (s[x-1][y+1])
count++;
if (s[x-1][y-1])
count++;
if (s[x][y+1])
count++;
if (s[x][y-1])
count++;
if (s[x+1][y])
count++;
if (s[x+1][y+1])
count++;
if (s[x+1][y-1])
count++;
return count;
}
bwang22's answer works. Sort of. But with two issues: It doesn't do the iterations until no more changes happen. And it does a shallow copy of the Array.. The two issues cooperate so to speak, cancelling each other out, resulting in an thinning, but not the best looking one.
Here is the corrected code, which gives a nicer looking result:
First two methods to convert from Image to bool[][] and back; the functions are not optimzed for speed; if you need that go for lockbits/unsafe..:
public static bool[][] Image2Bool(Image img)
{
Bitmap bmp = new Bitmap(img);
bool[][] s = new bool[bmp.Height][];
for (int y = 0; y < bmp.Height; y++ )
{
s[y] = new bool[bmp.Width];
for (int x = 0; x < bmp.Width; x++)
s[y][x] = bmp.GetPixel(x, y).GetBrightness() < 0.3;
}
return s;
}
public static Image Bool2Image(bool[][] s)
{
Bitmap bmp = new Bitmap(s[0].Length, s.Length);
using (Graphics g = Graphics.FromImage(bmp)) g.Clear(Color.White);
for (int y = 0; y < bmp.Height; y++)
for (int x = 0; x < bmp.Width; x++)
if (s[y][x]) bmp.SetPixel(x, y, Color.Black);
return (Bitmap)bmp;
}
Now the corrected thinning code, much of it more or less unchanged from bwang22's answer:
public static bool[][] ZhangSuenThinning(bool[][] s)
{
bool[][] temp = ArrayClone(s); // make a deep copy to start..
int count = 0;
do // the missing iteration
{
count = step(1, temp, s);
temp = ArrayClone(s); // ..and on each..
count += step(2, temp, s);
temp = ArrayClone(s); // ..call!
}
while (count > 0);
return s;
}
static int step(int stepNo, bool[][] temp, bool[][] s)
{
int count = 0;
for (int a = 1; a < temp.Length - 1; a++)
{
for (int b = 1; b < temp[0].Length - 1; b++)
{
if (SuenThinningAlg(a, b, temp, stepNo == 2))
{
// still changes happening?
if (s[a][b]) count++;
s[a][b] = false;
}
}
}
return count;
}
static bool SuenThinningAlg(int x, int y, bool[][] s, bool even)
{
bool p2 = s[x][y - 1];
bool p3 = s[x + 1][y - 1];
bool p4 = s[x + 1][y];
bool p5 = s[x + 1][y + 1];
bool p6 = s[x][y + 1];
bool p7 = s[x - 1][y + 1];
bool p8 = s[x - 1][y];
bool p9 = s[x - 1][y - 1];
int bp1 = NumberOfNonZeroNeighbors(x, y, s);
if (bp1 >= 2 && bp1 <= 6) //2nd condition
{
if (NumberOfZeroToOneTransitionFromP9(x, y, s) == 1)
{
if (even)
{
if (!((p2 && p4) && p8))
{
if (!((p2 && p6) && p8))
{
return true;
}
}
}
else
{
if (!((p2 && p4) && p6))
{
if (!((p4 && p6) && p8))
{
return true;
}
}
}
}
}
return false;
}
static int NumberOfZeroToOneTransitionFromP9(int x, int y, bool[][] s)
{
bool p2 = s[x][y - 1];
bool p3 = s[x + 1][y - 1];
bool p4 = s[x + 1][y];
bool p5 = s[x + 1][y + 1];
bool p6 = s[x][y + 1];
bool p7 = s[x - 1][y + 1];
bool p8 = s[x - 1][y];
bool p9 = s[x - 1][y - 1];
int A = Convert.ToInt32((!p2 && p3 )) + Convert.ToInt32((!p3 && p4 )) +
Convert.ToInt32((!p4 && p5 )) + Convert.ToInt32((!p5 && p6 )) +
Convert.ToInt32((!p6 && p7 )) + Convert.ToInt32((!p7 && p8 )) +
Convert.ToInt32((!p8 && p9 )) + Convert.ToInt32((!p9 && p2 ));
return A;
}
static int NumberOfNonZeroNeighbors(int x, int y, bool[][] s)
{
int count = 0;
if (s[x - 1][y]) count++;
if (s[x - 1][y + 1]) count++;
if (s[x - 1][y - 1]) count++;
if (s[x][y + 1]) count++;
if (s[x][y - 1]) count++;
if (s[x + 1][y]) count++;
if (s[x + 1][y + 1]) count++;
if (s[x + 1][y - 1]) count++;
return count;
}
I have kept the original even flag, but call it by comparing a step number. And I have saved a few characters by using the bools directly..
Finally a function to get a deep copy of the nested 2d array:
public static T[][] ArrayClone<T>(T [][] A)
{ return A.Select(a => a.ToArray()).ToArray(); }
This is how to call it, using two PictureBoxes:
pictureBox1.Image = Image.FromFile("D:\\RCdemo.png");
bool[][] t = Image2Bool(pictureBox1.Image);
t = ZhangSuenThinning(t);
pictureBox2.Image = Bool2Image(t);
I append a test image.
bwang22's answer is very slow. Try this instead:
public readonly struct ConnectivityData
{
public readonly int[] N;
public readonly int NumNeighbors;
public readonly int NumChanges;
public ConnectivityData(in int[] n, in int numNeighbors, in int numChanges)
{
N = n;
NumNeighbors = numNeighbors;
NumChanges = numChanges;
}
}
public static void ZhangSuen(in HashSet<Pixel> pixels)
{
while (true)
{
// Pass #1:
List<Pixel> mark1 = new List<Pixel>();
foreach (Pixel p in pixels)
{
ConnectivityData conn = ComputeConnectivity(p, pixels);
if (conn.NumNeighbors > 1 &&
conn.NumNeighbors < 7 &&
conn.NumChanges == 1 &&
conn.N[0] * conn.N[2] * conn.N[4] == 0 &&
conn.N[2] * conn.N[4] * conn.N[6] == 0)
{
mark1.Add(p);
}
}
//delete all marked:
foreach (Pixel p in mark1)
{
pixels.Remove(p);
}
// PASS #2:
List<Pixel> mark2 = new List<Pixel>();
foreach (Pixel p in pixels)
{
ConnectivityData conn = ComputeConnectivity(p, pixels);
if (conn.NumNeighbors > 1 &&
conn.NumNeighbors < 7 &&
conn.NumChanges == 1 &&
conn.N[0] * conn.N[2] * conn.N[6] == 0 &&
conn.N[0] * conn.N[4] * conn.N[6] == 0)
{
mark2.Add(p);
}
}
//delete all marked:
foreach (Pixel p in mark2)
{
pixels.Remove(p);
}
if (mark1.Count == 0 && mark2.Count == 0)
{
break;
}
}
}
private static ConnectivityData ComputeConnectivity(
in Pixel p,
in HashSet<Pixel> pixels)
{
// calculate #neighbors and number of changes:
int[] n = new int[8];
if (pixels.Contains(new Pixel(p.X, p.Y - 1)))
{
n[0] = 1;
}
if (pixels.Contains(new Pixel(p.X + 1, p.Y - 1)))
{
n[1] = 1;
}
if (pixels.Contains(new Pixel(p.X + 1, p.Y)))
{
n[2] = 1;
}
if (pixels.Contains(new Pixel(p.X + 1, p.Y + 1)))
{
n[3] = 1;
}
if (pixels.Contains(new Pixel(p.X, p.Y + 1)))
{
n[4] = 1;
}
if (pixels.Contains(new Pixel(p.X - 1, p.Y + 1)))
{
n[5] = 1;
}
if (pixels.Contains(new Pixel(p.X - 1, p.Y)))
{
n[6] = 1;
}
if (pixels.Contains(new Pixel(p.X - 1, p.Y - 1)))
{
n[7] = 1;
}
return new ConnectivityData(
n,
n[0] + n[1] + n[2] + n[3] + n[4] + n[5] + n[6] + n[7],
ComputeNumberOfChanges(n));
}
private static int ComputeNumberOfChanges(in int[] n)
{
int numberOfChanges = 0;
// Iterate over each location and see if it is has changed from 0 to 1:
int current = n[0];
for (int i = 1; i < 8; i++)
{
if (n[i] == 1 && current == 0)
{
numberOfChanges++;
}
current = n[i];
}
// Also consider the change over the discontinuity between n[7] and n[0]:
if (n[0] == 1 && n[7] == 0)
{
numberOfChanges++;
}
return numberOfChanges;
}
To use:
From your Bitmap etc, create a hash set of type Pixel, (which contains all the black pixels you want to thin) eg:
public class Pixel
{
public int X;
public int Y;
public Pixel(in int x, in int y)
{
X = x;
Y = y;
}
public override bool Equals(object pixel)
{
Pixel b = pixel as Pixel;
return X == b.X && Y == b.Y;
}
public override int GetHashCode()
{
//return (a.X << 2) ^ a.Y; // this is also commonly used as a pixel hash code
return X * 100000 + Y; // a bit hacky [will fail if bitmap width is > 100000]
}
}
...then call ZhangSuen(pixels). This will delete the appropriate pixels from the set.
Note that this method does not work perfectly on all images. It makes parts of some images disappear. Specifically, I am having problems with downward-right pointing diagonal lines of thickness around 11 pixels wide.
I am currently working on a way to improve this, but it performs better than the similar Staniford algorithm on most files I have tested it with (CAD files).