mirror of
https://github.com/PolyhedralDev/Terra.git
synced 2025-07-02 16:05:29 +00:00
Implement distance transform sampler
This commit is contained in:
parent
514e7065e2
commit
c219eff149
@ -4,6 +4,7 @@ import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.dfsek.terra.addons.image.config.image.DistanceTransformNoiseSamplerTemplate;
|
||||
import com.dfsek.terra.addons.image.config.image.ImageTemplate;
|
||||
import com.dfsek.terra.addons.image.config.image.StitchedImageTemplate;
|
||||
import com.dfsek.terra.addons.image.config.sampler.ConstantColorSamplerTemplate;
|
||||
@ -12,6 +13,7 @@ import com.dfsek.terra.addons.image.config.sampler.image.TileImageColorSamplerTe
|
||||
import com.dfsek.terra.addons.image.config.sampler.mutate.RotateColorSamplerTemplate;
|
||||
import com.dfsek.terra.addons.image.config.sampler.mutate.TranslateColorSamplerTemplate;
|
||||
import com.dfsek.terra.addons.image.image.Image;
|
||||
import com.dfsek.terra.addons.image.operator.DistanceTransform;
|
||||
import com.dfsek.terra.addons.image.sampler.ColorSampler;
|
||||
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
|
||||
import com.dfsek.terra.api.Platform;
|
||||
@ -20,6 +22,7 @@ import com.dfsek.terra.api.config.ConfigPack;
|
||||
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
|
||||
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
|
||||
import com.dfsek.terra.api.inject.annotations.Inject;
|
||||
import com.dfsek.terra.api.noise.NoiseSampler;
|
||||
import com.dfsek.terra.api.registry.CheckedRegistry;
|
||||
import com.dfsek.terra.api.util.reflection.TypeKey;
|
||||
|
||||
@ -32,6 +35,8 @@ public class ImageLibraryAddon implements AddonInitializer {
|
||||
public static final TypeKey<Supplier<ObjectTemplate<ColorSampler>>> COLOR_PICKER_REGISTRY_KEY = new TypeKey<>() {
|
||||
};
|
||||
|
||||
public static final TypeKey<Supplier<ObjectTemplate<NoiseSampler>>> NOISE_SAMPLER_TOKEN = new TypeKey<>() {
|
||||
};
|
||||
@Inject
|
||||
private Platform platform;
|
||||
|
||||
@ -49,6 +54,18 @@ public class ImageLibraryAddon implements AddonInitializer {
|
||||
CheckedRegistry<Supplier<ObjectTemplate<Image>>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY);
|
||||
imageRegistry.register(addon.key("BITMAP"), () -> new ImageTemplate(pack.getLoader(), pack));
|
||||
imageRegistry.register(addon.key("STITCHED_BITMAP"), () -> new StitchedImageTemplate(pack.getLoader(), pack));
|
||||
// imageRegistry.register(addon.key("DISTANCE_TRANSFORM"), DistanceTransformTemplate::new);
|
||||
})
|
||||
.then(event -> {
|
||||
event.getPack()
|
||||
.applyLoader(DistanceTransform.CostFunction.class,
|
||||
(type, o, loader, depthTracker) -> DistanceTransform.CostFunction.valueOf((String) o))
|
||||
.applyLoader(DistanceTransform.Normalization.class,
|
||||
(type, o, loader, depthTracker) -> DistanceTransform.Normalization.valueOf((String) o));
|
||||
|
||||
CheckedRegistry<Supplier<ObjectTemplate<NoiseSampler>>> noiseRegistry = event.getPack().getOrCreateRegistry(
|
||||
NOISE_SAMPLER_TOKEN);
|
||||
noiseRegistry.register(addon.key("DISTANCE_TRANSFORM"), DistanceTransformNoiseSamplerTemplate::new);
|
||||
})
|
||||
.then(event -> {
|
||||
CheckedRegistry<Supplier<ObjectTemplate<ColorSampler>>> colorSamplerRegistry = event.getPack().getOrCreateRegistry(
|
||||
|
@ -0,0 +1,48 @@
|
||||
package com.dfsek.terra.addons.image.config.image;
|
||||
|
||||
import com.dfsek.tectonic.api.config.template.annotations.Default;
|
||||
import com.dfsek.tectonic.api.config.template.annotations.Value;
|
||||
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
|
||||
|
||||
import com.dfsek.terra.addons.image.image.Image;
|
||||
import com.dfsek.terra.addons.image.operator.DistanceTransform;
|
||||
import com.dfsek.terra.addons.image.operator.DistanceTransform.CostFunction;
|
||||
import com.dfsek.terra.addons.image.operator.DistanceTransform.Normalization;
|
||||
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
|
||||
import com.dfsek.terra.api.noise.NoiseSampler;
|
||||
|
||||
|
||||
public class DistanceTransformNoiseSamplerTemplate implements ObjectTemplate<NoiseSampler> {
|
||||
|
||||
@Value("image")
|
||||
private Image image;
|
||||
|
||||
@Value("threshold")
|
||||
@Default
|
||||
private int threshold = 127;
|
||||
|
||||
@Value("clamp-to-max-edge")
|
||||
@Default
|
||||
private boolean clampToEdge = false;
|
||||
|
||||
@Value("channel")
|
||||
@Default
|
||||
private Channel channel = Channel.GRAYSCALE;
|
||||
|
||||
@Value("cost-function")
|
||||
@Default
|
||||
private CostFunction costFunction = CostFunction.Channel;
|
||||
|
||||
@Value("invert-threshold")
|
||||
@Default
|
||||
private boolean invertThreshold = false;
|
||||
|
||||
@Value("normalization")
|
||||
@Default
|
||||
private Normalization normalization = Normalization.None;
|
||||
|
||||
@Override
|
||||
public NoiseSampler get() {
|
||||
return new DistanceTransform.Noise(new DistanceTransform(image, channel, threshold, clampToEdge, costFunction, invertThreshold), normalization);
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
package com.dfsek.terra.addons.image.operator;
|
||||
|
||||
import net.jafama.FastMath;
|
||||
|
||||
import com.dfsek.terra.addons.image.image.Image;
|
||||
import com.dfsek.terra.addons.image.util.ColorUtil;
|
||||
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
|
||||
import com.dfsek.terra.api.noise.NoiseSampler;
|
||||
|
||||
/**
|
||||
* Computes a 2D distance transform of a given image and stores the result in a 2D array of distances.
|
||||
* Implementation based on the algorithm described in the paper
|
||||
* <a href="https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf">Distance Transforms of Sampled Functions</a>
|
||||
* by Pedro F. Felzenszwalb and Daniel P. Huttenlocher.
|
||||
*/
|
||||
public class DistanceTransform {
|
||||
|
||||
private final double[][] distances;
|
||||
|
||||
/**
|
||||
* Size bounds matching the provided image.
|
||||
*/
|
||||
private final int width, height;
|
||||
|
||||
/**
|
||||
* Min and max distances of the distance computation. These may change after {@link #normalize(Normalization)} calls.
|
||||
*/
|
||||
private double minDistance, maxDistance;
|
||||
|
||||
private static final double MAX_DISTANCE_CAP = 10_000_000; // Arbitrarily large value, doubtful someone would
|
||||
// ever use an image large enough to exceed this.
|
||||
public DistanceTransform(Image image, Channel channel, int threshold, boolean clampToMaxEdgeDistance, CostFunction costFunction, boolean invertThreshold) {
|
||||
// Construct binary image based on threshold value
|
||||
boolean[][] binaryImage = new boolean[image.getWidth()][image.getHeight()];
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
binaryImage[x][y] = ColorUtil.getChannel(image.getRGB(x, y), channel) > threshold ^ invertThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
// Get edges of binary image
|
||||
boolean[][] binaryImageEdge = new boolean[image.getWidth()][image.getHeight()];
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
if(!binaryImage[x][y])
|
||||
binaryImageEdge[x][y] = false;
|
||||
else
|
||||
// If cell borders any false cell
|
||||
binaryImageEdge[x][y] = x > 0 && !binaryImage[x-1][y] ||
|
||||
y > 0 && !binaryImage[x][y-1] ||
|
||||
x < image.getWidth ()-1 && !binaryImage[x+1][y] ||
|
||||
y < image.getHeight()-1 && !binaryImage[x][y+1];
|
||||
}
|
||||
}
|
||||
|
||||
double[][] function = new double[image.getWidth()][image.getHeight()];
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
function[x][y] = switch (costFunction) {
|
||||
case Channel -> ColorUtil.getChannel(image.getRGB(x, y), channel);
|
||||
case Threshold -> binaryImage[x][y] ? MAX_DISTANCE_CAP : 0;
|
||||
case ThresholdEdge, ThresholdEdgeSigned -> binaryImageEdge[x][y] ? 0 : MAX_DISTANCE_CAP;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
distances = calculateDistance2D(function);
|
||||
|
||||
if(costFunction == CostFunction.ThresholdEdgeSigned) {
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
distances[x][y] *= binaryImage[x][y] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(clampToMaxEdgeDistance) {
|
||||
// Find largest value on the edge of the image
|
||||
double max = Double.NEGATIVE_INFINITY;
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
max = Math.max(max, distances[x][0]);
|
||||
max = Math.max(max, distances[x][image.getHeight()-1]);
|
||||
}
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
max = Math.max(max, distances[0][y]);
|
||||
max = Math.max(max, distances[image.getWidth()-1][y]);
|
||||
}
|
||||
// Clamp to that largest value
|
||||
for(int x = 0; x < image.getWidth(); x++) {
|
||||
for(int y = 0; y < image.getHeight(); y++) {
|
||||
distances[x][y] = Math.max(max, distances[x][y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.width = image.getWidth();
|
||||
this.height = image.getHeight();
|
||||
|
||||
setOutputRange();
|
||||
}
|
||||
|
||||
private double[][] calculateDistance2D(double[][] f) {
|
||||
double[][] d = new double[f.length][f[0].length];
|
||||
// Distance pass for each column
|
||||
for(int x = 0; x < f.length; x++) {
|
||||
d[x] = calculateDistance1D(f[x]);
|
||||
}
|
||||
// Distance pass for each row
|
||||
double[] row = new double[f.length];
|
||||
for(int y = 0; y < f[0].length; y++) {
|
||||
for(int x = 0; x < f[0].length; x++)
|
||||
row[x] = d[x][y];
|
||||
row = calculateDistance1D(row);
|
||||
for(int x = 0; x < f[0].length; x++) {
|
||||
d[x][y] = FastMath.sqrt(row[x]);
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private double[] calculateDistance1D(double[] f) {
|
||||
double[] d = new double[f.length];
|
||||
int[] v = new int[f.length];
|
||||
double[] z = new double[f.length+1];
|
||||
int k = 0;
|
||||
v[0] = 0;
|
||||
z[0] = Integer.MIN_VALUE;
|
||||
z[1] = Integer.MAX_VALUE;
|
||||
for(int q = 1; q <= f.length-1; q++) {
|
||||
double s = ((f[q]+FastMath.pow2(q))-(f[v[k]]+FastMath.pow2(v[k])))/(2*q-2*v[k]);
|
||||
while (s <= z[k]) {
|
||||
k--;
|
||||
s = ((f[q]+FastMath.pow2(q))-(f[v[k]]+FastMath.pow2(v[k])))/(2*q-2*v[k]);
|
||||
}
|
||||
k++;
|
||||
v[k] = q;
|
||||
z[k] = s;
|
||||
z[k+1] = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
k = 0;
|
||||
for(int q = 0; q <= f.length-1; q++) {
|
||||
while(z[k+1] < q)
|
||||
k++;
|
||||
d[q] = FastMath.pow2(q-v[k]) + f[v[k]];
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redistributes the stored distance computation according to the provided {@link Normalization} method.
|
||||
*/
|
||||
private void normalize(Normalization normalization) {
|
||||
for(int x = 0; x < width; x++) {
|
||||
for(int y = 0; y < height; y++) {
|
||||
double d = distances[x][y];
|
||||
distances[x][y] = switch(normalization) {
|
||||
case None -> distances[x][y];
|
||||
case Linear -> lerp(d, minDistance, -1, maxDistance, 1);
|
||||
case SmoothPreserveZero -> {
|
||||
if(minDistance > 0 || maxDistance < 0) {
|
||||
// Can't preserve zero if it is not contained in range so just lerp
|
||||
yield lerp(distances[x][y], minDistance, -1, maxDistance, 1);
|
||||
} else {
|
||||
if(d > 0) {
|
||||
yield FastMath.pow2(d/maxDistance);
|
||||
} else if(d < 0) {
|
||||
yield -FastMath.pow2(d/minDistance);
|
||||
} else {
|
||||
yield 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
setOutputRange();
|
||||
}
|
||||
|
||||
private void setOutputRange() {
|
||||
double minDistance = Double.POSITIVE_INFINITY;
|
||||
double maxDistance = Double.NEGATIVE_INFINITY;
|
||||
for(int x = 0; x < width; x++) {
|
||||
for(int y = 0; y < height; y++) {
|
||||
minDistance = Math.min(minDistance, distances[x][y]);
|
||||
maxDistance = Math.max(maxDistance, distances[x][y]);
|
||||
}
|
||||
}
|
||||
this.minDistance = minDistance;
|
||||
this.maxDistance = maxDistance;
|
||||
}
|
||||
|
||||
private static double lerp(double x, double x1, double y1, double x2, double y2) {
|
||||
return (((y1-y2)*(x-x1))/(x1-x2))+y1;
|
||||
}
|
||||
|
||||
public enum CostFunction {
|
||||
Channel,
|
||||
Threshold,
|
||||
ThresholdEdge,
|
||||
ThresholdEdgeSigned,
|
||||
}
|
||||
|
||||
public enum Normalization {
|
||||
None,
|
||||
Linear,
|
||||
SmoothPreserveZero,
|
||||
}
|
||||
|
||||
public static class Noise implements NoiseSampler {
|
||||
|
||||
private final DistanceTransform transform;
|
||||
|
||||
public Noise(DistanceTransform transform, Normalization normalization) {
|
||||
this.transform = transform;
|
||||
transform.normalize(normalization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double noise(long seed, double x, double y) {
|
||||
if(x<0 || y<0 || x>=transform.width || y>=transform.height) return transform.minDistance;
|
||||
return transform.distances[FastMath.floorToInt(x)][FastMath.floorToInt(y)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public double noise(long seed, double x, double y, double z) {
|
||||
return noise(seed, x, z);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user