if ((e != null && l.size() > 0) && (l != null)) {
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(2));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < l.size() - 1; i++) {
Point p1 = l.get(i);
Point p2 = l.get(i + 1);
g.setColor(currentColor);
g.drawLine(p1.x, p1.y, p2.x, p2.y);
}
Point p3 = l.get(l.size() - 1);
g.drawLine(p3.x, p3.y, e.getPoint().x, e.getPoint().y);
}
I am creating a pencil-like tool in java and I'm having some problems when it comes to drawing the lines. It works as it should, but the lines are very edged and really not smooth at all.
I've looked into smoothing them by bezier things but that would be tricky for points close to one another. I just maybe want some form of line next to the one I have but with a lower alpha-value or something like that. That is the effect i'm after.
This is what it looks like now.
http://imgur.com/dycSEwT
I suspect that the problem you are running into is that you are preserving a little too much information. You could certainly do some blurring or fattening of the line, which might help you to create a blurrier, fatter line, but I think that that would not actually help you to clean up the jitters. My recommendation to make the lines look really nice would be a two step process. The first process would be to use DP Line simplification to remove a lot of the slight jitters. The second step of the process would be to use a Centripetal Catmull-Rom Spline in order to make the whole line flow like a very elegant curve. The beauty of using this sort of spline for this purpose is that you don't need to do any serious work trying to figure out how to come up with all the control points that you would for doing Besier curves. You can just use the original points plus two points at the start and end of your curve.
The Duglas-Peucker line simplifier is available in java using JTS from vivid solutions:
http://www.vividsolutions.com/jts/JTSHome.htm
Here is the link to the Catmull-Rom code.
Catmull-rom curve with no cusps and no self-intersections
Direct Drawing (jittery)
Simplified Drawing (Using JTS DP Line Simplification)
Simplified and Smoothed using Centripetal Catmull-Rom
Draw Panel Example Code
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package demo;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.simplify.DouglasPeuckerSimplifier;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* #author hdunsford
*/
public class DrawPanel extends javax.swing.JPanel {
private List<Coordinate> coords;
/**
* Creates new form DrawPanel
*/
public DrawPanel() {
initComponents();
coords = new ArrayList<>();
this.addMouseMotionListener(new MouseMotionListener() {
#Override
public void mouseDragged(MouseEvent e) {
coords.add(new Coordinate(e.getX(), e.getY()));
repaint();
}
#Override
public void mouseMoved(MouseEvent e) {
}
});
}
#Override
protected void paintComponent(Graphics g) {
try {
super.paintComponent(g); // paint background
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(2));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
GeometryFactory f = new GeometryFactory();
if (coords.size() < 2) {
return;
}
LineString ls = f.createLineString(coords.toArray(new Coordinate[0]));
//Geometry simple = ls;
Geometry simple = DouglasPeuckerSimplifier.simplify(ls, 3.0);
if (simple.getCoordinates().length < 2) {
return;
}
List<Coordinate> raw = new ArrayList<>();
raw.addAll(Arrays.asList(simple.getCoordinates()));
List<Coordinate> spline = CatmullRom.interpolate(raw, 10);
int[] xPoints = new int[spline.size()];
int[] yPoints = new int[spline.size()];
for (int i = 0; i < spline.size(); i++) {
xPoints[i] = (int) spline.get(i).x;
yPoints[i] = (int) spline.get(i).y;
}
g2d.setColor(Color.red);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawPolyline(xPoints, yPoints, xPoints.length);
} catch (Exception ex) {
Logger.getLogger(DrawPanel.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* This method is called from within the constructor to initialize the form. WARNING:
* Do NOT modify this code. The content of this method is always regenerated by the
* Form Editor.
*/
#SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
setBackground(new java.awt.Color(255, 255, 255));
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 400, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 300, Short.MAX_VALUE)
);
}// </editor-fold>
// Variables declaration - do not modify
// End of variables declaration
}
Centripetal Catmull-Rom (tweaked to work with JTS Coordinate)
package demo;
import com.vividsolutions.jts.geom.Coordinate;
import java.util.ArrayList;
import java.util.List;
/**
*
* #author hdunsford
*/
public class CatmullRom {
/**
* This method will calculate the Catmull-Rom interpolation curve, returning it as a
* list of Coordinate coordinate objects. This method in particular adds the first and
* last control points which are not visible, but required for calculating the spline.
*
* #param coordinates The list of original straight line points to calculate an
* interpolation from.
* #param pointsPerSegment The integer number of equally spaced points to return along
* each curve. The actual distance between each point will depend on the spacing
* between the control points.
* #return The list of interpolated coordinates.
*/
public static List<Coordinate> interpolate(List<Coordinate> coordinates, int pointsPerSegment)
throws Exception {
List<Coordinate> vertices = new ArrayList<>();
for (Coordinate c : coordinates) {
vertices.add(new Coordinate(c.x, c.y));
}
if (pointsPerSegment < 2) {
throw new Exception("The pointsPerSegment parameter must be greater than 2, since 2 points is just the linear segment.");
}
// Cannot interpolate curves given only two points. Two points
// is best represented as a simple line segment.
if (vertices.size() < 3) {
return vertices;
}
// Test whether the shape is open or closed by checking to see if
// the first point intersects with the last point. M and Z are ignored.
boolean isClosed = vertices.get(0).x == vertices.get(vertices.size() - 1).x
&& vertices.get(0).y == vertices.get(vertices.size() - 1).y;
if (isClosed) {
// Use the second and second from last points as control points.
// get the second point.
Coordinate p2 = new Coordinate(vertices.get(1));
// get the point before the last point
Coordinate pn1 = new Coordinate(vertices.get(vertices.size() - 2));
// insert the second from the last point as the first point in the list
// because when the shape is closed it keeps wrapping around to
// the second point.
vertices.add(0, pn1);
// add the second point to the end.
vertices.add(p2);
} else {
// The shape is open, so use control points that simply extend
// the first and last segments
// Get the change in x and y between the first and second coordinates.
double dx = vertices.get(1).x - vertices.get(0).x;
double dy = vertices.get(1).y - vertices.get(0).y;
// Then using the change, extrapolate backwards to find a control point.
double x1 = vertices.get(0).x - dx;
double y1 = vertices.get(0).y - dy;
// Actaully create the start point from the extrapolated values.
Coordinate start = new Coordinate(x1, y1);
// Repeat for the end control point.
int n = vertices.size() - 1;
dx = vertices.get(n).x - vertices.get(n - 1).x;
dy = vertices.get(n).y - vertices.get(n - 1).y;
double xn = vertices.get(n).x + dx;
double yn = vertices.get(n).y + dy;
Coordinate end = new Coordinate(xn, yn);
// insert the start control point at the start of the vertices list.
vertices.add(0, start);
// append the end control ponit to the end of the vertices list.
vertices.add(end);
}
// Dimension a result list of coordinates.
List<Coordinate> result = new ArrayList<>();
// When looping, remember that each cycle requires 4 points, starting
// with i and ending with i+3. So we don't loop through all the points.
for (int i = 0; i < vertices.size() - 3; i++) {
// Actually calculate the Catmull-Rom curve for one segment.
List<Coordinate> points = interpolate(vertices, i, pointsPerSegment);
// Since the middle points are added twice, once for each bordering
// segment, we only add the 0 index result point for the first
// segment. Otherwise we will have duplicate points.
if (result.size() > 0) {
points.remove(0);
}
// Add the coordinates for the segment to the result list.
result.addAll(points);
}
return result;
}
/**
* Given a list of control points, this will create a list of pointsPerSegment points
* spaced uniformly along the resulting Catmull-Rom curve.
*
* #param points The list of control points, leading and ending with a coordinate that
* is only used for controling the spline and is not visualized.
* #param index The index of control point p0, where p0, p1, p2, and p3 are used in
* order to create a curve between p1 and p2.
* #param pointsPerSegment The total number of uniformly spaced interpolated points to
* calculate for each segment. The larger this number, the smoother the resulting
* curve.
* #return the list of coordinates that define the CatmullRom curve between the points
* defined by index+1 and index+2.
*/
public static List<Coordinate> interpolate(List<Coordinate> points, int index, int pointsPerSegment) {
List<Coordinate> result = new ArrayList<>();
double[] x = new double[4];
double[] y = new double[4];
double[] time = new double[4];
for (int i = 0; i < 4; i++) {
x[i] = points.get(index + i).x;
y[i] = points.get(index + i).y;
time[i] = i;
}
double tstart;
double tend;
double total = 0;
for (int i = 1; i < 4; i++) {
double dx = x[i] - x[i - 1];
double dy = y[i] - y[i - 1];
total += Math.pow(dx * dx + dy * dy, .25);
time[i] = total;
}
tstart = time[1];
tend = time[2];
int segments = pointsPerSegment - 1;
result.add(points.get(index + 1));
for (int i = 1; i < segments; i++) {
double xi = interpolate(x, time, tstart + (i * (tend - tstart)) / segments);
double yi = interpolate(y, time, tstart + (i * (tend - tstart)) / segments);
result.add(new Coordinate(xi, yi));
}
result.add(points.get(index + 2));
return result;
}
/**
* Unlike the other implementation here, which uses the default "uniform" treatment of
* t, this computation is used to calculate the same values but introduces the ability
* to "parameterize" the t values used in the calculation. This is based on Figure 3
* from http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
*
* #param p An array of double values of length 4, where interpolation occurs from p1
* to p2.
* #param time An array of time measures of length 4, corresponding to each p value.
* #param t the actual interpolation ratio from 0 to 1 representing the position
* between p1 and p2 to interpolate the value.
* #return
*/
public static double interpolate(double[] p, double[] time, double t) {
double L01 = p[0] * (time[1] - t) / (time[1] - time[0]) + p[1] * (t - time[0]) / (time[1] - time[0]);
double L12 = p[1] * (time[2] - t) / (time[2] - time[1]) + p[2] * (t - time[1]) / (time[2] - time[1]);
double L23 = p[2] * (time[3] - t) / (time[3] - time[2]) + p[3] * (t - time[2]) / (time[3] - time[2]);
double L012 = L01 * (time[2] - t) / (time[2] - time[0]) + L12 * (t - time[0]) / (time[2] - time[0]);
double L123 = L12 * (time[3] - t) / (time[3] - time[1]) + L23 * (t - time[1]) / (time[3] - time[1]);
double C12 = L012 * (time[2] - t) / (time[2] - time[1]) + L123 * (t - time[1]) / (time[2] - time[1]);
return C12;
}
}
Related
Description
Given the following parameters:
Outer radius
Inner radius
Amount of regions
Starting angle (that second picture starts at 180 degrees)
Angle drawn (that second picture draws a total of 320 degrees from its starting angle)
How does one calculate the maximal size of the Images to draw so that they remain within their sector? On the left picture, I'm just using a hard-coded value because I haven't specifically been able to think of a nice, general, mathematical equation for that.
More variables could eventually come into play, if I ever decide to expand my library even more. Things such as the width of the separators or of the circumference's outline.
Code
This is what I use to draw everything but the children Actors:
/**
* Takes care of drawing everything that {#link #layout()} didn't.
* (Basically everything but the children Actors.)
*
* #param batch a Batch used to draw Drawables. The {#link #sd} is used to
* draw everything else.
* #param parentAlpha
*/
protected void drawWithShapeDrawer(Batch batch, float parentAlpha) {
/* Pre-calculating */
float bgRadian = MathUtils.degreesToRadians*style.totalDegreesDrawn;
float tmpOffset = MathUtils.degreesToRadians*style.startDegreesOffset;
int size = getChildren().size;
float tmpRad = bgRadian / size;
/* Background image */
if(style.background != null)
style.background.draw(batch, getX(), getY(), getWidth(), getHeight());
/* Rest of background */
if(style.backgroundColor != null) {
sd.setColor(style.backgroundColor);
sd.sector(getX()+style.radius, getY()+style.radius, style.radius-BG_BUFFER, tmpOffset, bgRadian);
}
/* Children */
vector2.set(getX()+style.radius, getY()+style.radius);
for(int i=0; i<size; i++) {
float tmp = tmpOffset + i*tmpRad;
drawChildWithoutSelection(vector2, i, tmp, tmpRad);
/* Separator */
drawChildSeparator(vector2, tmp);
}
/* The remaining last separator to be drawn */
drawChildSeparator(vector2, tmpOffset + size*tmpRad);
}
protected void drawChildSeparator(Vector2 vector2, float drawnRadianAngle) {
if(getChildren().size > 1 && style.separatorColor != null)
sd.line(pointAtAngle(vector22, vector2, style.innerRadius, drawnRadianAngle),
pointAtAngle(vector23, vector2, style.radius, drawnRadianAngle),
style.separatorColor, style.separatorWidth);
}
protected void drawChildWithoutSelection(Vector2 vector2, int index, float startAngle, float radian) {
if(style.childRegionColor != null) {
if(style.alternateChildRegionColor != null) {
sd.setColor(index%2 == 0 ? style.childRegionColor : style.alternateChildRegionColor);
sd.arc(vector2.x, vector2.y, (style.radius+style.innerRadius)/2, startAngle, radian, style.radius-style.innerRadius);
} else {
sd.setColor(style.childRegionColor);
sd.arc(vector2.x, vector2.y, (style.radius+style.innerRadius)/2, startAngle, radian, style.radius-style.innerRadius);
}
}
drawChildCircumference(vector2, startAngle, radian, style.radius - style.circumferenceWidth/2);
}
protected void drawChildCircumference(Vector2 vector2, float startAngle, float radian, float radius) {
if(style.circumferenceColor != null && style.circumferenceWidth > 0) {
sd.setColor(style.circumferenceColor);
sd.arc(vector2.x, vector2.y, radius, startAngle, radian, style.circumferenceWidth);
}
}
And this is how I'm laying out those children:
#Override
public void layout() {
float degreesPerChild = style.totalDegreesDrawn / getChildren().size;
float half = (float)1 / 2;
for (int i = 0; i < getChildren().size; i++) {
Actor actor = getChildren().get(i);
vector2.set((style.radius+style.innerRadius)/2, 0);
vector2.rotate(degreesPerChild*(i + half) + style.startDegreesOffset);
if(actor instanceof Image) { // todo: do this properly !
actor.setSize(30, 30);
}
actor.setPosition(vector2.x+style.radius, vector2.y+style.radius, Align.center);
}
}
Here is what I ended up doing:
/**
* Used to estimate the radius of a circle to be constrained within the widget
* according to the input parameters. Doubling the returned value would give
* you the size of a contained Actor which would roughly fill most of its
* sector, or possibly overflow slightly. It is suggested to adjust slightly
* the returned value by multiplying it with a factor of your choice.<br>
* It's basically the minimum between 3 different possible radius values
* based on certain layout properties.
*
* #param degreesPerChild the amount of degrees that a child's sector takes.
* #param actorDistanceFromCenter the distance at which the child Actor is
* positioned from the center of the widget.
* #return an estimated radius value for an Actor placed with the given
* constraints.
*/
public float getEstimatedRadiusAt(float degreesPerChild, float actorDistanceFromCenter) {
float tmp1 = actorDistanceFromCenter * MathUtils.sinDeg(degreesPerChild/2);
float tmp2 = getMaxRadius() - actorDistanceFromCenter;
float tmp3 = actorDistanceFromCenter - getInnerRadiusLength();
return Math.min(Math.min(tmp1, tmp2), tmp3);
}
This is an extension to my previous question posted here -- Java Swing GUI for equation 5((θ/β) - cos(2πθ/β))
I have implemented the Java program based on the answers provided in the post an here is my program:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class DisplacementFunctionNew extends JFrame {
public DisplacementFunctionNew() {
setLayout(new BorderLayout());
add(new CosGraph(), BorderLayout.CENTER);
}
public static void main(String[] args) {
DisplacementFunctionNew frame = new DisplacementFunctionNew();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(6000, 6000);
frame.setVisible(true);
frame.setLocationRelativeTo(null);
frame.setTitle("SineWave");
}
class CosGraph extends JPanel {
public void paintComponent(Graphics g) {
int graphHeight = 5; // Declared this to set the height of graph based on the value given here.
super.paintComponent(g);
int xBase = 100;
int top = 100;
int yScale = 100;
int xAxis = 360;
int yBase = top + yScale;
g.drawLine(xBase, top, xBase, top + 2 * yScale);
g.drawLine(xBase, yBase, xBase + xAxis, yBase);
g.setColor(Color.red);
double maxY = 0;
for (int i = 0; i < 360; i++) {
maxY = Math.max(maxY, Math.abs(getValue(i)));
}
int x, y;
for (int i = 0; i < 360; i++) {
x = xBase + i;
y = yBase - (int) (getValue(i)*graphHeight / maxY * yScale);
g.drawLine(x, y, x, y);
}
}
private double getValue(int theta) {
int beta = 45;
double b = (theta / (double) beta);
double angle = 2 * Math.PI * (b);
double c = Math.cos(angle);
double s = (b - c);
return s;
}
}
}
Now in this program I want to have a variable called graphHeight that helps to increase the height of the graph. If I give the value of the variable as 1 then I can see the output like this:
Now if I try to increase the height to 5 then I get the graph but it is not shown smoothly or continuous curve, I get the output like this:
Can someone please help me how to get the output as smooth continuous curve?
You are drawing a curve using points. You are placing one point at each location on the x axis -- this makes sense to do.
When the graph is small, it looks fine, because the y separation of these points is relatively small. However, as you increase the graph size, this flaw becomes more noticeable.
The solution here is to fill in the vertical space with lines. You have a few options for the exact implementation of this:
Draw a line from [x(i), y(i)] to [x(i+1),y(i+1)] -- this is easy, but may not look the way you want.
Draw a line from [x(i), y(i)] to [x(i),y(i+1)] -- this is still pretty easy, but it won't be quite correct: you're continuing up so that you could be an entire pixel off.
Draw a line from [x(i), y(i)] to [x(i),(y(i)+y(i+1))/2], and then from [x(i+1), (y(i)+y(i+1))/2] to [x(i+1),y(i+1)] -- this is what 1 should do (neglecting anti-aliasing), and will be the most correct of your possible options.
I would suggest number 3. Note that you can implement this with a loop of the form:
int lastY = yBase - (int) (getValue(0)*graphHeight / maxY * yScale);
for (int i = 1; i < 360; i++) {
x = xBase + i;
y = yBase - (int) (getValue(i)*graphHeight / maxY * yScale);
g.drawLine(x-1, lastY, x-1, (y+lastY)/2);
g.drawLine(x, (y+lastY)/2, x, y);
}
If the one pixel overlap bothers you, you can make it a bit more complex such that the second line starts at +/- 1 pixel (depending on if the function is increasing or decreasing).
Alternatively, you can implement number 3 by manually drawing the line, using a for loop, and basically write a special-case version of Bresenham's line algorithm.
You use graphHeight to define the y of the next point to be painted with g.drawLine(x, y, x, y);. The distance between drawn points will be related to the graphHeight variable
I am trying to figure out the proper way to call arrays from the area method, which are then supposed to calculate the area of the points given. Not sure what the proper way to select the specific x and y coordinates from each array is.
MyPolygon class
import java.util.ArrayList;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
/**
* A class that represents a geometric polygon. Methods are provided for adding
* a point to the polygon and for calculating the perimeter and area of the
* polygon.
*/
class MyPolygon {
// list of the points of the polygon
private ArrayList<Point2D.Double> points;
/**
* Constructs a polygon with no points in it.
*/
public MyPolygon() {
points = new ArrayList<Point2D.Double>();
}
/**
* Adds a point to the end of the list of points in the polygon.
*
* #param x
* The x coordinate of the point.
* #param y
* The y coordinate of the point.
*/
public void add(double x, double y) {
points.add(new Point2D.Double(x, y));
}
/**
* Calculates and returns the perimeter of the polygon.
*
* #return 0.0 if < 2 points in polygon, otherwise returns the sum of the
* lengths of the line segments.
*/
public double perimeter() {
if (points.size() < 2) {
return 0.0;
}
int i = 0;
double d = 0;
double total = points.get(0).distance(points.get(points.size() - 1));
while (i < points.size() - 1) {
Point2D.Double point1 = points.get(i);
// double x = point1.x;
// double y = point1.y;
Point2D.Double point2 = points.get(i + 1);
// double x1 = point2.x;
// double y1 = point2.y;
d = point1.distance(point2);
// d = Math.sqrt(Math.pow(x1 - x,2) + Math.pow(y1 - y, 2));
total = total + d;
i++;
}
return total;
}
/**
* Calculates and returns the area of the polygon.
*
* #return 0.0 if < 3 points in the polygon, otherwise returns the area of
* the polygon.
*/
public double area() {
int i = 0;
double a = 0;
double total = 0;
total = total + a;
if (points.size() < 3) {
return 0.0;
}
for (int m = 0; m < points.size(); m++) {
total = total + (points[m].x() * points[m + 1].y()) - (points[m].y() * points[m + 1].x());
}
return 0.5 * total;
}
}
Tester Class
class PolygonTester {
public static void main(String args[]) {
MyPolygon poly = new MyPolygon();
poly.add(1.0,1.0);
poly.add(3.0,1.0);
poly.add(1.0,3.0);
System.out.println(poly.perimeter());
System.out.println(poly.area());
}
}
Your headline is actually already the solution. You use points[m] which is array notation. But points ist not an array. It is a list. Use points.get(int i) instead, as you did in perimeter().
You're going to run out of bounds on the list. Your for loop continues while m < size(). However you access m+1 in your calculation. So if the list contains 5 elements and m = 4, (4 < 5) so keep looping, you will then access m + 1 which is 5. You don't have an index of 5 since these lists are 0 based.
Also the code is probably not compiling because you're using array syntax to access a list. You should say points.get(m)
The solution is quite simple, and is given away by your title (which I assume to be a compiler error.)
You are treating points as an array, which it is not. You access elements of an ArrayList slightly differently: you use points.get(m) instead of points[m]. If you make that change in area, it will work.
ArrayLists are not arrays. They are objects that are indexed with the get(int) method.
Wherever you have points[m], or something similar, replace it with points.get(m). The line would then become:
total = total + (points.get(m).x() * points.get(m + 1).y()) - (points.get(m).y() * points.get(m + 1).x());
That should clear up that issue, but you will still probably get an IndexOutOfBoundsException on the last iteration of the loop, because you will be trying to index m + 1 when m is the last index. You should change your code depending on how you want it to handle the last element.
I have a image with blackened circles.
The image is a scanned copy of an survey sheet pretty much like an OMR questionnaire sheet.
I want to detect the circles that have been blackened using the JUI(if any other api required)
I have a few examples while searching, but they dont give me accurate result.
I tried..UDAI,Moodle...etc...
Then I decided to make my own. I am able to detect the black pixels but as follows.
BufferedImage mapa = BMPDecoder.read(new File("testjui.bmp"));
final int xmin = mapa.getMinX();
final int ymin = mapa.getMinY();
final int ymax = ymin + mapa.getHeight();
final int xmax = xmin + mapa.getWidth();
for (int i = xmin;i<xmax;i++)
{
for (int j = ymin;j<ymax;j++)
{
int pixel = mapa.getRGB(i, j);
if ((pixel & 0x00FFFFFF) == 0)
{
System.out.println("("+i+","+j+")");
}
}
}
This gives me the co-ordinates of all the black pixels but i cannot make out if its a circle or not.
How can I identify if its a circle.
2] Also I want to know if the image scanned is tilted....I know that the Udai api takes care of that, but for some reason I am not able to get my survey template to run with that code.
So if I understood correctly, you have code that picks out the black pixels so now you have the coordinates of all black pixels and you want to determine all of those that fall on a circle.
The way I would approach this is in 2 steps.
1) Cluster the pixels. Create a class called Cluster, that contains a list of points and use your clustering algorithm to put all the points in the right cluster.
2) Determine which clusters are circles. To do this find the midpoint of all of the points in each cluster (just take the mean of all the points). Then find the minimum and maximum distances from the center, The difference between these should be less than the maximum thickness for a circle in your file. These will give you the radii for the innermost and outermost circles contained within the circle. Now use the equation of a circle x^2 + y^2 = radius, with the radius set to a value between the maximum and minimum found previously to find the points that your cluster should contain. If your cluster contains these it is a circle.
Of course other considerations to consider is whether the shapes you have approximate ellipses rather than circles, in which case you should use the equation of an ellipse. Furthermore, if your file contains circle-like shapes you will need to write additional code to exclude these. On the other hand if all of your circles are exactly the same size you can cut the work that needs to be done by having your algorithm search for circles of that size only.
I hope I could be of some help, good luck!
To answer your first question, I created a class that checks weather an image contains a single non black filled black outlined circle.
This class is experimental, it does not provide exact results all the time, feel free to edit it and to correct the bugs you might encounter.
The setters do not check for nulls or out of range values.
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
/**
* Checks weather an image contains a single non black filled black outlined circle<br />
* This class is experimental, it does not provide exact results all the time, feel free to edit it and to correct
* the bugs you might encounter.
* #author Ahmed KRAIEM
* #version 0.9 alpha
* #since 2013-04-03
*/
public class CircleChecker {
private BufferedImage image;
/**
* Points that are equal to the calculated radius±<code>radiusesErrorMargin%</code> are not considered rogue points.<br />
* <code>radiusesErrorMargin</code> must be <code>>0 && <1</code>
*/
private double radiusesErrorMargin = 0.2;
/**
* A shape that has fewer than roguePointSensitivity% of rogue points is considered a circle.<br />
* <code>roguePointSensitivity</code> must be <code>>0 && <1</code>
*/
private double roguePointSensitivity = 0.05;
/**
* The presumed circle is divided into <code>angleCompartimentPrecision</code> parts,<br />
* each part must have <code>minPointsPerCompartiment</code> points
* <code>angleCompartimentPrecision</code> must be <code>> 0</code>
*/
private int angleCompartimentPrecision = 50;
/**
* The minimum number of points requiered to declare a part valid.<br />
* <code>minPointsPerCompartiment</code> must be <code>> 0</code>
*/
private int minPointsPerCompartiment = 20;
public CircleChecker(BufferedImage image) {
super();
this.image = image;
}
public CircleChecker(BufferedImage image, double radiusesErrorMargin,
int minPointsPerCompartiment, double roguePointSensitivity,
int angleCompartimentPrecision) {
this(image);
this.radiusesErrorMargin = radiusesErrorMargin;
this.minPointsPerCompartiment = minPointsPerCompartiment;
this.roguePointSensitivity = roguePointSensitivity;
this.angleCompartimentPrecision = angleCompartimentPrecision;
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public double getRadiusesErrorMargin() {
return radiusesErrorMargin;
}
public void setRadiusesErrorMargin(double radiusesErrorMargin) {
this.radiusesErrorMargin = radiusesErrorMargin;
}
public double getMinPointsPerCompartiment() {
return minPointsPerCompartiment;
}
public void setMinPointsPerCompartiment(int minPointsPerCompartiment) {
this.minPointsPerCompartiment = minPointsPerCompartiment;
}
public double getRoguePointSensitivity() {
return roguePointSensitivity;
}
public void setRoguePointSensitivity(double roguePointSensitivity) {
this.roguePointSensitivity = roguePointSensitivity;
}
public int getAngleCompartimentPrecision() {
return angleCompartimentPrecision;
}
public void setAngleCompartimentPrecision(int angleCompartimentPrecision) {
this.angleCompartimentPrecision = angleCompartimentPrecision;
}
/**
*
* #return true if the image contains no more than <code>roguePointSensitivity%</code> rogue points
* and all the parts contain at least <code>minPointsPerCompartiment</code> points.
*/
public boolean isCircle() {
List<Point> list = new ArrayList<>();
final int xmin = image.getMinX();
final int ymin = image.getMinY();
final int ymax = ymin + image.getHeight();
final int xmax = xmin + image.getWidth();
for (int i = xmin; i < xmax; i++) {
for (int j = ymin; j < ymax; j++) {
int pixel = image.getRGB(i, j);
if ((pixel & 0x00FFFFFF) == 0) {
list.add(new Point(i, j));
}
}
}
if (list.size() == 0)
return false;
double diameter = -1;
Point p1 = list.get(0);
Point across = null;
for (Point p2 : list) {
double d = distance(p1, p2);
if (d > diameter) {
diameter = d;
across = p2;
}
}
double radius = diameter / 2;
Point center = center(p1, across);
int diffs = 0;
int diffsUntilError = (int) (list.size() * roguePointSensitivity);
double minRadius = radius - radius * radiusesErrorMargin;
double maxRadius = radius + radius * radiusesErrorMargin;
int[] compartiments = new int[angleCompartimentPrecision];
for (int i=0; i<list.size(); i++) {
Point p = list.get(i);
double calRadius = distance(p, center);
if (calRadius>maxRadius || calRadius < minRadius)
diffs++;
else{
//Angle
double angle = Math.atan2(p.y -center.y,p.x-center.x);
//angle is between -pi and pi
int index = (int) ((angle + Math.PI)/(Math.PI * 2 / angleCompartimentPrecision));
compartiments[index]++;
}
if (diffs >= diffsUntilError){
return false;
}
}
int sumCompartiments = list.size() - diffs;
for(int comp : compartiments){
if (comp < minPointsPerCompartiment){
return false;
}
}
return true;
}
private double distance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
private Point center(Point p1, Point p2) {
return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}
public static void main(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File("image.bmp"));
CircleChecker cc = new CircleChecker(image);
System.out.println(cc.isCircle());
}
}
You'll need to program in a template of what a circle would look like, and then make it scalable to suit the different circle sizes.
For example circle of radius 3 would be:
o
ooo
o
This assumes you have a finite set of circles you need to find, maybe up to 5x5 or 6x6 this would be feasible.
or you could use: Midpoint circle algorithm
This would involve finding all black pixel groups and then selecting the middle pixel for each one.
Apply this algorithm using the outer pixels as a guid to how big the circle could be.
Finding the difference between black /expected black pixels.
If the black to expected black ratio is high enough, its a black circle and you can delete / whiten it.
Is there a reason that they decided not to add the contains method (for Path) in Android?
I'm wanting to know what points I have in a Path and hoped it was easier than seen here:
How can I tell if a closed path contains a given point?
Would it be better for me to create an ArrayList and add the integers into the array? (I only check the points once in a control statement) Ie. if(myPath.contains(x,y)
So far my options are:
Using a Region
Using an ArrayList
Extending the Class
Your suggestion
I'm just looking for the most efficient way I should go about this
I came up against this same problem a little while ago, and after some searching, I found this to be the best solution.
Java has a Polygon class with a contains() method that would make things really simple. Unfortunately, the java.awt.Polygonclass is not supported in Android. However, I was able to find someone who wrote an equivalent class.
I don't think you can get the individual points that make up the path from the Android Path class, so you will have to store the data in a different way.
The class uses a Crossing Number algorithm to determine whether or not the point is inside of the given list of points.
/**
* Minimum Polygon class for Android.
*/
public class Polygon
{
// Polygon coodinates.
private int[] polyY, polyX;
// Number of sides in the polygon.
private int polySides;
/**
* Default constructor.
* #param px Polygon y coods.
* #param py Polygon x coods.
* #param ps Polygon sides count.
*/
public Polygon( int[] px, int[] py, int ps )
{
polyX = px;
polyY = py;
polySides = ps;
}
/**
* Checks if the Polygon contains a point.
* #see "http://alienryderflex.com/polygon/"
* #param x Point horizontal pos.
* #param y Point vertical pos.
* #return Point is in Poly flag.
*/
public boolean contains( int x, int y )
{
boolean oddTransitions = false;
for( int i = 0, j = polySides -1; i < polySides; j = i++ )
{
if( ( polyY[ i ] < y && polyY[ j ] >= y ) || ( polyY[ j ] < y && polyY[ i ] >= y ) )
{
if( polyX[ i ] + ( y - polyY[ i ] ) / ( polyY[ j ] - polyY[ i ] ) * ( polyX[ j ] - polyX[ i ] ) < x )
{
oddTransitions = !oddTransitions;
}
}
}
return oddTransitions;
}
}
I would just like to comment on #theisenp answer: The code has integer arrays and if you look on the algorithm description webpage it warns against using integers instead of floating point.
I copied your code above and it seemed to work fine except for some corner cases when I made lines that didnt connect to themselves very well.
By changing everything to floating point, I got rid of this bug.
Tried the other answer, but it gave an erroneous outcome for my case. Didn't bother to find the exact cause, but made my own direct translation from the algorithm on:
http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
Now the code reads:
/**
* Minimum Polygon class for Android.
*/
public class Polygon
{
// Polygon coodinates.
private int[] polyY, polyX;
// Number of sides in the polygon.
private int polySides;
/**
* Default constructor.
* #param px Polygon y coods.
* #param py Polygon x coods.
* #param ps Polygon sides count.
*/
public Polygon( int[] px, int[] py, int ps )
{
polyX = px;
polyY = py;
polySides = ps;
}
/**
* Checks if the Polygon contains a point.
* #see "http://alienryderflex.com/polygon/"
* #param x Point horizontal pos.
* #param y Point vertical pos.
* #return Point is in Poly flag.
*/
public boolean contains( int x, int y )
{
boolean c = false;
int i, j = 0;
for (i = 0, j = polySides - 1; i < polySides; j = i++) {
if (((polyY[i] > y) != (polyY[j] > y))
&& (x < (polyX[j] - polyX[i]) * (y - polyY[i]) / (polyY[j] - polyY[i]) + polyX[i]))
c = !c;
}
return c;
}
}
For completeness, I want to make a couple notes here:
As of API 19, there is an intersection operation for Paths. You could create a very small square path around your test point, intersect it with the Path, and see if the result is empty or not.
You can convert Paths to Regions and do a contains() operation. However Regions work in integer coordinates, and I think they use transformed (pixel) coordinates, so you'll have to work with that. I also suspect that the conversion process is computationally intensive.
The edge-crossing algorithm that Hans posted is good and quick, but you have to be very careful for certain corner cases such as when the ray passes directly through a vertex, or intersects a horizontal edge, or when round-off error is a problem, which it always is.
The winding number method is pretty much fool proof, but involves a lot of trig and is computationally expensive.
This paper by Dan Sunday gives a hybrid algorithm that's as accurate as the winding number but as computationally simple as the ray-casting algorithm. It blew me away how elegant it was.
My code
This is some code I wrote recently in Java which handles a path made out of both line segments and arcs. (Also circles, but those are complete paths on their own, so it's sort of a degenerate case.)
package org.efalk.util;
/**
* Utility: determine if a point is inside a path.
*/
public class PathUtil {
static final double RAD = (Math.PI/180.);
static final double DEG = (180./Math.PI);
protected static final int LINE = 0;
protected static final int ARC = 1;
protected static final int CIRCLE = 2;
/**
* Used to cache the contents of a path for pick testing. For a
* line segment, x0,y0,x1,y1 are the endpoints of the line. For
* a circle (ellipse, actually), x0,y0,x1,y1 are the bounding box
* of the circle (this is how Android and X11 like to represent
* circles). For an arc, x0,y0,x1,y1 are the bounding box, a1 is
* the start angle (degrees CCW from the +X direction) and a1 is
* the sweep angle (degrees CCW).
*/
public static class PathElement {
public int type;
public float x0,y0,x1,y1; // Endpoints or bounding box
public float a0,a1; // Arcs and circles
}
/**
* Determine if the given point is inside the given path.
*/
public static boolean inside(float x, float y, PathElement[] path) {
// Based on algorithm by Dan Sunday, but allows for arc segments too.
// http://geomalgorithms.com/a03-_inclusion.html
int wn = 0;
// loop through all edges of the polygon
// An upward crossing requires y0 <= y and y1 > y
// A downward crossing requires y0 > y and y1 <= y
for (PathElement pe : path) {
switch (pe.type) {
case LINE:
if (pe.x0 < x && pe.x1 < x) // left
break;
if (pe.y0 <= y) { // start y <= P.y
if (pe.y1 > y) { // an upward crossing
if (isLeft(pe, x, y) > 0) // P left of edge
++wn; // have a valid up intersect
}
}
else { // start y > P.y
if (pe.y1 <= y) { // a downward crossing
if (isLeft(pe, x, y) < 0) // P right of edge
--wn; // have a valid down intersect
}
}
break;
case ARC:
wn += arcCrossing(pe, x, y);
break;
case CIRCLE:
// This should be the only element in the path, so test it
// and get out.
float rx = (pe.x1-pe.x0)/2;
float ry = (pe.y1-pe.y0)/2;
float xc = (pe.x1+pe.x0)/2;
float yc = (pe.y1+pe.y0)/2;
return (x-xc)*(x-xc)/rx*rx + (y-yc)*(y-yc)/ry*ry <= 1;
}
}
return wn != 0;
}
/**
* Return >0 if p is left of line p0-p1; <0 if to the right; 0 if
* on the line.
*/
private static float
isLeft(float x0, float y0, float x1, float y1, float x, float y)
{
return (x1 - x0) * (y - y0) - (x - x0) * (y1 - y0);
}
private static float isLeft(PathElement pe, float x, float y) {
return isLeft(pe.x0,pe.y0, pe.x1,pe.y1, x,y);
}
/**
* Determine if an arc segment crosses the test ray up or down, or not
* at all.
* #return winding number increment:
* +1 upward crossing
* 0 no crossing
* -1 downward crossing
*/
private static int arcCrossing(PathElement pe, float x, float y) {
// Look for trivial reject cases first.
if (pe.x1 < x || pe.y1 < y || pe.y0 > y) return 0;
// Find the intersection of the test ray with the arc. This consists
// of finding the intersection(s) of the line with the ellipse that
// contains the arc, then determining if the intersection(s)
// are within the limits of the arc.
// Since we're mostly concerned with whether or not there *is* an
// intersection, we have several opportunities to punt.
// An upward crossing requires y0 <= y and y1 > y
// A downward crossing requires y0 > y and y1 <= y
float rx = (pe.x1-pe.x0)/2;
float ry = (pe.y1-pe.y0)/2;
float xc = (pe.x1+pe.x0)/2;
float yc = (pe.y1+pe.y0)/2;
if (rx == 0 || ry == 0) return 0;
if (rx < 0) rx = -rx;
if (ry < 0) ry = -ry;
// We start by transforming everything so the ellipse is the unit
// circle; this simplifies the math.
x -= xc;
y -= yc;
if (x > rx || y > ry || y < -ry) return 0;
x /= rx;
y /= ry;
// Now find the points of intersection. This is simplified by the
// fact that our line is horizontal. Also, by the time we get here,
// we know there *is* an intersection.
// The equation for the circle is x²+y² = 1. We have y, so solve
// for x = ±sqrt(1 - y²)
double x0 = 1 - y*y;
if (x0 <= 0) return 0;
x0 = Math.sqrt(x0);
// We only care about intersections to the right of x, so
// that's another opportunity to punt. For a CCW arc, The right
// intersection is an upward crossing and the left intersection
// is a downward crossing. The reverse is true for a CW arc.
if (x > x0) return 0;
int wn = arcXing1(x0,y, pe.a0, pe.a1);
if (x < -x0) wn -= arcXing1(-x0,y, pe.a0, pe.a1);
return wn;
}
/**
* Return the winding number of the point x,y on the unit circle
* which passes through the arc segment defined by a0,a1.
*/
private static int arcXing1(double x, float y, float a0, float a1) {
double a = Math.atan2(y,x) * DEG;
if (a < 0) a += 360;
if (a1 > 0) { // CCW
if (a < a0) a += 360;
return a0 + a1 > a ? 1 : 0;
} else { // CW
if (a0 < a) a0 += 360;
return a0 + a1 <= a ? -1 : 0;
}
}
}
Edit: by request, adding some sample code that makes use of this.
import PathUtil;
import PathUtil.PathElement;
/**
* This class represents a single geographic area defined by a
* circle or a list of line segments and arcs.
*/
public class Area {
public float lat0, lon0, lat1, lon1; // bounds
Path path = null;
PathElement[] pathList;
/**
* Return true if this point is inside the area bounds. This is
* used to confirm touch events and may be computationally expensive.
*/
public boolean pointInBounds(float lat, float lon) {
if (lat < lat0 || lat > lat1 || lon < lon0 || lon > lon1)
return false;
return PathUtil.inside(lon, lat, pathList);
}
static void loadBounds() {
int n = number_of_elements_in_input;
path = new Path();
pathList = new PathElement[n];
for (Element element : elements_in_input) {
PathElement pe = new PathElement();
pathList[i] = pe;
pe.type = element.type;
switch (element.type) {
case LINE: // Line segment
pe.x0 = element.x0;
pe.y0 = element.y0;
pe.x1 = element.x1;
pe.y1 = element.y1;
// Add to path, not shown here
break;
case ARC: // Arc segment
pe.x0 = element.xmin; // Bounds of arc ellipse
pe.y0 = element.ymin;
pe.x1 = element.xmax;
pe.y1 = element.ymax;
pe.a0 = a0; pe.a1 = a1;
break;
case CIRCLE: // Circle; hopefully the only entry here
pe.x0 = element.xmin; // Bounds of ellipse
pe.y0 = element.ymin;
pe.x1 = element.xmax;
pe.y1 = element.ymax;
// Add to path, not shown here
break;
}
}
path.close();
}