Well, your images already have a dark background.
A basic naive method is to spawn circles on white (shape), and grow until they touch another circle or dark (mask). So, you could threshold your image into a mask – for example, by looping over pixels and setting each to 1 (255) or 0 based on a threshold. You will probably need to do some cleanup such as eroding or annealing to remove mess on the edges and get a clean mask region.
Another alternative is to create a complex polygon, then spawn circles inside the polygon and grow until they cross outside. Your shape is concave, which is a pain. For a polygon approach, one method is to begin by finding points along the edges. Here is a really simple way of doing that – it works on your example skin, but might not work on ones that have highly concave regions.
PImage img = loadImage("proc-sheepskin.jpeg");
size(557, 462);
image(img, 0, 0);
ArrayList<PVector> points = new ArrayList<PVector>();
int grain = 10;
int thresh = 64;
for (int x=0; x<width; x=x+grain) {
for (int y=0; y<height; y++) {
if (brightness(get(x, y))>thresh) {
points.add(new PVector(x, y));
break;
}
}
for (int y=height; y>0; y--) {
if (brightness(get(x, y))>thresh) {
points.add(new PVector(x, y));
break;
}
}
}
for (int y=0; y<height; y=y+grain) {
for (int x=0; x<width; x++) {
if (brightness(get(x, y))>thresh) {
points.add(new PVector(x, y));
break;
}
}
for (int x=width; x>0; x--) {
if (brightness(get(x, y))>thresh) {
points.add(new PVector(x, y));
break;
}
}
}
for (int i=0; i<points.size(); i++) {
PVector pt = points.get(i);
ellipse(pt.x, pt.y, grain, grain);
}
Once you have a set of edge points you can try to find the polygon that they define in various ways. For example: try to walk the edge from an arbitrary starting point in an arbitrary direction, finding the next closest point. Or you could try radial sorting from the center of the bounding box. Or you could use the mesh library to get a Delaunay triangulation, then prune long exterior edges from the convex hull to get a concave hull – from there, the concave hull is a polygon.
Note that concave hulls are hard. There are lots of approaches:
https://community.esri.com/blogs/dan_patterson/2018/03/11/concave-hulls-the-elusive-container
If you have a lot of complicated shapes then at some point you may need to test regions between points to see if they are white or not to guess whether those regions should be connected. There is no perfect geometric solution – just mesh up your points and then test. This might be one reason to consider a raw threshold and flood fill approach rather than a polygon – if you have to check all points anyway, you could just use a magic wand tool approach at the pixel level.
Just as an example of the polygon approach, here is a very crude example that takes the Dalaunay diagram and then simply ignores lines that are less than a threshold distance. It almost sort of works at giving you a list of points / lines for your polygon.
import megamu.mesh.Delaunay;
int sz = points.size();
float[][] pointsArray = new float[sz][];
for (int i=0; i<sz; i++) {
PVector pt = points.get(i);
pointsArray[i] = new float[]{pt.x, pt.y};
}
Delaunay myDelaunay = new Delaunay( pointsArray );
float[][] myEdges = myDelaunay.getEdges();
for (int i=0; i<myEdges.length; i++) {
float startX = myEdges[i][0];
float startY = myEdges[i][1];
float endX = myEdges[i][2];
float endY = myEdges[i][3];
if (dist(startX, startY, endX, endY)<3*grain) {
stroke(255, 0, 0);
} else {
stroke(32);
}
line( startX, startY, endX, endY );
}
Once you actually have an irregular polygon, I recommend checking out this discussion and paper: