Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
f99985dc8This file is pinned to the selected release. Download the original to open it in your design tool.
package com.marginallyclever.makelangelo.makeart;
import com.marginallyclever.makelangelo.makeart.imagefilter.ImageFilter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
/**
* TransformedImage is a {@link BufferedImage} with a transformation matrix on top.
* All sampling interactions are done in paper space coordinates and {@link TransformedImage} takes care of the rest.
* The original {@link BufferedImage} is not modified so there is no data loss. This also means one matrix transform
* per pixel sample, which is slow.
* @author Dan Royer
*/
public class TransformedImage {
private final BufferedImage sourceImage;
private float scaleX, scaleY;
private float translateX, translateY;
private final int componentCount;
private final double [] pixel;
private final WritableRaster raster;
class Box {
public double left, top, right, bottom;
public Box(double left, double top, double right, double bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public void transform() {
// transform the corners
left = getTransformedX(left);
bottom = getTransformedY(bottom);
right = getTransformedX(right);
top = getTransformedY(top);
// make sure left <= right
if(left > right) {
double temp = left;
left = right;
right = temp;
}
// make sure bottom <= top
if(bottom > top) {
double temp = bottom;
bottom = top;
top = temp;
}
// find the bounds of the image once, instead of inside the loops.
bottom = Math.clamp(bottom, 0, sourceImage.getHeight());
top = Math.clamp(top , 0, sourceImage.getHeight());
left = Math.clamp(left , 0, sourceImage.getWidth());
right = Math.clamp(right , 0, sourceImage.getWidth());
}
}
public TransformedImage(BufferedImage src) {
sourceImage = deepCopy(src);
translateX = -src.getWidth() / 2.0f;
translateY = -src.getHeight() / 2.0f;
scaleX = 1;
scaleY = 1;
componentCount = sourceImage.getColorModel().getNumComponents();
pixel = new double[componentCount];
raster = sourceImage.getRaster();
}
public TransformedImage(TransformedImage copy) {
sourceImage = deepCopy(copy.sourceImage);
translateX = copy.translateX;
translateY = copy.translateY;
scaleX = copy.scaleX;
scaleY = copy.scaleY;
componentCount = sourceImage.getColorModel().getNumComponents();
pixel = new double[componentCount];
raster = sourceImage.getRaster();
}
// https://stackoverflow.com/questions/3514158/how-do-you-clone-a-bufferedimage
protected BufferedImage deepCopy(BufferedImage bi) {
ColorModel cm = bi.getColorModel();
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
WritableRaster raster = bi.copyData(null);
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
/**
* Can the image be sampled at this location?
* @param x pre-transform x
* @param y pre-transform y
* @return true if the image can be sampled at this location
*/
public boolean canSampleAt(double x, double y) {
double sampleX = getTransformedX(x);
double sampleY = getTransformedY(y);
if (sampleX < 0 || sampleX >= sourceImage.getWidth ()) return false;
if (sampleY < 0 || sampleY >= sourceImage.getHeight()) return false;
return true;
}
public float getScaleX() {
return scaleX;
}
public float getScaleY() {
return scaleY;
}
public BufferedImage getSourceImage() {
return sourceImage;
}
private double getTransformedX(double x) {
return ((x / scaleX) - translateX);
}
public double getTransformedY(double y) {
return ((y / scaleY) - translateY);
}
/**
* Returns the greyscale intensity [0...255]
* @param cx center of the sample area
* @param cy center of the sample area
* @param radius radius of the sample area
* @return the greyscale intensity [0...255]
*/
public int sample(double cx, double cy, double radius) {
return sample(new Box(cx-radius,cy-radius,cx+radius,cy+radius));
}
/**
* Sample the image, taking into account fractions of pixels.
* @param box the area to sample
* @return greyscale intensity in this region. [0...255]
*/
private int sample(Box box) {
box.transform();
// now sample the entire area to average the intensity
int bBottom = (int)box.bottom;
int bTop = (int)box.top;
int bLeft = (int)box.left;
int bRight = (int)box.right;
double count = 0;
double sum = 0;
for(int y = bBottom; y < bTop; ++y) {
// sample whole pixels
for(int x = bLeft; x < bRight; ++x) {
raster.getPixel(x, y, pixel);
sum += pixel[0]; // in a greyscale image all components are the same value, so only test one.
count++;
}
}
if(count==0) return 255;
// average the intensity
double result = sum / count;
return (int) Math.clamp(result, 0, 255);
}
/**
* @param cx center of the sample area
* @param cy center of the sample area
* @param radius radius of the sample area
* @return the average color in this region.
*/
public Color sampleColor(double cx, double cy, double radius) {
return sampleColor(new Box(cx-radius,cy-radius,cx+radius,cy+radius));
}
/**
* Sample the image, taking into account fractions of pixels.
* @param box the area to sample
* @return the average color in this region.
*/
private Color sampleColor(Box box) {
box.transform();
// now sample the entire area to average the intensity
int count = 0;
var sum = new double[componentCount];
for(int y = (int)box.bottom; y <= box.top; ++y) {
for(int x = (int)box.left; x <= box.right; ++x) {
raster.getPixel(x, y, pixel);
for(int i=0;i<componentCount;++i) {
sum[i] += pixel[i];
}
count++;
}
}
if(count==0) return Color.WHITE;
// average the intensity
for(int i=0;i<componentCount;++i) {
int j = (int)(sum[i]/count);
sum[i] = Math.clamp(j, 0, 255);
}
return new Color(
(int)sum[0], // red
(int)sum[1], // green
(int)sum[2], // blue
componentCount > 3 ? (int)sum[3] : 255 // alpha
);
}
/**
* Attempt to sample a pixel of the source image, if the (x,y) coordinate is within the bounds of the
* @param x paper-space coordinates of the image
* @param y paper-space coordinates of the image
* @return 255 if the image cannot be sampled. The intensity of the color channel [0...255]
*/
public int sample1x1(double x, double y) {
if (canSampleAt(x, y)) {
return sample1x1Unchecked(x, y);
}
return 255;
}
/**
* Transforms x,y to the local space and samples the source image without a safety check.
* @param x paper-space coordinates of the image
* @param y paper-space coordinates of the image
* @return 255 if the image cannot be sampled. The intensity of the color channel [0...255]. the color channel is selected with
*/
public int sample1x1Unchecked(double x, double y) {
int sampleX = (int)getTransformedX(x);
int sampleY = (int)getTransformedY(y);
int c2 = sourceImage.getRGB(sampleX, sampleY);
return ImageFilter.decode32bit(c2) & 0xFF;
}
public void setScale(float x,float y) {
scaleX = x;
scaleY = y;
}
public void setTranslation(float x,float y) {
translateX = x;
translateY = y;
}
public void setRGB(float x, float y, int c) {
sourceImage.setRGB((int)getTransformedX(x), (int)getTransformedY(y), c);
}
}