Geographic coordinates converter - c#

I wanted a class that can convert one system to another.
I've found a source code in python and tried to port it into C#.
This is the python source. From here
import math
class GlobalMercator(object):
def __init__(self, tileSize=256):
"Initialize the TMS Global Mercator pyramid"
self.tileSize = tileSize
self.initialResolution = 2 * math.pi * 6378137 / self.tileSize
# 156543.03392804062 for tileSize 256 pixels
self.originShift = 2 * math.pi * 6378137 / 2.0
# 20037508.342789244
def LatLonToMeters(self, lat, lon ):
"Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913"
mx = lon * self.originShift / 180.0
my = math.log( math.tan((90 + lat) * math.pi / 360.0 )) / (math.pi / 180.0)
my = my * self.originShift / 180.0
return mx, my
def MetersToLatLon(self, mx, my ):
"Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum"
lon = (mx / self.originShift) * 180.0
lat = (my / self.originShift) * 180.0
lat = 180 / math.pi * (2 * math.atan( math.exp( lat * math.pi / 180.0)) - math.pi / 2.0)
return lat, lon
def PixelsToMeters(self, px, py, zoom):
"Converts pixel coordinates in given zoom level of pyramid to EPSG:900913"
res = self.Resolution( zoom )
mx = px * res - self.originShift
my = py * res - self.originShift
return mx, my
def MetersToPixels(self, mx, my, zoom):
"Converts EPSG:900913 to pyramid pixel coordinates in given zoom level"
res = self.Resolution( zoom )
px = (mx + self.originShift) / res
py = (my + self.originShift) / res
return px, py
def PixelsToTile(self, px, py):
"Returns a tile covering region in given pixel coordinates"
tx = int( math.ceil( px / float(self.tileSize) ) - 1 )
ty = int( math.ceil( py / float(self.tileSize) ) - 1 )
return tx, ty
def PixelsToRaster(self, px, py, zoom):
"Move the origin of pixel coordinates to top-left corner"
mapSize = self.tileSize << zoom
return px, mapSize - py
def MetersToTile(self, mx, my, zoom):
"Returns tile for given mercator coordinates"
px, py = self.MetersToPixels( mx, my, zoom)
return self.PixelsToTile( px, py)
def TileBounds(self, tx, ty, zoom):
"Returns bounds of the given tile in EPSG:900913 coordinates"
minx, miny = self.PixelsToMeters( tx*self.tileSize, ty*self.tileSize, zoom )
maxx, maxy = self.PixelsToMeters( (tx+1)*self.tileSize, (ty+1)*self.tileSize, zoom )
return ( minx, miny, maxx, maxy )
def TileLatLonBounds(self, tx, ty, zoom ):
"Returns bounds of the given tile in latutude/longitude using WGS84 datum"
bounds = self.TileBounds( tx, ty, zoom)
minLat, minLon = self.MetersToLatLon(bounds[0], bounds[1])
maxLat, maxLon = self.MetersToLatLon(bounds[2], bounds[3])
return ( minLat, minLon, maxLat, maxLon )
def Resolution(self, zoom ):
"Resolution (meters/pixel) for given zoom level (measured at Equator)"
# return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom)
return self.initialResolution / (2**zoom)
def ZoomForPixelSize(self, pixelSize ):
"Maximal scaledown zoom of the pyramid closest to the pixelSize."
for i in range(30):
if pixelSize > self.Resolution(i):
return i-1 if i!=0 else 0 # We don't want to scale up
def GoogleTile(self, tx, ty, zoom):
"Converts TMS tile coordinates to Google Tile coordinates"
# coordinate origin is moved from bottom-left to top-left corner of the extent
return tx, (2**zoom - 1) - ty
def QuadTree(self, tx, ty, zoom ):
"Converts TMS tile coordinates to Microsoft QuadTree"
quadKey = ""
ty = (2**zoom - 1) - ty
for i in range(zoom, 0, -1):
digit = 0
mask = 1 << (i-1)
if (tx & mask) != 0:
digit += 1
if (ty & mask) != 0:
digit += 2
quadKey += str(digit)
return quadKey
Here is my C# port.
public class GlobalMercator {
private Int32 TileSize;
private Double InitialResolution;
private Double OriginShift;
private const Int32 EarthRadius = 6378137;
public GlobalMercator() {
TileSize = 256;
InitialResolution = 2 * Math.PI * EarthRadius / TileSize;
OriginShift = 2 * Math.PI * EarthRadius / 2;
}
public DPoint LatLonToMeters(Double lat, Double lon) {
var p = new DPoint();
p.X = lon * OriginShift / 180;
p.Y = Math.Log(Math.Tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
p.Y = p.Y * OriginShift / 180;
return p;
}
public GeoPoint MetersToLatLon(DPoint m) {
var ll = new GeoPoint();
ll.Longitude = (m.X / OriginShift) * 180;
ll.Latitude = (m.Y / OriginShift) * 180;
ll.Latitude = 180 / Math.PI * (2 * Math.Atan(Math.Exp(ll.Latitude * Math.PI / 180)) - Math.PI / 2);
return ll;
}
public DPoint PixelsToMeters(DPoint p, Int32 zoom) {
var res = Resolution(zoom);
var met = new DPoint();
met.X = p.X * res - OriginShift;
met.Y = p.Y * res - OriginShift;
return met;
}
public DPoint MetersToPixels(DPoint m, Int32 zoom) {
var res = Resolution(zoom);
var pix = new DPoint();
pix.X = (m.X + OriginShift) / res;
pix.Y = (m.Y + OriginShift) / res;
return pix;
}
public Point PixelsToTile(DPoint p) {
var t = new Point();
t.X = (Int32)Math.Ceiling(p.X / (Double)TileSize) - 1;
t.Y = (Int32)Math.Ceiling(p.Y / (Double)TileSize) - 1;
return t;
}
public Point PixelsToRaster(Point p, Int32 zoom) {
var mapSize = TileSize << zoom;
return new Point(p.X, mapSize - p.Y);
}
public Point MetersToTile(Point m, Int32 zoom) {
var p = MetersToPixels(m, zoom);
return PixelsToTile(p);
}
public Pair<DPoint> TileBounds(Point t, Int32 zoom) {
var min = PixelsToMeters(new DPoint(t.X * TileSize, t.Y * TileSize), zoom);
var max = PixelsToMeters(new DPoint((t.X + 1) * TileSize, (t.Y + 1) * TileSize), zoom);
return new Pair<DPoint>(min, max);
}
public Pair<GeoPoint> TileLatLonBounds(Point t, Int32 zoom) {
var bound = TileBounds(t, zoom);
var min = MetersToLatLon(bound.Min);
var max = MetersToLatLon(bound.Max);
return new Pair<GeoPoint>(min, max);
}
public Double Resolution(Int32 zoom) {
return InitialResolution / (2 ^ zoom);
}
public Double ZoomForPixelSize(Double pixelSize) {
for (var i = 0; i < 30; i++)
if (pixelSize > Resolution(i))
return i != 0 ? i - 1 : 0;
throw new InvalidOperationException();
}
public Point ToGoogleTile(Point t, Int32 zoom) {
return new Point(t.X, ((Int32)Math.Pow(2, zoom) - 1) - t.Y);
}
public Point ToTmsTile(Point t, Int32 zoom) {
return new Point(t.X, ((Int32)Math.Pow(2, zoom) - 1) - t.Y);
}
}
public struct Point {
public Point(Int32 x, Int32 y)
: this() {
X = x;
Y = y;
}
public Int32 X { get; set; }
public Int32 Y { get; set; }
}
public struct DPoint {
public DPoint(Double x, Double y)
: this() {
this.X = x;
this.Y = y;
}
public Double X { get; set; }
public Double Y { get; set; }
public static implicit operator DPoint(Point p) {
return new DPoint(p.X, p.Y);
}
}
public class GeoPoint {
public Double Latitude { get; set; }
public Double Longitude { get; set; }
}
public class Pair<T> {
public Pair() {}
public Pair(T min, T max) {
Min = min;
Max = max;
}
public T Min { get; set; }
public T Max { get; set; }
}
I have two questions.
Did I port the code correctly? (I intentionally omitted one method as I don't use it and added one my own)
Here I have coordinates
WGS84 datum (longitude/latitude):
-123.75 36.59788913307022
-118.125 40.97989806962013
Spherical Mercator (meters):
-13775786.985667605 4383204.9499851465
-13149614.849955441 5009377.085697312
Pixels
2560 6144 2816 6400
Google
x:10, y:24, z:6
TMS
x:10, y:39, z:6
QuadTree
023010
How should I chain the methods so that I get from Google's tile coordinates (10, 24, 6) the spherical mercator meters?
Update
Finding answer for my second question is more important for me.

There's at least one bug in your class:
public Double Resolution(Int32 zoom) {
return InitialResolution / (2 ^ zoom); // BAD CODE, USE Math.Pow, not ^
}
Where you've mistaken the binary XOR operator for the exponent operator.
I've rewritten the code, made most functions static, and added a few more relevant functions:
/// <summary>
/// Conversion routines for Google, TMS, and Microsoft Quadtree tile representations, derived from
/// http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/
/// </summary>
public class WebMercator
{
private const int TileSize = 256;
private const int EarthRadius = 6378137;
private const double InitialResolution = 2 * Math.PI * EarthRadius / TileSize;
private const double OriginShift = 2 * Math.PI * EarthRadius / 2;
//Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
public static Point LatLonToMeters(double lat, double lon)
{
var p = new Point();
p.X = lon * OriginShift / 180;
p.Y = Math.Log(Math.Tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
p.Y = p.Y * OriginShift / 180;
return p;
}
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
public static Point MetersToLatLon(Point m)
{
var ll = new Point();
ll.X = (m.X / OriginShift) * 180;
ll.Y = (m.Y / OriginShift) * 180;
ll.Y = 180 / Math.PI * (2 * Math.Atan(Math.Exp(ll.Y * Math.PI / 180)) - Math.PI / 2);
return ll;
}
//Converts pixel coordinates in given zoom level of pyramid to EPSG:900913
public static Point PixelsToMeters(Point p, int zoom)
{
var res = Resolution(zoom);
var met = new Point();
met.X = p.X * res - OriginShift;
met.Y = p.Y * res - OriginShift;
return met;
}
//Converts EPSG:900913 to pyramid pixel coordinates in given zoom level
public static Point MetersToPixels(Point m, int zoom)
{
var res = Resolution(zoom);
var pix = new Point();
pix.X = (m.X + OriginShift) / res;
pix.Y = (m.Y + OriginShift) / res;
return pix;
}
//Returns a TMS (NOT Google!) tile covering region in given pixel coordinates
public static Tile PixelsToTile(Point p)
{
var t = new Tile();
t.X = (int)Math.Ceiling(p.X / (double)TileSize) - 1;
t.Y = (int)Math.Ceiling(p.Y / (double)TileSize) - 1;
return t;
}
public static Point PixelsToRaster(Point p, int zoom)
{
var mapSize = TileSize << zoom;
return new Point(p.X, mapSize - p.Y);
}
//Returns tile for given mercator coordinates
public static Tile MetersToTile(Point m, int zoom)
{
var p = MetersToPixels(m, zoom);
return PixelsToTile(p);
}
//Returns bounds of the given tile in EPSG:900913 coordinates
public static Rect TileBounds(Tile t, int zoom)
{
var min = PixelsToMeters(new Point(t.X * TileSize, t.Y * TileSize), zoom);
var max = PixelsToMeters(new Point((t.X + 1) * TileSize, (t.Y + 1) * TileSize), zoom);
return new Rect(min, max);
}
//Returns bounds of the given tile in latutude/longitude using WGS84 datum
public static Rect TileLatLonBounds(Tile t, int zoom)
{
var bound = TileBounds(t, zoom);
var min = MetersToLatLon(new Point(bound.Left, bound.Top));
var max = MetersToLatLon(new Point(bound.Right, bound.Bottom));
return new Rect(min, max);
}
//Resolution (meters/pixel) for given zoom level (measured at Equator)
public static double Resolution(int zoom)
{
return InitialResolution / (Math.Pow(2, zoom));
}
public static double ZoomForPixelSize(double pixelSize)
{
for (var i = 0; i < 30; i++)
if (pixelSize > Resolution(i))
return i != 0 ? i - 1 : 0;
throw new InvalidOperationException();
}
// Switch to Google Tile representation from TMS
public static Tile ToGoogleTile(Tile t, int zoom)
{
return new Tile(t.X, ((int)Math.Pow(2, zoom) - 1) - t.Y);
}
// Switch to TMS Tile representation from Google
public static Tile ToTmsTile(Tile t, int zoom)
{
return new Tile(t.X, ((int)Math.Pow(2, zoom) - 1) - t.Y);
}
//Converts TMS tile coordinates to Microsoft QuadTree
public static string QuadTree(int tx, int ty, int zoom)
{
var quadtree = "";
ty = ((1 << zoom) - 1) - ty;
for (var i = zoom; i >= 1; i--)
{
var digit = 0;
var mask = 1 << (i - 1);
if ((tx & mask) != 0)
digit += 1;
if ((ty & mask) != 0)
digit += 2;
quadtree += digit;
}
return quadtree;
}
//Converts a quadtree to tile coordinates
public static Tile QuadTreeToTile(string quadtree, int zoom)
{
int tx= 0, ty = 0;
for (var i = zoom; i >= 1; i--)
{
var ch = quadtree[zoom - i];
var mask = 1 << (i - 1);
var digit = ch - '0';
if ((digit & 1) != 0)
tx += mask;
if ((digit & 2) != 0)
ty += mask;
}
ty = ((1 << zoom) - 1) - ty;
return new Tile(tx, ty);
}
//Converts a latitude and longitude to quadtree at the specified zoom level
public static string LatLonToQuadTree(Point latLon, int zoom)
{
Point m = LatLonToMeters(latLon.Y, latLon.X);
Tile t = MetersToTile(m, zoom);
return QuadTree(t.X, t.Y, zoom);
}
//Converts a quadtree location into a latitude/longitude bounding rectangle
public static Rect QuadTreeToLatLon(string quadtree)
{
int zoom = quadtree.Length;
Tile t = QuadTreeToTile(quadtree, zoom);
return TileLatLonBounds(t, zoom);
}
//Returns a list of all of the quadtree locations at a given zoom level within a latitude/longude box
public static List<string> GetQuadTreeList(int zoom, Point latLonMin, Point latLonMax)
{
if (latLonMax.Y< latLonMin.Y|| latLonMax.X< latLonMin.X)
return null;
Point mMin = LatLonToMeters(latLonMin.Y, latLonMin.X);
Tile tmin = MetersToTile(mMin, zoom);
Point mMax = LatLonToMeters(latLonMax.Y, latLonMax.X);
Tile tmax = MetersToTile(mMax, zoom);
var arr = new List<string>();
for (var ty = tmin.Y; ty <= tmax.Y; ty++)
{
for (var tx = tmin.X; tx <= tmax.X; tx++)
{
var quadtree = QuadTree(tx, ty, zoom);
arr.Add(quadtree);
}
}
return arr;
}
}
/// <summary>
/// Reference to a Tile X, Y index
/// </summary>
public class Tile
{
public Tile() { }
public Tile(int x, int y)
{
X = x;
Y = y;
}
public int X { get; set; }
public int Y { get; set; }
}
To solve your second question, use the following sequence:
int zoom = 6;
Tile googleTile = new Tile(10,24);
Tile tmsTile = GlobalMercator.ToTmsTile(googleTile, zoom);
Rect r3 = GlobalMercator.TileLatLonBounds(tmsTile, zoom);
var tl = GlobalMercator.LatLonToMeters(r3.Top, r3.Left);
var br = GlobalMercator.LatLonToMeters(r3.Bottom, r3.Right);
Debug.WriteLine("{0:0.000} {1:0.000}", tl.X, tl.Y);
Debug.WriteLine("{0:0.000} {1:0.000}", br.X, br.Y);
// -13775787.000 4383205.000
// -13149615.000 5009376.500

The best opensource solution for converting coordinates from one projection to another is Proj4 originally written in c but ported to numerous programming languages. The port to c# that I have tried and used is DotSpatial Projections found on CodePlex. It is easy to find out how to use it based on the examples. The only thing you need to know are conversion parameters for your case.

CoordinateSharp is available on NuGet. It's light weight and makes coordinate conversions really. It's not designed for mapping, but strait up conversions (and location based celestial information) if that is a factor.
Example
Coordinate c = new Coordinate(myLat,myLong);
c.UTM; //Outputs UTM string
c.UTM.Easting //UTM easting property

A couple of pointers for anyone reading that wants to use Oybek excellent code:
You need to add using System.Windows but also Add a Reference to the WindowsBase assembly, otherwise VS wont find Point and Rect.
Note that just must not use System.Drawing
And here's a new function that will convert Zoom lat/lng to a Google Tile:
public static Tile LatLonToGoogleTile(Point latLon, int zoom)
{
Point m = LatLonToMeters(latLon.Y, latLon.X);
Tile t = MetersToTile(m, zoom);
return ToGoogleTile(t, zoom);
}

Related

How to construct a graph of weights from an image

Project: intelligence scissors.
The first part of the project is to load an image (in the form of RGBPixel 2d array) then to construct a graph of weights to use it later to determine the shortest path between 2 points on the image (the 2 points will be determined by an anchor and a free point..In short i will have the source and the destination points).
I have a function that open the image and return a RGBPixel 2d array already made.Now image is loaded i want to construct the graph to do the i should use a function called CalculatePixelEnergies here is the code
public static Vector2D CalculatePixelEnergies(int x, int y, RGBPixel[,] ImageMatrix)
{
if (ImageMatrix == null) throw new Exception("image is not set!");
Vector2D gradient = CalculateGradientAtPixel(x, y, ImageMatrix);
double gradientMagnitude = Math.Sqrt(gradient.X * gradient.X + gradient.Y * gradient.Y);
double edgeAngle = Math.Atan2(gradient.Y, gradient.X);
double rotatedEdgeAngle = edgeAngle + Math.PI / 2.0;
Vector2D energy = new Vector2D();
energy.X = Math.Abs(gradientMagnitude * Math.Cos(rotatedEdgeAngle));
energy.Y = Math.Abs(gradientMagnitude * Math.Sin(rotatedEdgeAngle));
return energy;
}
This function use CalculateGradientAtPixel, Here is the code in case you want it.
private static Vector2D CalculateGradientAtPixel(int x, int y, RGBPixel[,] ImageMatrix)
{
Vector2D gradient = new Vector2D();
RGBPixel mainPixel = ImageMatrix[y, x];
double pixelGrayVal = 0.21 * mainPixel.red + 0.72 * mainPixel.green + 0.07 * mainPixel.blue;
if (y == GetHeight(ImageMatrix) - 1)
{
//boundary pixel.
for (int i = 0; i < 3; i++)
{
gradient.Y = 0;
}
}
else
{
RGBPixel downPixel = ImageMatrix[y + 1, x];
double downPixelGrayVal = 0.21 * downPixel.red + 0.72 * downPixel.green + 0.07 * downPixel.blue;
gradient.Y = pixelGrayVal - downPixelGrayVal;
}
if (x == GetWidth(ImageMatrix) - 1)
{
//boundary pixel.
gradient.X = 0;
}
else
{
RGBPixel rightPixel = ImageMatrix[y, x + 1];
double rightPixelGrayVal = 0.21 * rightPixel.red + 0.72 * rightPixel.green + 0.07 * rightPixel.blue;
gradient.X = pixelGrayVal - rightPixelGrayVal;
}
return gradient;
}
In my code of graph construction i decided to make a 2d double array to hold the weights, here what i do but it seems to be a wrong construction
public static double [,] calculateWeights(RGBPixel[,] ImageMatrix)
{
double[,] weights = new double[1000, 1000];
int height = ImageOperations.GetHeight(ImageMatrix);
int width = ImageOperations.GetWidth(ImageMatrix);
for (int y = 0; y < height - 1; y++)
{
for (int x = 0; x < width - 1; x++)
{
Vector2D e;
e = ImageOperations.CalculatePixelEnergies(x, y, ImageMatrix);
weights[y + 1, x] = 1 / e.X;
weights[y, x + 1] = 1 / e.Y;
}
}
return weights;
}
an example for an image
an other example for an image

HSV triangle in C# [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
For my assignment I need to make to make a color picker that looks like this:
I've got the wheel part but can't figure out how to draw the triangle.
using System.Drawing;
using System.Drawing.Imaging;
void Main()
{
double hue = 3.3;
double sat = 0.4;
double val = 0.9;
var wheel = new ColorPicker(400);
var img = wheel.DrawImage(hue, sat, val);
using (var g = Graphics.FromImage(img))
{
var pen = val < 0.5 ? Pens.White : Pens.Black;
var wheelPosition = wheel.GetWheelPosition(hue);
g.DrawEllipse(pen, (float)wheelPosition.X - 5, (float)wheelPosition.Y - 5, 10, 10);
var trianglePosition = wheel.GetTrianglePosition(sat, val);
g.DrawEllipse(pen, (float)trianglePosition.X - 5, (float)trianglePosition.Y - 5, 10, 10);
}
img.Dump(); // LINQPad extension method
}
public class ColorPicker
{
public int Size { get; }
public int CenterX => Size / 2;
public int CenterY => Size / 2;
public int InnerRadius => Size * 5 / 12;
public int OuterRadius => Size / 2;
public ColorPicker(int size = 400)
{
Size = size;
}
public enum Area
{
Outside,
Wheel,
Triangle
}
public struct PickResult
{
public Area Area { get; set; }
public double? Hue { get; set; }
public double? Sat { get; set; }
public double? Val { get; set; }
}
public PickResult Pick(double x, double y)
{
var distanceFromCenter = Math.Sqrt((x - CenterX) * (x - CenterX) + (y - CenterY) * (y - CenterY));
var sqrt3 = Math.Sqrt(3);
if (distanceFromCenter > OuterRadius)
{
// Outside
return new PickResult { Area = Area.Outside };
}
else if (distanceFromCenter > InnerRadius)
{
// Wheel
var angle = Math.Atan2(y - CenterY, x - CenterX) + Math.PI / 2;
if (angle < 0) angle += 2 * Math.PI;
var hue = angle;
return new PickResult { Area = Area.Wheel, Hue = hue };
}
else
{
// Inside
var x1 = (x - CenterX) * 1.0 / InnerRadius;
var y1 = (y - CenterY) * 1.0 / InnerRadius;
if (0 * x1 + 2 * y1 > 1) return new PickResult { Area = Area.Outside };
else if (sqrt3 * x1 + (-1) * y1 > 1) return new PickResult { Area = Area.Outside };
else if (-sqrt3 * x1 + (-1) * y1 > 1) return new PickResult { Area = Area.Outside };
else
{
// Triangle
var sat = (1 - 2 * y1) / (sqrt3 * x1 - y1 + 2);
var val = (sqrt3 * x1 - y1 + 2) / 3;
return new PickResult { Area = Area.Triangle, Sat = sat, Val = val };
}
}
}
public Image DrawImage(double hue = 0.0, double sat = 1.0, double val = 1.0)
{
var img = new Bitmap(Size, Size, PixelFormat.Format32bppArgb);
for (int y = 0; y < Size; y++)
{
for (int x = 0; x < Size; x++)
{
Color color;
var result = Pick(x, y);
if (result.Area == Area.Outside)
{
// Outside
color = Color.Transparent;
}
else if (result.Area == Area.Wheel)
{
// Wheel
color = HSV(result.Hue.Value, sat, val, 1);
}
else
{
// Triangle
color = HSV(hue, result.Sat.Value, result.Val.Value, 1);
}
img.SetPixel(x, y, color);
}
}
return img;
}
private Color HSV(double hue, double sat, double val, double alpha)
{
var chroma = val * sat;
var step = Math.PI / 3;
var interm = chroma * (1 - Math.Abs((hue / step) % 2.0 - 1));
var shift = val - chroma;
if (hue < 1 * step) return RGB(shift + chroma, shift + interm, shift + 0, alpha);
if (hue < 2 * step) return RGB(shift + interm, shift + chroma, shift + 0, alpha);
if (hue < 3 * step) return RGB(shift + 0, shift + chroma, shift + interm, alpha);
if (hue < 4 * step) return RGB(shift + 0, shift + interm, shift + chroma, alpha);
if (hue < 5 * step) return RGB(shift + interm, shift + 0, shift + chroma, alpha);
return RGB(shift + chroma, shift + 0, shift + interm, alpha);
}
private Color RGB(double red, double green, double blue, double alpha)
{
return Color.FromArgb(
Math.Min(255, (int)(alpha * 256)),
Math.Min(255, (int)(red * 256)),
Math.Min(255, (int)(green * 256)),
Math.Min(255, (int)(blue * 256)));
}
public PointD GetWheelPosition(double hue)
{
double middleRadius = (InnerRadius + OuterRadius) / 2;
return new PointD
{
X = CenterX + middleRadius * Math.Sin(hue),
Y = CenterY - middleRadius * Math.Cos(hue)
};
}
public PointD GetTrianglePosition(double sat, double val)
{
var sqrt3 = Math.Sqrt(3);
return new PointD
{
X = CenterX + InnerRadius * (2 * val - sat * val - 1) * sqrt3 / 2,
Y = CenterY + InnerRadius * (1 - 3 * sat * val) / 2
};
}
}
public class PointD
{
public double X { get; set; }
public double Y { get; set; }
}
Result:

Bandpass filter bank

I have implemented a bank of oriented bandpass filters described in this article.
See the last paragraph of the section named "2.1 Pre-processing".
We selected 12 not overlapping filters, to analyze 12 different directions, rotated with respect to 15° each other.
I am having the following issue,
The filter-bank was supposed to generate 12 filtered images. But, in reality, I am having only 03 outputs as seen in the following snapshot,
Source Code:
Here is the complete VS2013 solution as a zipped file.
Here is the most relevant part of the source code,
public class KassWitkinFunction
{
/*
* tx = centerX * cos
* ty = centerY * sin
*
* u* = cos . (u + tx) + sin . (v + ty)
* v* = - sin . (u + tx) + cos . (v + ty)
*
*/
//#region MyRegion
public static double tx(int centerX, double theta)
{
double costheta = Math.Cos(theta);
double txx = centerX * costheta;
return txx;
}
public static double ty(int centerY, double theta)
{
double sintheta = Math.Sin(theta);
double tyy = centerY * sintheta;
return tyy;
}
public static double uStar(double u, double v, int centerX, int centerY, double theta)
{
double txx = tx(centerX, theta);
double tyy = ty(centerY, theta);
double sintheta = Math.Sin(theta);
double costheta = Math.Cos(theta);
double cosThetaUTx = costheta * (u + txx);
double sinThetaVTy = sintheta * (v + tyy);
double returns = cosThetaUTx + sinThetaVTy;
return returns;
}
//#endregion
public static double vStar(double u, double v, int centerX, int centerY, double theta)
{
double txx = tx(centerX, theta);
double tyy = ty(centerY, theta);
double sintheta = Math.Sin(theta);
double costheta = Math.Cos(theta);
double sinThetaUTx = (-1) * sintheta * (u + txx);
double cosThetaVTy = costheta * (v + tyy);
double returns = sinThetaUTx + cosThetaVTy;
return returns;
}
public static double ApplyFilterOnOneCoord(double u, double v, double Du, double Dv, int CenterX, int CenterY, double Theta, int N)
{
double uStar = KassWitkinFunction.uStar(u, v, CenterX, CenterY, Theta);
double vStar = KassWitkinFunction.vStar(u, v, CenterX, CenterY, Theta);
double uStarDu = uStar / Du;
double vStarDv = vStar / Dv;
double sqrt = Math.Sqrt(uStarDu + vStarDv);
double _2n = 2 * N;
double pow = Math.Pow(sqrt, _2n);
double div = 1 + 0.414 * pow;
double returns = 1/div;
return returns;
}
}
public class KassWitkinKernel
{
public readonly int N = 4;
public int Width { get; set; }
public int Height { get; set; }
public double[,] Kernel { get; private set; }
public double[,] PaddedKernel { get; private set; }
public double Du { get; set; }
public double Dv { get; set; }
public int CenterX { get; set; }
public int CenterY { get; set; }
public double ThetaInRadian { get; set; }
public void SetKernel(double[,] value)
{
Kernel = value;
Width = Kernel.GetLength(0);
Height = Kernel.GetLength(1);
}
public void Pad(int newWidth, int newHeight)
{
double[,] temp = (double[,])Kernel.Clone();
PaddedKernel = ImagePadder.Pad(temp, newWidth, newHeight);
}
public Bitmap ToBitmap()
{
return ImageDataConverter.ToBitmap(Kernel);
}
public Bitmap ToBitmapPadded()
{
return ImageDataConverter.ToBitmap(PaddedKernel);
}
public Complex[,] ToComplex()
{
return ImageDataConverter.ToComplex(Kernel);
}
public Complex[,] ToComplexPadded()
{
return ImageDataConverter.ToComplex(PaddedKernel);
}
public void Compute()
{
Kernel = new double[Width, Height];
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
Kernel[i, j] = (double)KassWitkinFunction.ApplyFilterOnOneCoord(i, j,
Du,
Dv,
CenterX,
CenterY,
ThetaInRadian,
N);
//Data[i, j] = r.NextDouble();
}
}
string str = string.Empty;
}
}
public class KassWitkinBandpassFilter
{
public Bitmap Apply(Bitmap image, KassWitkinKernel kernel)
{
Complex[,] cImagePadded = ImageDataConverter.ToComplex(image);
Complex[,] cKernelPadded = kernel.ToComplexPadded();
Complex[,] convolved = Convolution.Convolve(cImagePadded, cKernelPadded);
return ImageDataConverter.ToBitmap(convolved);
}
}
public class KassWitkinFilterBank
{
private List<KassWitkinKernel> Kernels;
public int NoOfFilters { get; set; }
public double FilterAngle { get; set; }
public int WidthWithPadding { get; set; }
public int HeightWithPadding { get; set; }
public int KernelDimension { get; set; }
public KassWitkinFilterBank()
{}
public List<Bitmap> Apply(Bitmap bitmap)
{
Kernels = new List<KassWitkinKernel>();
double degrees = FilterAngle;
KassWitkinKernel kernel;
for (int i = 0; i < NoOfFilters; i++)
{
kernel = new KassWitkinKernel();
kernel.Width = KernelDimension;
kernel.Height = KernelDimension;
kernel.CenterX = (kernel.Width) / 2;
kernel.CenterY = (kernel.Height) / 2;
kernel.Du = 2;
kernel.Dv = 2;
kernel.ThetaInRadian = Tools.DegreeToRadian(degrees);
kernel.Compute();
kernel.Pad(WidthWithPadding, HeightWithPadding);
Kernels.Add(kernel);
degrees += degrees;
}
List<Bitmap> list = new List<Bitmap>();
foreach (KassWitkinKernel k in Kernels)
{
Bitmap image = (Bitmap)bitmap.Clone();
Complex[,] cImagePadded = ImageDataConverter.ToComplex(image);
Complex[,] cKernelPadded = k.ToComplexPadded();
Complex[,] convolved = Convolution.Convolve(cImagePadded, cKernelPadded);
Bitmap temp = ImageDataConverter.ToBitmap(convolved);
list.Add(temp);
}
return list;
}
}
As I pointed out earlier in comments, most of the filter outputs are blank because they contain NaNs. These are caused by
the implementation of equations (1) and (2) from
your reference article.
Getting in touch with the original authors would probably have the best chance of reproducing the original results, but at the very least you could ensure that no NaNs are produced with:
double arg = uStarDu + vStarDv;
double div = 1 + 0.414 * Math.Pow(Math.Abs(arg), N);
On the other hand, given the general form of the equation being reminescent of a Butterworth filter
(together with the mention of bandpass filtering),
and the seemingly unecessary square root followed by exponentiation (which suggest either a missed obvious simplification, or more likely in my opinion an error
in rendering of the equation), I would suggest to instead use the following equation:
where is the center of the image. This could be implemented with:
public static double uStar(double u, double v, int centerX, int centerY, double theta)
{
double sintheta = Math.Sin(theta);
double costheta = Math.Cos(theta);
return costheta * (u - centerX) + sintheta * (v - centerY);
}
public static double vStar(double u, double v, int centerX, int centerY, double theta)
{
double sintheta = Math.Sin(theta);
double costheta = Math.Cos(theta);
return (-1) * sintheta * (u - centerX) + costheta * (v - centerY);
}
public static double ApplyFilterOnOneCoord(double u, double v, double Du, double Dv, int CenterX, int CenterY, double Theta, int N)
{
double uStarDu = KassWitkinFunction.uStar(u, v, CenterX, CenterY, Theta) / Du;
double vStarDv = KassWitkinFunction.vStar(u, v, CenterX, CenterY, Theta) / Dv;
double arg = uStarDu + vStarDv;
double div = Math.Sqrt(1 + Math.Pow(arg, 2*N));;
return 1/div;
}
Now you must realize that those equations are given for the filter representation in the frequency domain, whereas your Convolution.Convolve
expect the filter kernel to be provided in the spatial domain (despite the core of the computation being done in the frequency domain).
The easiest way to apply these filters (and still get the correct padding in the spatial domain) is to:
set the frequency domain kernel size to the padded size to compute the kernel in the frequency domain
transform the frequency domain kernel to spatial domain
zero out the kernel where the padding would have been added
transform the kernel back to frequency domain
This can be achieved with the following modified version of KassWitkinKernel.Pad:
private Complex[,] cPaddedKernel;
public void Pad(int unpaddedWidth, int unpaddedHeight, int newWidth, int newHeight)
{
Complex[,] unpaddedKernelFrequencyDomain = ImageDataConverter.ToComplex((double[,])Kernel.Clone());
FourierTransform ftInverse = new FourierTransform();
ftInverse.InverseFFT(FourierShifter.RemoveFFTShift(unpaddedKernelFrequencyDomain));
Complex[,] cKernel = FourierShifter.FFTShift(ftInverse.GrayscaleImageComplex);
int startPointX = (int)Math.Ceiling((double)(newWidth - unpaddedWidth) / (double)2) - 1;
int startPointY = (int)Math.Ceiling((double)(newHeight - unpaddedHeight) / (double)2) - 1;
for (int j = 0; j < newHeight; j++)
{
for (int i=0; i<startPointX; i++)
{
cKernel[i, j] = 0;
}
for (int i = startPointX + unpaddedWidth; i < newWidth; i++)
{
cKernel[i, j] = 0;
}
}
for (int i = startPointX; i < startPointX + unpaddedWidth; i++)
{
for (int j = 0; j < startPointY; j++)
{
cKernel[i, j] = 0;
}
for (int j = startPointY + unpaddedHeight; j < newHeight; j++)
{
cKernel[i, j] = 0;
}
}
FourierTransform ftForward = new FourierTransform(cKernel); ftForward.ForwardFFT();
cPaddedKernel = ftForward.FourierImageComplex;
}
public Complex[,] ToComplexPadded()
{
return cPaddedKernel;
}
Latter when computing the convolution you would skip the FFT for the kernel in the convolution.
Note that you could similarly avoid recomputing the image's FFT for every filter in the filter bank.
If you precompute the FFT of the image, the remaining computations required to get the convolution
is reduced to the frequency domain multiplication and the final inverse transform:
public partial class Convolution
{
public static Complex[,] ConvolveInFrequencyDomain(Complex[,] fftImage, Complex[,] fftKernel)
{
Complex[,] convolve = null;
int imageWidth = fftImage.GetLength(0);
int imageHeight = fftImage.GetLength(1);
int maskWidth = fftKernel.GetLength(0);
int maskHeight = fftKernel.GetLength(1);
if (imageWidth == maskWidth && imageHeight == maskHeight)
{
Complex[,] fftConvolved = new Complex[imageWidth, imageHeight];
for (int j = 0; j < imageHeight; j++)
{
for (int i = 0; i < imageWidth; i++)
{
fftConvolved[i, j] = fftImage[i, j] * fftKernel[i, j];
}
}
FourierTransform ftForConv = new FourierTransform();
ftForConv.InverseFFT(fftConvolved);
convolve = FourierShifter.FFTShift(ftForConv.GrayscaleImageComplex);
Rescale(convolve);
}
else
{
throw new Exception("padding needed");
}
return convolve;
}
}
Which would be used in KassWitkinFilterBank.Apply as follow:
Bitmap image = (Bitmap)bitmap.Clone();
Complex[,] cImagePadded = ImageDataConverter.ToComplex(image);
FourierTransform ftForImage = new FourierTransform(cImagePadded); ftForImage.ForwardFFT();
Complex[,] fftImage = ftForImage.FourierImageComplex;
foreach (KassWitkinKernel k in Kernels)
{
Complex[,] cKernelPadded = k.ToComplexPadded();
Complex[,] convolved = Convolution.ConvolveInFrequencyDomain(fftImage, cKernelPadded);
Bitmap temp = ImageDataConverter.ToBitmap(convolved);
list.Add(temp);
}
So that should get you past the bump indicated in your question.
Of course if the intention is to reproduce the results of the paper you still have other hurdles to get over.
The first one being to actually use a sharpened image as input to the filter bank.
While you do this, you may want to first smooth the edges of the image to avoid generating a strong edge all
around the image, which would skew the results of the line detection algorithm.
The problem is here:
public static double ApplyFilterOnOneCoord(double u, double v, double Du, double Dv, int CenterX, int CenterY, double Theta, int N)
{
double uStar = KassWitkinFunction.uStar(u, v, CenterX, CenterY, Theta);
double vStar = KassWitkinFunction.vStar(u, v, CenterX, CenterY, Theta);
double uStarDu = uStar / Du;
double vStarDv = vStar / Dv;
double sqrt = Math.Sqrt(uStarDu + vStarDv);
double _2n = 2 * N;
double pow = Math.Pow(sqrt, _2n);
if (!double.IsNaN(sqrt) && Math.Abs(pow - Math.Pow(uStarDu + vStarDv, N)) > 1e-7)
{
//execution will never reach here!!
}
pow = Math.Pow(uStarDu + vStarDv, N);
double div = 1 + 0.414 * pow;
double returns = 1 / div;
return returns;
}
What I don't understand is why should we take the square root, before computing the Math.Pow, especially when we know that the power is an even number. The only thing it does (besides making the code more complex and slow) is to generate NaN for negative values.
I'm not sure if the entire computation is right now, but now all the 12 filtered images appear!
This is used in pre-processing, and is claimed to come from the paper by Kass and Within. I tried to read the original paper, but the quality is very low and hard to read. Do you happen to have a link to a better quality scan of their [15] reference?

GPS Calculator conversion, calculate lat/lon value for new point

Here is problem which i have:
I load image in C#. On that image I have to insert 2 points: point A and point B by clicking mouse on random possitions.
Point A have it cords (Xa, Ya) which is read from program and I need to manually insert its GPS coords (LatitudeA and LongtudeA) for it.
Point B have it own cords (Xb, Yb) which is also read from program and I need to manually insert its GPS coords (LatitudeB and LongtudeB) for it.
So main problem is next: on every next click on screen I have to know GPS cords for that point. Also for that point C i have (Xc, Yc).
Here is my ComputeLatitudeAndLogitude method, but it seems it doesnt works perfectly. I need this on street level size.
Example:
A (487, 361, 45.252464, 19.850337)
B (1167, 397, 45.252026, 19.853990)
C (810, 513, ??? , ???); results should be C(810, 513, 45.251592 , 19.852075)
PLEASE feel free to contact me so we can fix problem, mymailis hladnopivo1990#gmail.com
public void ComputeLatitudeAndLogitud (Wpf_Maps.Point point)
{
int diffX = pointA.X - pointB.X;
int diffY = pointA.Y - pointB.Y;
double diffLatitude = pointA.Latitude - pointB.Latitude;
double diffLongitude = pointA.Longitude - pointB.Longitude;
double latitudePerPixel = Math.Abs(diffLatitude / diffX);
double longitudePerPixel = Math.Abs(diffLongitude / diffY);
int diffXforA = pointA.X - point.X;
int diffYforA = pointA.Y - point.Y;
int diffXforB = pointB.X - point.X;
int diffYforB = pointB.Y - point.Y;
double newLatitudeFromA = pointA.Latitude + (diffXforA * latitudePerPixel);
double newLatitudeFromB = pointB.Latitude + (diffXforB * latitudePerPixel);
double newLongitudeFromA = pointA.Longitude + (diffYforA * longitudePerPixel);
double newLongitudeFromB = pointB.Longitude + (diffYforB * longitudePerPixel);
point.Latitude = (newLatitudeFromA + newLatitudeFromB) / 2;
point.Longitude = (newLongitudeFromA + newLongitudeFromB) / 2;
}
Depending on the distance you need to cover, linear extrapolation will not work too good; the earth is not plain, and latitude distances vary with longitude.
One approximation would be a sphere on which you calculate the Great-circle distance.
(GPS) coordinates are usually recorded relative to a (non-sphere) model of the earth, the WGS-84 ellipsoid being the most common today. So for maximum accuracy, you'll have to calculate distances based on the corresponding reference model.
Additionally, if the reference model of the image is different from that of the GPS coordinates you may need more than two reference points to determine the exact mapping.
I presume pointA and pointB are at opposite corners of the map, A being bottom left (or top left?)... meaning every point C is up and right of the point A.
Try this simplification:
public void ComputeLatitudeAndLogitud (Wpf_Maps.Point point)
{
int diffX = pointA.X - pointB.X;
int diffY = pointA.Y - pointB.Y;
double diffLatitude = pointA.Latitude - pointB.Latitude;
double diffLongitude = pointA.Longitude - pointB.Longitude;
double latitudePerPixel = Math.Abs(diffLatitude / diffX);
double longitudePerPixel = Math.Abs(diffLongitude / diffY);
int diffXforC = point.X - pointA.X;
int diffYforC = point.Y - pointA.Y;
point.Latitude = pointA.Latitude + (diffXforC * latitudePerPixel);
point.Longitude = pointA.Longitude + (diffYforC * longitudePerPixel);
}
Here's my full code. I've got three tests cases there. The first is where pointC is somewhere random, in the second, pointC matches pointA, and in the third, pointC matches pointB.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
ComputeLatitudeAndLongitude(new MyPoint(0, 0, 10, 10), new MyPoint(100, 100, 40, 40), new MyPoint(20, 40));
ComputeLatitudeAndLongitude(new MyPoint(0, 0, 10, 10), new MyPoint(100, 100, 40, 40), new MyPoint(0, 0));
ComputeLatitudeAndLongitude(new MyPoint(0, 0, 10, 10), new MyPoint(100, 100, 40, 40), new MyPoint(100, 100));
}
public void ComputeLatitudeAndLongitude(MyPoint pointA, MyPoint pointB, MyPoint pointC)
{
int diffX = pointA.X - pointB.X;
int diffY = pointA.Y - pointB.Y;
double diffLatitude = pointA.Latitude - pointB.Latitude;
double diffLongitude = pointA.Longitude - pointB.Longitude;
double latitudePerPixel = Math.Abs(diffLatitude / diffX);
double longitudePerPixel = Math.Abs(diffLongitude / diffY);
int diffXforC = pointC.X - pointA.X;
int diffYforC = pointC.Y - pointA.Y;
pointC.Latitude = pointA.Latitude + (diffXforC * latitudePerPixel);
pointC.Longitude = pointA.Longitude + (diffYforC * longitudePerPixel);
LogResults(String.Format("pointA X:{0} Y:{1} Lat:{2} Long:{3}", pointA.X, pointA.Y, pointA.Latitude, pointA.Longitude), true);
LogResults(String.Format("pointB X:{0} Y:{1} Lat:{2} Long:{3}", pointB.X, pointB.Y, pointB.Latitude, pointB.Longitude), true);
LogResults(String.Format("pointC X:{0} Y:{1} Lat:{2} Long:{3}", pointC.X, pointC.Y, pointC.Latitude, pointC.Longitude), true);
LogResults(String.Empty, true);
}
public void LogResults(string message, bool insertNewline)
{
txtResults.Text += message + (insertNewline ? Environment.NewLine : String.Empty);
}
}
public class MyPoint
{
public int X;
public int Y;
public double Latitude = 0;
public double Longitude = 0;
public MyPoint(int x, int y)
{
X = x;
Y = y;
}
public MyPoint(int x, int y, double latitude, double longitude) : this(x, y)
{
Latitude = latitude;
Longitude = longitude;
}
}
Results:
pointA X:0 Y:0 Lat:10 Long:10
pointB X:100 Y:100 Lat:40 Long:40
pointC X:20 Y:40 Lat:16 Long:22 // C.X is 20% of the way from A.X to B.X, so C.Lat is 20% of the way from A.Lat to B.Lat, Y/Long are 40%
pointA X:0 Y:0 Lat:10 Long:10
pointB X:100 Y:100 Lat:40 Long:40
pointC X:0 Y:0 Lat:10 Long:10 // C.X = A.X and C.Y = A.Y, therefore C.Lat and C.Long = A.Lat and A.Long
pointA X:0 Y:0 Lat:10 Long:10
pointB X:100 Y:100 Lat:40 Long:40
pointC X:100 Y:100 Lat:40 Long:40 // C.X = B.X and C.Y = B.Y, therefore C.Lat and C.Long = B.Lat and B.Long
It cant be done with code above i wrote. Constant EARTH_RADIUS MUST me implemented in function for calculation, if we want to get high precision (6 digits at least: n,mmmmmm). Next code will scale proper lat/lon per pixel, so calculated point C will match referent points A and B when we put that (X,Y) cords which points A and B have.
public void ComputeLatitudeAndLogitude(Wpf_Maps.Point point){
double diffLatAB = pointB.Latitude - pointA.Latitude;
double diffLonAB = pointB.Longitude - pointA.Longitude;
int diffXAB = pointB.X - pointA.X;
int diffYAB = pointB.Y - pointA.Y;
int diffXAC = point.X - pointA.X;
int diffYAC = point.Y - pointA.Y;
point.Latitude = diffLatAB / diffXAB * diffXAC + pointA.Latitude;
point.Longitude = diffLonAB / diffYAB * diffYAC + pointA.Longitude;
}

Smoothing a hand-drawn curve

I've got a program that allows users to draw curves. But these curves don't look nice - they look wobbly and hand-drawn.
So I want an algorithm that will automatically smooth them. I know there are inherent ambiguities in the smoothing process, so it won't be perfect every time, but such algorithms do seem to exist in several drawing packages and they work quite well.
Are there any code samples for something like this? C# would be perfect, but I can translate from other languages.
You can reduce the number of points using the Ramer–Douglas–Peucker algorithm there is a C# implementation here. I gave this a try using WPFs PolyQuadraticBezierSegment and it showed a small amount of improvement depending on the tolerance.
After a bit of searching sources (1,2) seem to indicate that using the curve fitting algorithm from Graphic Gems by Philip J Schneider works well, the C code is available. Geometric Tools also has some resources that could be worth investigating.
This is a rough sample I made, there are still some glitches but it works well a lot of the time. Here is the quick and dirty C# port of FitCurves.c. One of the issues is that if you don't reduce the original points the calculated error is 0 and it terminates early the sample uses the point reduction algorithm beforehand.
/*
An Algorithm for Automatically Fitting Digitized Curves
by Philip J. Schneider
from "Graphics Gems", Academic Press, 1990
*/
public static class FitCurves
{
/* Fit the Bezier curves */
private const int MAXPOINTS = 10000;
public static List<Point> FitCurve(Point[] d, double error)
{
Vector tHat1, tHat2; /* Unit tangent vectors at endpoints */
tHat1 = ComputeLeftTangent(d, 0);
tHat2 = ComputeRightTangent(d, d.Length - 1);
List<Point> result = new List<Point>();
FitCubic(d, 0, d.Length - 1, tHat1, tHat2, error,result);
return result;
}
private static void FitCubic(Point[] d, int first, int last, Vector tHat1, Vector tHat2, double error,List<Point> result)
{
Point[] bezCurve; /*Control points of fitted Bezier curve*/
double[] u; /* Parameter values for point */
double[] uPrime; /* Improved parameter values */
double maxError; /* Maximum fitting error */
int splitPoint; /* Point to split point set at */
int nPts; /* Number of points in subset */
double iterationError; /*Error below which you try iterating */
int maxIterations = 4; /* Max times to try iterating */
Vector tHatCenter; /* Unit tangent vector at splitPoint */
int i;
iterationError = error * error;
nPts = last - first + 1;
/* Use heuristic if region only has two points in it */
if(nPts == 2)
{
double dist = (d[first]-d[last]).Length / 3.0;
bezCurve = new Point[4];
bezCurve[0] = d[first];
bezCurve[3] = d[last];
bezCurve[1] = (tHat1 * dist) + bezCurve[0];
bezCurve[2] = (tHat2 * dist) + bezCurve[3];
result.Add(bezCurve[1]);
result.Add(bezCurve[2]);
result.Add(bezCurve[3]);
return;
}
/* Parameterize points, and attempt to fit curve */
u = ChordLengthParameterize(d, first, last);
bezCurve = GenerateBezier(d, first, last, u, tHat1, tHat2);
/* Find max deviation of points to fitted curve */
maxError = ComputeMaxError(d, first, last, bezCurve, u,out splitPoint);
if(maxError < error)
{
result.Add(bezCurve[1]);
result.Add(bezCurve[2]);
result.Add(bezCurve[3]);
return;
}
/* If error not too large, try some reparameterization */
/* and iteration */
if(maxError < iterationError)
{
for(i = 0; i < maxIterations; i++)
{
uPrime = Reparameterize(d, first, last, u, bezCurve);
bezCurve = GenerateBezier(d, first, last, uPrime, tHat1, tHat2);
maxError = ComputeMaxError(d, first, last,
bezCurve, uPrime,out splitPoint);
if(maxError < error)
{
result.Add(bezCurve[1]);
result.Add(bezCurve[2]);
result.Add(bezCurve[3]);
return;
}
u = uPrime;
}
}
/* Fitting failed -- split at max error point and fit recursively */
tHatCenter = ComputeCenterTangent(d, splitPoint);
FitCubic(d, first, splitPoint, tHat1, tHatCenter, error,result);
tHatCenter.Negate();
FitCubic(d, splitPoint, last, tHatCenter, tHat2, error,result);
}
static Point[] GenerateBezier(Point[] d, int first, int last, double[] uPrime, Vector tHat1, Vector tHat2)
{
int i;
Vector[,] A = new Vector[MAXPOINTS,2];/* Precomputed rhs for eqn */
int nPts; /* Number of pts in sub-curve */
double[,] C = new double[2,2]; /* Matrix C */
double[] X = new double[2]; /* Matrix X */
double det_C0_C1, /* Determinants of matrices */
det_C0_X,
det_X_C1;
double alpha_l, /* Alpha values, left and right */
alpha_r;
Vector tmp; /* Utility variable */
Point[] bezCurve = new Point[4]; /* RETURN bezier curve ctl pts */
nPts = last - first + 1;
/* Compute the A's */
for (i = 0; i < nPts; i++) {
Vector v1, v2;
v1 = tHat1;
v2 = tHat2;
v1 *= B1(uPrime[i]);
v2 *= B2(uPrime[i]);
A[i,0] = v1;
A[i,1] = v2;
}
/* Create the C and X matrices */
C[0,0] = 0.0;
C[0,1] = 0.0;
C[1,0] = 0.0;
C[1,1] = 0.0;
X[0] = 0.0;
X[1] = 0.0;
for (i = 0; i < nPts; i++) {
C[0,0] += V2Dot(A[i,0], A[i,0]);
C[0,1] += V2Dot(A[i,0], A[i,1]);
/* C[1][0] += V2Dot(&A[i][0], &A[i][9]);*/
C[1,0] = C[0,1];
C[1,1] += V2Dot(A[i,1], A[i,1]);
tmp = ((Vector)d[first + i] -
(
((Vector)d[first] * B0(uPrime[i])) +
(
((Vector)d[first] * B1(uPrime[i])) +
(
((Vector)d[last] * B2(uPrime[i])) +
((Vector)d[last] * B3(uPrime[i]))))));
X[0] += V2Dot(A[i,0], tmp);
X[1] += V2Dot(A[i,1], tmp);
}
/* Compute the determinants of C and X */
det_C0_C1 = C[0,0] * C[1,1] - C[1,0] * C[0,1];
det_C0_X = C[0,0] * X[1] - C[1,0] * X[0];
det_X_C1 = X[0] * C[1,1] - X[1] * C[0,1];
/* Finally, derive alpha values */
alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1;
alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1;
/* If alpha negative, use the Wu/Barsky heuristic (see text) */
/* (if alpha is 0, you get coincident control points that lead to
* divide by zero in any subsequent NewtonRaphsonRootFind() call. */
double segLength = (d[first] - d[last]).Length;
double epsilon = 1.0e-6 * segLength;
if (alpha_l < epsilon || alpha_r < epsilon)
{
/* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */
double dist = segLength / 3.0;
bezCurve[0] = d[first];
bezCurve[3] = d[last];
bezCurve[1] = (tHat1 * dist) + bezCurve[0];
bezCurve[2] = (tHat2 * dist) + bezCurve[3];
return (bezCurve);
}
/* First and last control points of the Bezier curve are */
/* positioned exactly at the first and last data points */
/* Control points 1 and 2 are positioned an alpha distance out */
/* on the tangent vectors, left and right, respectively */
bezCurve[0] = d[first];
bezCurve[3] = d[last];
bezCurve[1] = (tHat1 * alpha_l) + bezCurve[0];
bezCurve[2] = (tHat2 * alpha_r) + bezCurve[3];
return (bezCurve);
}
/*
* Reparameterize:
* Given set of points and their parameterization, try to find
* a better parameterization.
*
*/
static double[] Reparameterize(Point[] d,int first,int last,double[] u,Point[] bezCurve)
{
int nPts = last-first+1;
int i;
double[] uPrime = new double[nPts]; /* New parameter values */
for (i = first; i <= last; i++) {
uPrime[i-first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i-first]);
}
return uPrime;
}
/*
* NewtonRaphsonRootFind :
* Use Newton-Raphson iteration to find better root.
*/
static double NewtonRaphsonRootFind(Point[] Q,Point P,double u)
{
double numerator, denominator;
Point[] Q1 = new Point[3], Q2 = new Point[2]; /* Q' and Q'' */
Point Q_u, Q1_u, Q2_u; /*u evaluated at Q, Q', & Q'' */
double uPrime; /* Improved u */
int i;
/* Compute Q(u) */
Q_u = BezierII(3, Q, u);
/* Generate control vertices for Q' */
for (i = 0; i <= 2; i++) {
Q1[i].X = (Q[i+1].X - Q[i].X) * 3.0;
Q1[i].Y = (Q[i+1].Y - Q[i].Y) * 3.0;
}
/* Generate control vertices for Q'' */
for (i = 0; i <= 1; i++) {
Q2[i].X = (Q1[i+1].X - Q1[i].X) * 2.0;
Q2[i].Y = (Q1[i+1].Y - Q1[i].Y) * 2.0;
}
/* Compute Q'(u) and Q''(u) */
Q1_u = BezierII(2, Q1, u);
Q2_u = BezierII(1, Q2, u);
/* Compute f(u)/f'(u) */
numerator = (Q_u.X - P.X) * (Q1_u.X) + (Q_u.Y - P.Y) * (Q1_u.Y);
denominator = (Q1_u.X) * (Q1_u.X) + (Q1_u.Y) * (Q1_u.Y) +
(Q_u.X - P.X) * (Q2_u.X) + (Q_u.Y - P.Y) * (Q2_u.Y);
if (denominator == 0.0f) return u;
/* u = u - f(u)/f'(u) */
uPrime = u - (numerator/denominator);
return (uPrime);
}
/*
* Bezier :
* Evaluate a Bezier curve at a particular parameter value
*
*/
static Point BezierII(int degree,Point[] V,double t)
{
int i, j;
Point Q; /* Point on curve at parameter t */
Point[] Vtemp; /* Local copy of control points */
/* Copy array */
Vtemp = new Point[degree+1];
for (i = 0; i <= degree; i++) {
Vtemp[i] = V[i];
}
/* Triangle computation */
for (i = 1; i <= degree; i++) {
for (j = 0; j <= degree-i; j++) {
Vtemp[j].X = (1.0 - t) * Vtemp[j].X + t * Vtemp[j+1].X;
Vtemp[j].Y = (1.0 - t) * Vtemp[j].Y + t * Vtemp[j+1].Y;
}
}
Q = Vtemp[0];
return Q;
}
/*
* B0, B1, B2, B3 :
* Bezier multipliers
*/
static double B0(double u)
{
double tmp = 1.0 - u;
return (tmp * tmp * tmp);
}
static double B1(double u)
{
double tmp = 1.0 - u;
return (3 * u * (tmp * tmp));
}
static double B2(double u)
{
double tmp = 1.0 - u;
return (3 * u * u * tmp);
}
static double B3(double u)
{
return (u * u * u);
}
/*
* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent :
*Approximate unit tangents at endpoints and "center" of digitized curve
*/
static Vector ComputeLeftTangent(Point[] d,int end)
{
Vector tHat1;
tHat1 = d[end+1]- d[end];
tHat1.Normalize();
return tHat1;
}
static Vector ComputeRightTangent(Point[] d,int end)
{
Vector tHat2;
tHat2 = d[end-1] - d[end];
tHat2.Normalize();
return tHat2;
}
static Vector ComputeCenterTangent(Point[] d,int center)
{
Vector V1, V2, tHatCenter = new Vector();
V1 = d[center-1] - d[center];
V2 = d[center] - d[center+1];
tHatCenter.X = (V1.X + V2.X)/2.0;
tHatCenter.Y = (V1.Y + V2.Y)/2.0;
tHatCenter.Normalize();
return tHatCenter;
}
/*
* ChordLengthParameterize :
* Assign parameter values to digitized points
* using relative distances between points.
*/
static double[] ChordLengthParameterize(Point[] d,int first,int last)
{
int i;
double[] u = new double[last-first+1]; /* Parameterization */
u[0] = 0.0;
for (i = first+1; i <= last; i++) {
u[i-first] = u[i-first-1] + (d[i-1] - d[i]).Length;
}
for (i = first + 1; i <= last; i++) {
u[i-first] = u[i-first] / u[last-first];
}
return u;
}
/*
* ComputeMaxError :
* Find the maximum squared distance of digitized points
* to fitted curve.
*/
static double ComputeMaxError(Point[] d,int first,int last,Point[] bezCurve,double[] u,out int splitPoint)
{
int i;
double maxDist; /* Maximum error */
double dist; /* Current error */
Point P; /* Point on curve */
Vector v; /* Vector from point to curve */
splitPoint = (last - first + 1)/2;
maxDist = 0.0;
for (i = first + 1; i < last; i++) {
P = BezierII(3, bezCurve, u[i-first]);
v = P - d[i];
dist = v.LengthSquared;
if (dist >= maxDist) {
maxDist = dist;
splitPoint = i;
}
}
return maxDist;
}
private static double V2Dot(Vector a,Vector b)
{
return((a.X*b.X)+(a.Y*b.Y));
}
}
Kris's answer is a very good port of the original to C#, but the performance isn't ideal and there are some places where floating point instability can cause some issues and return NaN values (this is true in the original code too). I created a library that contains my own port of it as well as Ramer-Douglas-Peuker, and should work not only with WPF points, but the new SIMD-enabled vector types and Unity 3D also:
https://github.com/burningmime/curves
Maybe this WPF+Bezier-based article is a good start: Draw a Smooth Curve through a Set of 2D Points with Bezier Primitives
Well, the job of Kris was very useful.
I realized that the problem he pointed out about the algorithm terminating earlier because of a miscalculated error ending on 0, is due to the fact that one point is repeated and the computed tangent is infinite.
I have done a translation to Java, based on the code of Kris, it works fine I believe:
EDIT:
I still working and trying to get a better behavior on the algorithm. I realized that on very spiky angles, the Bezier curves simply don't behave good. So I tried to combine Bezier curves with Lines and this is the result:
import java.awt.Point;
import java.awt.Shape;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.vecmath.Point2d;
import javax.vecmath.Tuple2d;
import javax.vecmath.Vector2d;
/*
An Algorithm for Automatically Fitting Digitized Curves
by Philip J. Schneider
from "Graphics Gems", Academic Press, 1990
*/
public class FitCurves
{
/* Fit the Bezier curves */
private final static int MAXPOINTS = 10000;
private final static double epsilon = 1.0e-6;
/**
* Rubén:
* This is the sensitivity. When it is 1, it will create a line if it is at least as long as the
* distance from the previous control point.
* When it is greater, it will create less lines, and when it is lower, more lines.
* This is based on the previous control point since I believe it is a good indicator of the curvature
* where it is coming from, and we don't want long and second derived constant curves to be modeled with
* many lines.
*/
private static final double lineSensitivity=0.75;
public interface ResultCurve {
public Point2D getStart();
public Point2D getEnd();
public Shape getShape();
}
public static class BezierCurve implements ResultCurve {
public Point start;
public Point end;
public Point ctrl1;
public Point ctrl2;
public BezierCurve(Point2D start, Point2D ctrl1, Point2D ctrl2, Point2D end) {
this.start=new Point((int)Math.round(start.getX()), (int)Math.round(start.getY()));
this.end=new Point((int)Math.round(end.getX()), (int)Math.round(end.getY()));
this.ctrl1=new Point((int)Math.round(ctrl1.getX()), (int)Math.round(ctrl1.getY()));
this.ctrl2=new Point((int)Math.round(ctrl2.getX()), (int)Math.round(ctrl2.getY()));
if(this.ctrl1.x<=1 || this.ctrl1.y<=1) {
throw new IllegalStateException("ctrl1 invalid");
}
if(this.ctrl2.x<=1 || this.ctrl2.y<=1) {
throw new IllegalStateException("ctrl2 invalid");
}
}
public Shape getShape() {
return new CubicCurve2D.Float(start.x, start.y, ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, end.x, end.y);
}
public Point getStart() {
return start;
}
public Point getEnd() {
return end;
}
}
public static class CurveSegment implements ResultCurve {
Point2D start;
Point2D end;
public CurveSegment(Point2D startP, Point2D endP) {
this.start=startP;
this.end=endP;
}
public Shape getShape() {
return new Line2D.Float(start, end);
}
public Point2D getStart() {
return start;
}
public Point2D getEnd() {
return end;
}
}
public static List<ResultCurve> FitCurve(double[][] points, double error) {
Point[] allPoints=new Point[points.length];
for(int i=0; i < points.length; i++) {
allPoints[i]=new Point((int) Math.round(points[i][0]), (int) Math.round(points[i][1]));
}
return FitCurve(allPoints, error);
}
public static List<ResultCurve> FitCurve(Point[] d, double error)
{
Vector2d tHat1, tHat2; /* Unit tangent vectors at endpoints */
int first=0;
int last=d.length-1;
tHat1 = ComputeLeftTangent(d, first);
tHat2 = ComputeRightTangent(d, last);
List<ResultCurve> result = new LinkedList<ResultCurve>();
FitCubic(d, first, last, tHat1, tHat2, error, result);
return result;
}
private static void FitCubic(Point[] d, int first, int last, Vector2d tHat1, Vector2d tHat2, double error, List<ResultCurve> result)
{
PointE[] bezCurve; /*Control points of fitted Bezier curve*/
double[] u; /* Parameter values for point */
double[] uPrime; /* Improved parameter values */
double maxError; /* Maximum fitting error */
int nPts; /* Number of points in subset */
double iterationError; /*Error below which you try iterating */
int maxIterations = 4; /* Max times to try iterating */
Vector2d tHatCenter; /* Unit tangent vector at splitPoint */
int i;
double errorOnLine=error;
iterationError = error * error;
nPts = last - first + 1;
AtomicInteger outputSplitPoint=new AtomicInteger();
/**
* Rubén: Here we try to fit the form with a line, and we mark the split point if we find any line with a minimum length
*/
/*
* the minimum distance for a length (so we don't create a very small line, when it could be slightly modeled with the previous Bezier,
* will be proportional to the distance of the previous control point of the last Bezier.
*/
BezierCurve res=null;
for(i=result.size()-1; i >0; i--) {
ResultCurve thisCurve=result.get(i);
if(thisCurve instanceof BezierCurve) {
res=(BezierCurve)thisCurve;
break;
}
}
Line seg=new Line(d[first], d[last]);
int nAcceptableTogether=0;
int startPoint=-1;
int splitPointTmp=-1;
if(Double.isInfinite(seg.getGradient())) {
for (i = first; i <= last; i++) {
double dist=Math.abs(d[i].x-d[first].x);
if(dist<errorOnLine) {
nAcceptableTogether++;
if(startPoint==-1) startPoint=i;
} else {
if(startPoint!=-1) {
double minLineLength=Double.POSITIVE_INFINITY;
if(res!=null) {
minLineLength=lineSensitivity * res.ctrl2.distance(d[startPoint]);
}
double thisFromStart=d[startPoint].distance(d[i]);
if(thisFromStart >= minLineLength) {
splitPointTmp=i;
startPoint=-1;
break;
}
}
nAcceptableTogether=0;
startPoint=-1;
}
}
} else {
//looking for the max squared error
for (i = first; i <= last; i++) {
Point thisPoint=d[i];
Point2D calculatedP=seg.getByX(thisPoint.getX());
double dist=thisPoint.distance(calculatedP);
if(dist<errorOnLine) {
nAcceptableTogether++;
if(startPoint==-1) startPoint=i;
} else {
if(startPoint!=-1) {
double thisFromStart=d[startPoint].distance(thisPoint);
double minLineLength=Double.POSITIVE_INFINITY;
if(res!=null) {
minLineLength=lineSensitivity * res.ctrl2.distance(d[startPoint]);
}
if(thisFromStart >= minLineLength) {
splitPointTmp=i;
startPoint=-1;
break;
}
}
nAcceptableTogether=0;
startPoint=-1;
}
}
}
if(startPoint!=-1) {
double minLineLength=Double.POSITIVE_INFINITY;
if(res!=null) {
minLineLength=lineSensitivity * res.ctrl2.distance(d[startPoint]);
}
if(d[startPoint].distance(d[last]) >= minLineLength) {
splitPointTmp=startPoint;
startPoint=-1;
} else {
nAcceptableTogether=0;
}
}
outputSplitPoint.set(splitPointTmp);
if(nAcceptableTogether==(last-first+1)) {
//This is a line!
System.out.println("line, length: " + d[first].distance(d[last]));
result.add(new CurveSegment(d[first], d[last]));
return;
}
/*********************** END of the Line approach, lets try the normal algorithm *******************************************/
if(splitPointTmp < 0) {
if(nPts == 2) {
double dist = d[first].distance(d[last]) / 3.0; //sqrt((last.x-first.x)^2 + (last.y-first.y)^2) / 3.0
bezCurve = new PointE[4];
bezCurve[0] = new PointE(d[first]);
bezCurve[3] = new PointE(d[last]);
bezCurve[1]=new PointE(tHat1).scaleAdd(dist, bezCurve[0]); //V2Add(&bezCurve[0], V2Scale(&tHat1, dist), &bezCurve[1]);
bezCurve[2]=new PointE(tHat2).scaleAdd(dist, bezCurve[3]); //V2Add(&bezCurve[3], V2Scale(&tHat2, dist), &bezCurve[2]);
result.add(new BezierCurve(bezCurve[0],bezCurve[1],bezCurve[2],bezCurve[3]));
return;
}
/* Parameterize points, and attempt to fit curve */
u = ChordLengthParameterize(d, first, last);
bezCurve = GenerateBezier(d, first, last, u, tHat1, tHat2);
/* Find max deviation of points to fitted curve */
maxError = ComputeMaxError(d, first, last, bezCurve, u, outputSplitPoint);
if(maxError < error) {
result.add(new BezierCurve(bezCurve[0],bezCurve[1],bezCurve[2],bezCurve[3]));
return;
}
/* If error not too large, try some reparameterization */
/* and iteration */
if(maxError < iterationError)
{
for(i = 0; i < maxIterations; i++) {
uPrime = Reparameterize(d, first, last, u, bezCurve);
bezCurve = GenerateBezier(d, first, last, uPrime, tHat1, tHat2);
maxError = ComputeMaxError(d, first, last, bezCurve, uPrime, outputSplitPoint);
if(maxError < error) {
result.add(new BezierCurve(bezCurve[0],bezCurve[1],bezCurve[2],bezCurve[3]));
return;
}
u = uPrime;
}
}
}
/* Fitting failed -- split at max error point and fit recursively */
tHatCenter = ComputeCenterTangent(d, outputSplitPoint.get());
FitCubic(d, first, outputSplitPoint.get(), tHat1, tHatCenter, error,result);
tHatCenter.negate();
FitCubic(d, outputSplitPoint.get(), last, tHatCenter, tHat2, error,result);
}
//Checked!!
static PointE[] GenerateBezier(Point2D[] d, int first, int last, double[] uPrime, Vector2d tHat1, Vector2d tHat2)
{
int i;
Vector2d[][] A = new Vector2d[MAXPOINTS][2]; /* Precomputed rhs for eqn */
int nPts; /* Number of pts in sub-curve */
double[][] C = new double[2][2]; /* Matrix C */
double[] X = new double[2]; /* Matrix X */
double det_C0_C1, /* Determinants of matrices */
det_C0_X,
det_X_C1;
double alpha_l, /* Alpha values, left and right */
alpha_r;
PointE[] bezCurve = new PointE[4]; /* RETURN bezier curve ctl pts */
nPts = last - first + 1;
/* Compute the A's */
for (i = 0; i < nPts; i++) {
Vector2d v1=new Vector2d(tHat1);
Vector2d v2=new Vector2d(tHat2);
v1.scale(B1(uPrime[i]));
v2.scale(B2(uPrime[i]));
A[i][0] = v1;
A[i][1] = v2;
}
/* Create the C and X matrices */
C[0][0] = 0.0;
C[0][1] = 0.0;
C[1][0] = 0.0;
C[1][1] = 0.0;
X[0] = 0.0;
X[1] = 0.0;
for (i = 0; i < nPts; i++) {
C[0][0] += A[i][0].dot(A[i][0]); //C[0][0] += V2Dot(&A[i][0], &A[i][0]);
C[0][1] += A[i][0].dot(A[i][1]); //C[0][1] += V2Dot(&A[i][0], &A[i][1]);
/* C[1][0] += V2Dot(&A[i][0], &A[i][9]);*/
C[1][0] = C[0][1]; //C[1][0] = C[0][1]
C[1][1] += A[i][1].dot(A[i][1]); //C[1][1] += V2Dot(&A[i][1], &A[i][1]);
Tuple2d scaleLastB2=new Vector2d(PointE.getPoint2d(d[last])); scaleLastB2.scale(B2(uPrime[i])); // V2ScaleIII(d[last], B2(uPrime[i]))
Tuple2d scaleLastB3=new Vector2d(PointE.getPoint2d(d[last])); scaleLastB3.scale(B3(uPrime[i])); // V2ScaleIII(d[last], B3(uPrime[i]))
Tuple2d dLastB2B3Sum=new Vector2d(scaleLastB2); dLastB2B3Sum.add(scaleLastB3); //V2AddII(V2ScaleIII(d[last], B2(uPrime[i])), V2ScaleIII(d[last], B3(uPrime[i]))
Tuple2d scaleFirstB1=new Vector2d(PointE.getPoint2d(d[first])); scaleFirstB1.scale(B1(uPrime[i])); //V2ScaleIII(d[first], B1(uPrime[i]))
Tuple2d sumScaledFirstB1andB2B3=new Vector2d(scaleFirstB1); sumScaledFirstB1andB2B3.add(dLastB2B3Sum); //V2AddII(V2ScaleIII(d[first], B1(uPrime[i])), V2AddII(V2ScaleIII(d[last], B2(uPrime[i])), V2ScaleIII(d[last], B3(uPrime[i])))
Tuple2d scaleFirstB0=new Vector2d(PointE.getPoint2d(d[first])); scaleFirstB0.scale(B0(uPrime[i])); //V2ScaleIII(d[first], B0(uPrime[i])
Tuple2d sumB0Rest=new Vector2d(scaleFirstB0); sumB0Rest.add(sumScaledFirstB1andB2B3); //V2AddII(V2ScaleIII(d[first], B0(uPrime[i])), V2AddII( V2ScaleIII(d[first], B1(uPrime[i])), V2AddII(V2ScaleIII(d[last], B2(uPrime[i])), V2ScaleIII(d[last], B3(uPrime[i]))))));
Vector2d tmp=new Vector2d(PointE.getPoint2d(d[first + i]));
tmp.sub(sumB0Rest);
X[0] += A[i][0].dot(tmp);
X[1] += A[i][1].dot(tmp);
}
/* Compute the determinants of C and X */
det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
/* Finally, derive alpha values */
alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1;
alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1;
/* If alpha negative, use the Wu/Barsky heuristic (see text) */
/* (if alpha is 0, you get coincident control points that lead to
* divide by zero in any subsequent NewtonRaphsonRootFind() call. */
double segLength = d[first].distance(d[last]); //(d[first] - d[last]).Length;
double epsilonRel = epsilon * segLength;
if (alpha_l < epsilonRel || alpha_r < epsilonRel) {
/* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */
double dist = segLength / 3.0;
bezCurve[0] = new PointE(d[first]);
bezCurve[3] = new PointE(d[last]);
Vector2d b1Tmp=new Vector2d(tHat1); b1Tmp.scaleAdd(dist, bezCurve[0].getPoint2d());
bezCurve[1] = new PointE(b1Tmp); //(tHat1 * dist) + bezCurve[0];
Vector2d b2Tmp=new Vector2d(tHat2); b2Tmp.scaleAdd(dist, bezCurve[3].getPoint2d());
bezCurve[2] = new PointE(b2Tmp); //(tHat2 * dist) + bezCurve[3];
return (bezCurve);
}
/* First and last control points of the Bezier curve are */
/* positioned exactly at the first and last data points */
/* Control points 1 and 2 are positioned an alpha distance out */
/* on the tangent vectors, left and right, respectively */
bezCurve[0] = new PointE(d[first]);
bezCurve[3] = new PointE(d[last]);
Vector2d alphaLTmp=new Vector2d(tHat1); alphaLTmp.scaleAdd(alpha_l, bezCurve[0].getPoint2d());
bezCurve[1] = new PointE(alphaLTmp); //(tHat1 * alpha_l) + bezCurve[0]
Vector2d alphaRTmp=new Vector2d(tHat2); alphaRTmp.scaleAdd(alpha_r, bezCurve[3].getPoint2d());
bezCurve[2] = new PointE(alphaRTmp); //(tHat2 * alpha_r) + bezCurve[3];
return (bezCurve);
}
/*
* Reparameterize:
* Given set of points and their parameterization, try to find
* a better parameterization.
*
*/
static double[] Reparameterize(Point2D[] d,int first,int last,double[] u, Point2D[] bezCurve)
{
int nPts = last-first+1;
int i;
double[] uPrime = new double[nPts]; /* New parameter values */
for (i = first; i <= last; i++) {
uPrime[i-first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i-first]);
}
return uPrime;
}
/*
* NewtonRaphsonRootFind :
* Use Newton-Raphson iteration to find better root.
*/
static double NewtonRaphsonRootFind(Point2D[] Q, Point2D P, double u)
{
double numerator, denominator;
Point2D[] Q1 = new Point2D[3]; //Q'
Point2D[] Q2 = new Point2D[2]; //Q''
Point2D Q_u, Q1_u, Q2_u; /*u evaluated at Q, Q', & Q'' */
double uPrime; /* Improved u */
int i;
/* Compute Q(u) */
Q_u = BezierII(3, Q, u);
/* Generate control vertices for Q' */
for (i = 0; i <= 2; i++) {
double qXTmp=(Q[i+1].getX() - Q[i].getX()) * 3.0; //Q1[i].x = (Q[i+1].x - Q[i].x) * 3.0;
double qYTmp=(Q[i+1].getY() - Q[i].getY()) * 3.0; //Q1[i].y = (Q[i+1].y - Q[i].y) * 3.0;
Q1[i]=new Point2D.Double(qXTmp, qYTmp);
}
/* Generate control vertices for Q'' */
for (i = 0; i <= 1; i++) {
double qXTmp=(Q1[i+1].getX() - Q1[i].getX()) * 2.0; //Q2[i].x = (Q1[i+1].x - Q1[i].x) * 2.0;
double qYTmp=(Q1[i+1].getY() - Q1[i].getY()) * 2.0; //Q2[i].y = (Q1[i+1].y - Q1[i].y) * 2.0;
Q2[i]=new Point2D.Double(qXTmp, qYTmp);
}
/* Compute Q'(u) and Q''(u) */
Q1_u = BezierII(2, Q1, u);
Q2_u = BezierII(1, Q2, u);
/* Compute f(u)/f'(u) */
numerator = (Q_u.getX() - P.getX()) * (Q1_u.getX()) + (Q_u.getY() - P.getY()) * (Q1_u.getY());
denominator = (Q1_u.getX()) * (Q1_u.getX()) + (Q1_u.getY()) * (Q1_u.getY()) + (Q_u.getX() - P.getX()) * (Q2_u.getX()) + (Q_u.getY() - P.getY()) * (Q2_u.getY());
if (denominator == 0.0f) return u;
/* u = u - f(u)/f'(u) */
uPrime = u - (numerator/denominator);
return (uPrime);
}
/*
* Bezier :
* Evaluate a Bezier curve at a particular parameter value
*
*/
static Point2D BezierII(int degree, Point2D[] V, double t)
{
int i, j;
Point2D Q; /* Point on curve at parameter t */
Point2D[] Vtemp; /* Local copy of control points */
/* Copy array */
Vtemp = new Point2D[degree+1];
for (i = 0; i <= degree; i++) {
Vtemp[i] = new Point2D.Double(V[i].getX(), V[i].getY());
}
/* Triangle computation */
for (i = 1; i <= degree; i++) {
for (j = 0; j <= degree-i; j++) {
double tmpX, tmpY;
tmpX = (1.0 - t) * Vtemp[j].getX() + t * Vtemp[j+1].getX();
tmpY = (1.0 - t) * Vtemp[j].getY() + t * Vtemp[j+1].getY();
Vtemp[j].setLocation(tmpX, tmpY);
}
}
Q = Vtemp[0];
return Q;
}
/*
* B0, B1, B2, B3 :
* Bezier multipliers
*/
static double B0(double u)
{
double tmp = 1.0 - u;
return (tmp * tmp * tmp);
}
static double B1(double u)
{
double tmp = 1.0 - u;
return (3 * u * (tmp * tmp));
}
static double B2(double u)
{
double tmp = 1.0 - u;
return (3 * u * u * tmp);
}
static double B3(double u)
{
return (u * u * u);
}
/*
* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent :
*Approximate unit tangents at endpoints and "center" of digitized curve
*/
static Vector2d ComputeLeftTangent(Point[] d, int end)
{
Vector2d tHat1=new Vector2d(PointE.getPoint2d(d[end+1]));
tHat1.sub(PointE.getPoint2d(d[end]));
tHat1.normalize();
return tHat1;
}
static Vector2d ComputeRightTangent(Point[] d, int end)
{
//tHat2 = V2SubII(d[end-1], d[end]); tHat2 = *V2Normalize(&tHat2);
Vector2d tHat2=new Vector2d(PointE.getPoint2d(d[end-1]));
tHat2.sub(PointE.getPoint2d(d[end]));
tHat2.normalize();
return tHat2;
}
static Vector2d ComputeCenterTangent(Point[] d ,int center)
{
//V1 = V2SubII(d[center-1], d[center]);
Vector2d V1=new Vector2d(PointE.getPoint2d(d[center-1]));
V1.sub(new PointE(d[center]).getPoint2d());
//V2 = V2SubII(d[center], d[center+1]);
Vector2d V2=new Vector2d(PointE.getPoint2d(d[center]));
V2.sub(PointE.getPoint2d(d[center+1]));
//tHatCenter.x = (V1.x + V2.x)/2.0;
//tHatCenter.y = (V1.y + V2.y)/2.0;
//tHatCenter = *V2Normalize(&tHatCenter);
Vector2d tHatCenter=new Vector2d((V1.x + V2.x)/2.0, (V1.y + V2.y)/2.0);
tHatCenter.normalize();
return tHatCenter;
}
/*
* ChordLengthParameterize :
* Assign parameter values to digitized points
* using relative distances between points.
*/
static double[] ChordLengthParameterize(Point[] d,int first,int last)
{
int i;
double[] u = new double[last-first+1]; /* Parameterization */
u[0] = 0.0;
for (i = first+1; i <= last; i++) {
u[i-first] = u[i-first-1] + d[i-1].distance(d[i]);
}
for (i = first + 1; i <= last; i++) {
u[i-first] = u[i-first] / u[last-first];
}
return u;
}
/*
* ComputeMaxError :
* Find the maximum squared distance of digitized points
* to fitted curve.
*/
static double ComputeMaxError(Point2D[] d, int first, int last, Point2D[] bezCurve, double[] u, AtomicInteger splitPoint)
{
int i;
double maxDist; /* Maximum error */
double dist; /* Current error */
Point2D P; /* Point on curve */
Vector2d v; /* Vector from point to curve */
int tmpSplitPoint=(last - first + 1)/2;
maxDist = 0.0;
for (i = first + 1; i < last; i++) {
P = BezierII(3, bezCurve, u[i-first]);
v = new Vector2d(P.getX() - d[i].getX(), P.getY() - d[i].getY()); //P - d[i];
dist = v.lengthSquared();
if (dist >= maxDist) {
maxDist = dist;
tmpSplitPoint=i;
}
}
splitPoint.set(tmpSplitPoint);
return maxDist;
}
/**
* This is kind of a bridge between javax.vecmath and java.util.Point2D
* #author Ruben
* #since 1.24
*/
public static class PointE extends Point2D.Double {
private static final long serialVersionUID = -1482403817370130793L;
public PointE(Tuple2d tup) {
super(tup.x, tup.y);
}
public PointE(Point2D p) {
super(p.getX(), p.getY());
}
public PointE(double x, double y) {
super(x, y);
}
public PointE scale(double dist) {
return new PointE(getX()*dist, getY()*dist);
}
public PointE scaleAdd(double dist, Point2D sum) {
return new PointE(getX()*dist + sum.getX(), getY()*dist + sum.getY());
}
public PointE substract(Point2D p) {
return new PointE(getX() - p.getX(), getY() - p.getY());
}
public Point2d getPoint2d() {
return getPoint2d(this);
}
public static Point2d getPoint2d(Point2D p) {
return new Point2d(p.getX(), p.getY());
}
}
Here an image of the latter working, white are lines, and red are Bezier:
Using this approach we use less control points and more accurate.
The sensitivity for the lines creation can be adjusted through the lineSensitivity attribute. If you don't want lines to be used at all just set it to infinite.
I'm sure this can be improved. Feel free to contribute :)
The algorithm is not doing any reduction, and because of the first explained in my post we have to run one. Here is a DouglasPeuckerReduction implementation, which for me works in some cases even more efficiently (less points to store and faster to render) than an additional FitCurves
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class DouglasPeuckerReduction {
public static List<Point> reduce(Point[] points, double tolerance)
{
if (points == null || points.length < 3) return Arrays.asList(points);
int firstPoint = 0;
int lastPoint = points.length - 1;
SortedList<Integer> pointIndexsToKeep;
try {
pointIndexsToKeep = new SortedList<Integer>(LinkedList.class);
} catch (Throwable t) {
t.printStackTrace(System.out);
ErrorReport.process(t);
return null;
}
//Add the first and last index to the keepers
pointIndexsToKeep.add(firstPoint);
pointIndexsToKeep.add(lastPoint);
//The first and the last point cannot be the same
while (points[firstPoint].equals(points[lastPoint])) {
lastPoint--;
}
reduce(points, firstPoint, lastPoint, tolerance, pointIndexsToKeep);
List<Point> returnPoints = new ArrayList<Point>(pointIndexsToKeep.size());
for (int pIndex : pointIndexsToKeep) {
returnPoints.add(points[pIndex]);
}
return returnPoints;
}
private static void reduce(Point[] points, int firstPoint, int lastPoint, double tolerance, List<Integer> pointIndexsToKeep) {
double maxDistance = 0;
int indexFarthest = 0;
Line tmpLine=new Line(points[firstPoint], points[lastPoint]);
for (int index = firstPoint; index < lastPoint; index++) {
double distance = tmpLine.getDistanceFrom(points[index]);
if (distance > maxDistance) {
maxDistance = distance;
indexFarthest = index;
}
}
if (maxDistance > tolerance && indexFarthest != 0) {
//Add the largest point that exceeds the tolerance
pointIndexsToKeep.add(indexFarthest);
reduce(points, firstPoint, indexFarthest, tolerance, pointIndexsToKeep);
reduce(points, indexFarthest, lastPoint, tolerance, pointIndexsToKeep);
}
}
}
I am using here my own implementation of a SortedList, and of a Line. You will have to make it yourself, sorry.
I haven't tested it, but one approach that comes to mind would be sampling values in some interval and creating a spline to connect the dots.
For example, say the x value of your curve starts at 0 and ends at 10. So you sample the y values at x=1,2,3,4,5,6,7,8,9,10 and create a spline from the points (0, y(0)), (1,y(1)), ... (10, y(10))
It would probably have problems such as accidental spikes drawn by the user, but it may be worth a shot
For Silverlight users of Kris' answer, Point is hobbled and Vector doesn't exist. This is a minimal Vector class that supports the code:
public class Vector
{
public double X { get; set; }
public double Y { get; set; }
public Vector(double x=0, double y=0)
{
X = x;
Y = y;
}
public static implicit operator Vector(Point b)
{
return new Vector(b.X, b.Y);
}
public static Point operator *(Vector left, double right)
{
return new Point(left.X * right, left.Y * right);
}
public static Vector operator -(Vector left, Point right)
{
return new Vector(left.X - right.X, left.Y - right.Y);
}
internal void Negate()
{
X = -X;
Y = -Y;
}
internal void Normalize()
{
double factor = 1.0 / Math.Sqrt(LengthSquared);
X *= factor;
Y *= factor;
}
public double LengthSquared { get { return X * X + Y * Y; } }
}
Also had to address use of Length and +,- operators. I chose to just add functions to the FitCurves class, and rewrite their usages where the compiler complained.
public static double Length(Point a, Point b)
{
double x = a.X-b.X;
double y = a.Y-b.Y;
return Math.Sqrt(x*x+y*y);
}
public static Point Add(Point a, Point b)
{
return new Point(a.X + b.X, a.Y + b.Y);
}
public static Point Subtract(Point a, Point b)
{
return new Point(a.X - b.X, a.Y - b.Y);
}

Categories

Resources