Core now moved to common.

This commit is contained in:
Erlend Hamnaberg
2009-11-08 19:01:36 +01:00
parent e0a6c0a2bd
commit 7167a7a4ad
36 changed files with 36 additions and 223 deletions

View File

@@ -16,13 +16,20 @@
</description>
<dependencies>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>common-lang</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>common-io</artifactId>
</dependency>
<dependency>
<groupId>jmagick</groupId>
<artifactId>jmagick</artifactId>
<version>6.2.4</version>
<scope>provided</scope>
<optional>true</optional>
<artifactId>jmagick</artifactId>
<version>6.2.4</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.ImageProducer;
import java.awt.image.ImageConsumer;
import java.util.List;
import java.util.ArrayList;
/**
* AbstractImageSource
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java#1 $
*/
public abstract class AbstractImageSource implements ImageProducer {
private List<ImageConsumer> mConsumers = new ArrayList<ImageConsumer>();
protected int mWidth;
protected int mHeight;
protected int mXOff;
protected int mYOff;
// ImageProducer interface
public void addConsumer(ImageConsumer pConsumer) {
if (mConsumers.contains(pConsumer)) {
return;
}
mConsumers.add(pConsumer);
try {
initConsumer(pConsumer);
sendPixels(pConsumer);
if (isConsumer(pConsumer)) {
pConsumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
// Get rid of "sticky" consumers...
if (isConsumer(pConsumer)) {
pConsumer.imageComplete(ImageConsumer.IMAGEERROR);
removeConsumer(pConsumer);
}
}
}
catch (Exception e) {
e.printStackTrace();
if (isConsumer(pConsumer)) {
pConsumer.imageComplete(ImageConsumer.IMAGEERROR);
}
}
}
public void removeConsumer(ImageConsumer pConsumer) {
mConsumers.remove(pConsumer);
}
/**
* This implementation silently ignores this instruction. If pixeldata is
* not in TDLR order by default, subclasses must override this method.
*
* @param pConsumer the consumer that requested the resend
*
* @see ImageProducer#requestTopDownLeftRightResend(java.awt.image.ImageConsumer)
*/
public void requestTopDownLeftRightResend(ImageConsumer pConsumer) {
// ignore
}
public void startProduction(ImageConsumer pConsumer) {
addConsumer(pConsumer);
}
public boolean isConsumer(ImageConsumer pConsumer) {
return mConsumers.contains(pConsumer);
}
protected abstract void initConsumer(ImageConsumer pConsumer);
protected abstract void sendPixels(ImageConsumer pConsumer);
}

View File

@@ -0,0 +1,453 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
/**
* AreaAverageOp
*
* @author <a href="mailto:harald.kuhr@gmail.no">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java#2 $
*/
public class AreaAverageOp implements BufferedImageOp, RasterOp {
final private int mWidth;
final private int mHeight;
private Rectangle mSourceRegion;
public AreaAverageOp(final int pWidth, final int pHeight) {
mWidth = pWidth;
mHeight = pHeight;
}
public Rectangle getSourceRegion() {
if (mSourceRegion == null) {
return null;
}
return new Rectangle(mSourceRegion);
}
public void setSourceRegion(final Rectangle pSourceRegion) {
if (pSourceRegion == null) {
mSourceRegion = null;
}
else {
if (mSourceRegion == null) {
mSourceRegion = new Rectangle(pSourceRegion);
}
else {
mSourceRegion.setBounds(pSourceRegion);
}
}
}
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
BufferedImage result = dest != null ? dest : createCompatibleDestImage(src, null);
// TODO: src and dest can't be the same
// TODO: Do some type checking here..
// Should work with
// * all BYTE types, unless sub-byte packed rasters/IndexColorModel
// * all INT types (even custom, as long as they use 8bit/componnet)
// * all USHORT types (even custom)
// TODO: Also check if the images are really compatible!?
long start = System.currentTimeMillis();
// Straight-forward version
//Image scaled = src.getScaledInstance(mWidth, mHeight, Image.SCALE_AREA_AVERAGING);
//ImageUtil.drawOnto(result, scaled);
//result = new BufferedImageFactory(scaled).getBufferedImage();
/*
// Try: Use bilinear/bicubic and half the image down until it's less than
// twice as big, then use bicubic for the last step?
BufferedImage temp = null;
AffineTransform xform = null;
int w = src.getWidth();
int h = src.getHeight();
while (w / 2 > mWidth && h / 2 > mHeight) {
w /= 2;
h /= 2;
if (temp == null) {
xform = AffineTransform.getScaleInstance(.5, .5);
ColorModel cm = src.getColorModel();
temp = new BufferedImage(cm,
ImageUtil.createCompatibleWritableRaster(src, cm, w, h),
cm.isAlphaPremultiplied(), null);
resample(src, temp, xform);
}
else {
resample(temp, temp, xform);
}
System.out.println("w: " + w);
System.out.println("h: " + h);
}
if (temp != null) {
src = temp.getSubimage(0, 0, w, h);
}
resample(src, result, AffineTransform.getScaleInstance(mWidth / (double) w, mHeight / (double) h));
*/
// The real version
filterImpl(src.getRaster(), result.getRaster());
long time = System.currentTimeMillis() - start;
System.out.println("time: " + time);
return result;
}
private void resample(final BufferedImage pSrc, final BufferedImage pDest, final AffineTransform pXform) {
Graphics2D d = pDest.createGraphics();
d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
try {
d.drawImage(pSrc, pXform, null);
}
finally {
d.dispose();
}
}
public WritableRaster filter(Raster src, WritableRaster dest) {
WritableRaster result = dest != null ? dest : createCompatibleDestRaster(src);
return filterImpl(src, result);
}
private WritableRaster filterImpl(Raster src, WritableRaster dest) {
//System.out.println("src: " + src);
//System.out.println("dest: " + dest);
if (mSourceRegion != null) {
int cx = mSourceRegion.x;
int cy = mSourceRegion.y;
int cw = mSourceRegion.width;
int ch = mSourceRegion.height;
boolean same = src == dest;
dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null);
src = same ? dest : src.createChild(cx, cy, cw, ch, 0, 0, null);
//System.out.println("src: " + src);
//System.out.println("dest: " + dest);
}
final int width = src.getWidth();
final int height = src.getHeight();
// TODO: This don't work too well..
// The thing is that the step length and the scan length will vary, for
// non-even (1/2, 1/4, 1/8 etc) resampling
int widthSteps = (width + mWidth - 1) / mWidth;
int heightSteps = (height + mHeight - 1) / mHeight;
final boolean oddX = width % mWidth != 0;
final boolean oddY = height % mHeight != 0;
final int dataElements = src.getNumDataElements();
final int bands = src.getNumBands();
final int dataType = src.getTransferType();
Object data = null;
int scanW;
int scanH;
// TYPE_USHORT setup
int[] bitMasks = null;
int[] bitOffsets = null;
if (src.getTransferType() == DataBuffer.TYPE_USHORT) {
if (src.getSampleModel() instanceof SinglePixelPackedSampleModel) {
// DIRECT
SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel) src.getSampleModel();
bitMasks = sampleModel.getBitMasks();
bitOffsets = sampleModel.getBitOffsets();
}
else {
// GRAY
bitMasks = new int[]{0xffff};
bitOffsets = new int[]{0};
}
}
for (int y = 0; y < mHeight; y++) {
if (!oddY || y < mHeight) {
scanH = heightSteps;
}
else {
scanH = height - (y * heightSteps);
}
for (int x = 0; x < mWidth; x++) {
if (!oddX || x < mWidth) {
scanW = widthSteps;
}
else {
scanW = width - (x * widthSteps);
}
final int pixelCount = scanW * scanH;
final int pixelLength = pixelCount * dataElements;
try {
data = src.getDataElements(x * widthSteps, y * heightSteps, scanW, scanH, data);
}
catch (IndexOutOfBoundsException e) {
// TODO: FixMe!
// The bug is in the steps...
//System.err.println("x: " + x);
//System.err.println("y: " + y);
//System.err.println("widthSteps: " + widthSteps);
//System.err.println("heightSteps: " + heightSteps);
//System.err.println("scanW: " + scanW);
//System.err.println("scanH: " + scanH);
//
//System.err.println("width: " + width);
//System.err.println("height: " + height);
//System.err.println("mWidth: " + mWidth);
//System.err.println("mHeight: " + mHeight);
//
//e.printStackTrace();
continue;
}
// TODO: Might need more channels... Use an array?
// NOTE: These are not neccessarily ARGB..
double valueA = 0.0;
double valueR = 0.0;
double valueG = 0.0;
double valueB = 0.0;
switch (dataType) {
case DataBuffer.TYPE_BYTE:
// TODO: Doesn't hold for index color models...
// For index color, the best bet is probably convert to
// true color, then convert back to the same index color
// model
byte[] bytePixels = (byte[]) data;
for (int i = 0; i < pixelLength; i += dataElements) {
valueA += bytePixels[i] & 0xff;
if (bands > 1) {
valueR += bytePixels[i + 1] & 0xff;
valueG += bytePixels[i + 2] & 0xff;
if (bands > 3) {
valueB += bytePixels[i + 3] & 0xff;
}
}
}
// Average
valueA /= pixelCount;
if (bands > 1) {
valueR /= pixelCount;
valueG /= pixelCount;
if (bands > 3) {
valueB /= pixelCount;
}
}
//for (int i = 0; i < pixelLength; i += dataElements) {
bytePixels[0] = (byte) clamp((int) valueA);
if (bands > 1) {
bytePixels[1] = (byte) clamp((int) valueR);
bytePixels[2] = (byte) clamp((int) valueG);
if (bands > 3) {
bytePixels[3] = (byte) clamp((int) valueB);
}
}
//}
break;
case DataBuffer.TYPE_INT:
int[] intPixels = (int[]) data;
// TODO: Rewrite to use bit offsets and masks from
// color model (see TYPE_USHORT) in case of a non-
// 888 or 8888 colormodel?
for (int i = 0; i < pixelLength; i += dataElements) {
valueA += (intPixels[i] & 0xff000000) >> 24;
valueR += (intPixels[i] & 0xff0000) >> 16;
valueG += (intPixels[i] & 0xff00) >> 8;
valueB += (intPixels[i] & 0xff);
}
// Average
valueA /= pixelCount;
valueR /= pixelCount;
valueG /= pixelCount;
valueB /= pixelCount;
//for (int i = 0; i < pixelLength; i += dataElements) {
intPixels[0] = clamp((int) valueA) << 24;
intPixels[0] |= clamp((int) valueR) << 16;
intPixels[0] |= clamp((int) valueG) << 8;
intPixels[0] |= clamp((int) valueB);
//}
break;
case DataBuffer.TYPE_USHORT:
if (bitMasks != null) {
short[] shortPixels = (short[]) data;
for (int i = 0; i < pixelLength; i += dataElements)
{
valueA += (shortPixels[i] & bitMasks[0]) >> bitOffsets[0];
if (bitMasks.length > 1) {
valueR += (shortPixels[i] & bitMasks[1]) >> bitOffsets[1];
valueG += (shortPixels[i] & bitMasks[2]) >> bitOffsets[2];
if (bitMasks.length > 3) {
valueB += (shortPixels[i] & bitMasks[3]) >> bitOffsets[3];
}
}
}
// Average
valueA /= pixelCount;
valueR /= pixelCount;
valueG /= pixelCount;
valueB /= pixelCount;
//for (int i = 0; i < pixelLength; i += dataElements) {
shortPixels[0] = (short) (((int) valueA << bitOffsets[0]) & bitMasks[0]);
if (bitMasks.length > 1) {
shortPixels[0] |= (short) (((int) valueR << bitOffsets[1]) & bitMasks[1]);
shortPixels[0] |= (short) (((int) valueG << bitOffsets[2]) & bitMasks[2]);
if (bitMasks.length > 3) {
shortPixels[0] |= (short) (((int) valueB << bitOffsets[3]) & bitMasks[3]);
}
}
//}
break;
}
default:
throw new IllegalArgumentException("TransferType not supported: " + dataType);
}
dest.setDataElements(x, y, 1, 1, data);
}
}
return dest;
}
private static int clamp(final int pValue) {
return pValue > 255 ? 255 : pValue;
}
public RenderingHints getRenderingHints() {
return null;
}
// TODO: Refactor boilerplate to AbstractBufferedImageOp or use a delegate?
// Delegate is maybe better as we won't always implement both BIOp and RasterOP
// (but are there ever any time we want to implemnet RasterOp and not BIOp?)
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
ColorModel cm = destCM != null ? destCM : src.getColorModel();
return new BufferedImage(cm,
ImageUtil.createCompatibleWritableRaster(src, cm, mWidth, mHeight),
cm.isAlphaPremultiplied(), null);
}
public WritableRaster createCompatibleDestRaster(Raster src) {
return src.createCompatibleWritableRaster(mWidth, mHeight);
}
public Rectangle2D getBounds2D(Raster src) {
return new Rectangle(mWidth, mHeight);
}
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle(mWidth, mHeight);
}
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
// TODO: This is wrong!
if (dstPt == null) {
if (srcPt instanceof Point2D.Double) {
dstPt = new Point2D.Double();
}
else {
dstPt = new Point2D.Float();
}
}
dstPt.setLocation(srcPt);
return dstPt;
}
public static void main(String[] pArgs) throws IOException {
BufferedImage image = ImageIO.read(new File("2006-Lamborghini-Gallardo-Spyder-Y-T-1600x1200.png"));
//BufferedImage image = ImageIO.read(new File("focus-rs.jpg"));
//BufferedImage image = ImageIO.read(new File("blauesglas_16_bitmask444.bmp"));
//image = ImageUtil.toBuffered(image, BufferedImage.TYPE_USHORT_GRAY);
for (int i = 0; i < 100; i++) {
//new PixelizeOp(10).filter(image, null);
//new AffineTransformOp(AffineTransform.getScaleInstance(.1, .1), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
//ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
//new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_BOX).filter(image, null);
//new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_QUADRATIC).filter(image, null);
//new AreaAverageOp(image.getWidth() / 10, image.getHeight() / 10).filter(image, null);
}
long start = System.currentTimeMillis();
//PixelizeOp pixelizer = new PixelizeOp(image.getWidth() / 10, 1);
//pixelizer.setSourceRegion(new Rectangle(0, 2 * image.getHeight() / 3, image.getWidth(), image.getHeight() / 4));
//PixelizeOp pixelizer = new PixelizeOp(4);
//image = pixelizer.filter(image, image); // Filter in place, that's cool
//image = new AffineTransformOp(AffineTransform.getScaleInstance(.25, .25), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
//image = ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
//image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_BOX).filter(image, null);
//image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_QUADRATIC).filter(image, null);
//image = new AreaAverageOp(image.getWidth() / 7, image.getHeight() / 4).filter(image, null);
image = new AreaAverageOp(500, 600).filter(image, null);
//image = new ResampleOp(500, 600, ResampleOp.FILTER_BOX).filter(image, null);
long time = System.currentTimeMillis() - start;
System.out.println("time: " + time + " ms");
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(new JScrollPane(new JLabel(new BufferedImageIcon(image))));
frame.pack();
frame.setVisible(true);
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.RGBImageFilter;
/**
* Adjusts the contrast and brightness of an image.
* <p/>
* For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}.
* A value of {@code 0.0} means no change.
* Negative values will make the pixels darker.
* Maximum negative value ({@code -2}) will make all filtered pixels black.
* Positive values will make the pixels brighter.
* Maximum positive value ({@code 2}) will make all filtered pixels white.
* <p/>
* For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}.
* A value of {@code 0.0} means no change.
* Negative values will reduce contrast.
* Maximum negative value ({@code -1}) will make all filtered pixels grey
* (no contrast).
* Positive values will increase contrast.
* Maximum positive value ({@code 1}) will make all filtered pixels primary
* colors (either black, white, cyan, magenta, yellow, red, blue or green).
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java#1 $
*
* @todo consider doing something similar to http://archives.java.sun.com/cgi-bin/wa?A2=ind0302&L=jai-interest&F=&S=&P=15947
*/
public class BrightnessContrastFilter extends RGBImageFilter {
// This filter can filter IndexColorModel, as it is does not depend on
// the pixels' location
{
canFilterIndexColorModel = true;
}
// Use a precalculated lookup table for performace
private int[] mLUT = null;
/**
* Creates a BrightnessContrastFilter with default values
* ({@code brightness=0.3, contrast=0.3}).
* <p/>
* This will slightly increase both brightness and contrast.
*/
public BrightnessContrastFilter() {
this(0.3f, 0.3f);
}
/**
* Creates a BrightnessContrastFilter with the given values for brightness
* and contrast.
* <p/>
* For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}.
* A value of {@code 0.0} means no change.
* Negative values will make the pixels darker.
* Maximum negative value ({@code -2}) will make all filtered pixels black.
* Positive values will make the pixels brighter.
* Maximum positive value ({@code 2}) will make all filtered pixels white.
* <p/>
* For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}.
* A value of {@code 0.0} means no change.
* Negative values will reduce contrast.
* Maximum negative value ({@code -1}) will make all filtered pixels grey
* (no contrast).
* Positive values will increase contrast.
* Maximum positive value ({@code 1}) will make all filtered pixels primary
* colors (either black, white, cyan, magenta, yellow, red, blue or green).
*
* @param pBrightness adjust the brightness of the image, in the range
* {@code -2.0,..,0.0,..,2.0}.
* @param pContrast adjust the contrast of the image, in the range
* {@code -1.0,..,0.0,..,1.0}.
*/
public BrightnessContrastFilter(float pBrightness, float pContrast) {
mLUT = createLUT(pBrightness, pContrast);
}
private static int[] createLUT(float pBrightness, float pContrast) {
int[] lut = new int[256];
// Hmmm.. This approximates Photoshop values.. Not good though..
double contrast = pContrast > 0 ? Math.pow(pContrast, 7.0) * 127.0 : pContrast;
// Convert range [-1,..,0,..,1] -> [0,..,1,..,2]
double brightness = pBrightness + 1.0;
for (int i = 0; i < 256; i++) {
lut[i] = clamp((int) (127.5 * brightness + (i - 127) * (contrast + 1.0)));
}
// Special case, to ensure only primary colors for max contrast
if (pContrast == 1f) {
lut[127] = lut[126];
}
return lut;
}
private static int clamp(int i) {
if (i < 0) {
return 0;
}
if (i > 255) {
return 255;
}
return i;
}
/**
* Filters one pixel, adjusting brightness and contrast according to this
* filter.
*
* @param pX x
* @param pY y
* @param pARGB pixel value in default color space
*
* @return the filtered pixel value in the default color space
*/
public int filterRGB(int pX, int pY, int pARGB) {
// Get color components
int r = pARGB >> 16 & 0xFF;
int g = pARGB >> 8 & 0xFF;
int b = pARGB & 0xFF;
// Scale to new contrast
r = mLUT[r];
g = mLUT[g];
b = mLUT[b];
// Return ARGB pixel, leave transparency as is
return (pARGB & 0xFF000000) | (r << 16) | (g << 8) | b;
}
}

View File

@@ -0,0 +1,543 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.*;
import java.awt.image.*;
import java.util.*;
import java.util.List;
import java.lang.reflect.Array;
/**
* A faster, lighter and easier way to convert an {@code Image} to a
* {@code BufferedImage} than using a {@code PixelGrabber}.
* Clients may provide progress listeners to monitor conversion progress.
* <p/>
* Supports source image subsampling and source region extraction.
* Supports source images with 16 bit {@link ColorModel} and
* {@link DataBuffer#TYPE_USHORT} transfer type, without converting to
* 32 bit/TYPE_INT.
* <p/>
* NOTE: Does not support images with more than one {@code ColorModel} or
* different types of pixel data. This is not very common.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java#1 $
*/
public final class BufferedImageFactory {
private List<ProgressListener> mListeners;
private int mPercentageDone;
private ImageProducer mProducer;
private boolean mError;
private boolean mFetching;
private boolean mReadColorModelOnly;
private int mX = 0;
private int mY = 0;
private int mWidth = -1;
private int mHeight = -1;
private int mXSub = 1;
private int mYSub = 1;
private int mOffset;
private int mScanSize;
private ColorModel mSourceColorModel;
private Hashtable mSourceProperties; // ImageConsumer API dictates Hashtable
private Object mSourcePixels;
private BufferedImage mBuffered;
private ColorModel mColorModel;
// NOTE: Just to not expose the inheritance
private final Consumer mConsumer = new Consumer();
/**
* Creates a {@code BufferedImageFactory}.
* @param pSource the source image
*/
public BufferedImageFactory(Image pSource) {
this(pSource.getSource());
}
/**
* Creates a {@code BufferedImageFactory}.
* @param pSource the source image producer
*/
public BufferedImageFactory(ImageProducer pSource) {
mProducer = pSource;
}
/**
* Returns the {@code BufferedImage} extracted from the given
* {@code ImageSource}. Multiple requests will return the same image.
*
* @return the {@code BufferedImage}
*
* @throws ImageConversionException if the given {@code ImageSource} cannot
* be converted for some reason.
*/
public BufferedImage getBufferedImage() throws ImageConversionException {
doFetch(false);
return mBuffered;
}
/**
* Returns the {@code ColorModel} extracted from the
* given {@code ImageSource}. Multiple requests will return the same model.
*
* @return the {@code ColorModel}
*
* @throws ImageConversionException if the given {@code ImageSource} cannot
* be converted for some reason.
*/
public ColorModel getColorModel() throws ImageConversionException {
doFetch(true);
return mBuffered != null ? mBuffered.getColorModel() : mColorModel;
}
/**
* Frees resources used by this {@code BufferedImageFactory}.
*/
public void dispose() {
freeResources();
mBuffered = null;
mColorModel = null;
}
/**
* Aborts the image prodcution.
*/
public void abort() {
mConsumer.imageComplete(ImageConsumer.IMAGEABORTED);
}
/**
* Sets the source region (AOI) for the new image.
*
* @param pRect the source region
*/
public void setSourceRegion(Rectangle pRect) {
// Refetch everything, if region changed
if (mX != pRect.x || mY != pRect.y || mWidth != pRect.width || mHeight != pRect.height) {
dispose();
}
mX = pRect.x;
mY = pRect.y;
mWidth = pRect.width;
mHeight = pRect.height;
}
/**
* Sets the source subsampling for the new image.
*
* @param pXSub horisontal subsampling factor
* @param pYSub vertical subsampling factor
*/
public void setSourceSubsampling(int pXSub, int pYSub) {
// Refetch everything, if subsampling changed
if (mXSub != pXSub || mYSub != pYSub) {
dispose();
}
if (pXSub > 1) {
mXSub = pXSub;
}
if (pYSub > 1) {
mYSub = pYSub;
}
}
private synchronized void doFetch(boolean pColorModelOnly) throws ImageConversionException {
if (!mFetching && (!pColorModelOnly && mBuffered == null || mBuffered == null && mSourceColorModel == null)) {
// NOTE: Subsampling is only applied if extracting full image
if (!pColorModelOnly && (mXSub > 1 || mYSub > 1)) {
// If only sampling a region, the region must be scaled too
if (mWidth > 0 && mHeight > 0) {
mWidth = (mWidth + mXSub - 1) / mXSub;
mHeight = (mHeight + mYSub - 1) / mYSub;
mX = (mX + mXSub - 1) / mXSub;
mY = (mY + mYSub - 1) / mYSub;
}
mProducer = new FilteredImageSource(mProducer, new SubsamplingFilter(mXSub, mYSub));
}
// Start fetching
mFetching = true;
mReadColorModelOnly = pColorModelOnly;
mProducer.startProduction(mConsumer); // Note: If single-thread (synchronous), this call will block
// Wait until the producer wakes us up, by calling imageComplete
while (mFetching) {
try {
wait();
}
catch (InterruptedException e) {
throw new ImageConversionException("Image conversion aborted: " + e.getMessage(), e);
}
}
if (mError) {
throw new ImageConversionException("Image conversion failed: ImageConsumer.IMAGEERROR.");
}
if (pColorModelOnly) {
createColorModel();
}
else {
createBuffered();
}
}
}
private void createColorModel() {
mColorModel = mSourceColorModel;
// Clean up, in case any objects are copied/cloned, so we can free resources
freeResources();
}
private void createBuffered() {
if (mWidth > 0 && mHeight > 0) {
if (mSourceColorModel != null && mSourcePixels != null) {
// TODO: Fix pixel size / color model problem
WritableRaster raster = ImageUtil.createRaster(mWidth, mHeight, mSourcePixels, mSourceColorModel);
mBuffered = new BufferedImage(mSourceColorModel, raster, mSourceColorModel.isAlphaPremultiplied(), mSourceProperties);
}
else {
mBuffered = ImageUtil.createClear(mWidth, mHeight, null);
}
}
// Clean up, in case any objects are copied/cloned, so we can free resources
freeResources();
}
private void freeResources() {
mSourceColorModel = null;
mSourcePixels = null;
mSourceProperties = null;
}
private void processProgress(int mScanline) {
if (mListeners != null) {
int percent = 100 * mScanline / mHeight;
//System.out.println("Progress: " + percent + "%");
if (percent > mPercentageDone) {
mPercentageDone = percent;
// TODO: Fix concurrent modification if a listener removes itself...
for (ProgressListener listener : mListeners) {
listener.progress(this, percent);
}
}
}
}
/**
* Adds a progress listener to this factory.
*
* @param pListener the progress listener
*/
public void addProgressListener(ProgressListener pListener) {
if (mListeners == null) {
mListeners = new ArrayList<ProgressListener>();
}
mListeners.add(pListener);
}
/**
* Removes a progress listener from this factory.
*
* @param pListener the progress listener
*/
public void removeProgressListener(ProgressListener pListener) {
if (mListeners == null) {
return;
}
mListeners.remove(pListener);
}
/**
* Removes all progress listeners from this factory.
*/
public void removeAllProgressListeners() {
if (mListeners != null) {
mListeners.clear();
}
}
/**
* Converts an array of {@code int} pixles to an array of {@code short}
* pixels. The conversion is done, by masking out the
* <em>higher 16 bits</em> of the {@code int}.
*
* For eny given {@code int}, the {@code short} value is computed as
* follows:
* <blockquote>{@code
* short value = (short) (intValue & 0x0000ffff);
* }</blockquote>
*
* @param pPixels the pixel data to convert
* @return an array of {@code short}s, same lenght as {@code pPixels}
*/
private static short[] toShortPixels(int[] pPixels) {
short[] pixels = new short[pPixels.length];
for (int i = 0; i < pixels.length; i++) {
pixels[i] = (short) (pPixels[i] & 0xffff);
}
return pixels;
}
/**
* This interface allows clients of a {@code BufferedImageFactory} to
* receive notifications of decoding progress.
*
* @see BufferedImageFactory#addProgressListener
* @see BufferedImageFactory#removeProgressListener
*/
public static interface ProgressListener extends EventListener {
/**
* Reports progress to this listener.
* Invoked by the {@code BufferedImageFactory} to report progress in
* the image decoding.
*
* @param pFactory the factory reporting the progress
* @param pPercentage the perccentage of progress
*/
void progress(BufferedImageFactory pFactory, float pPercentage);
}
private class Consumer implements ImageConsumer {
/**
* Implementation of all setPixels methods.
* Note that this implementation assumes that all invocations for one
* image uses the same color model, and that the pixel data has the
* same type.
*
* @param pX x coordinate of pixel data region
* @param pY y coordinate of pixel data region
* @param pWidth width of pixel data region
* @param pHeight height of pixel data region
* @param pModel the color model of the pixel data
* @param pPixels the pixel data array
* @param pOffset the offset into the pixel data array
* @param pScanSize the scan size of the pixel data array
*/
private void setPixelsImpl(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, Object pPixels, int pOffset, int pScanSize) {
setColorModelOnce(pModel);
if (pPixels == null) {
return;
}
//System.out.println("Setting " + pPixels.getClass().getComponentType() + " pixels: " + Array.getLength(pPixels));
// Allocate array if neccessary
if (mSourcePixels == null) {
/*
System.out.println("ColorModel: " + pModel);
System.out.println("Scansize: " + pScanSize + " TrasferType: " + ImageUtil.getTransferType(pModel));
System.out.println("Creating " + pPixels.getClass().getComponentType() + " array of length " + (mWidth * mHeight));
*/
// Allocate a suitable source pixel array
// TODO: Should take pixel "width" into consideration, for byte packed rasters?!
// OR... Is anything but single-pixel models really supported by the API?
mSourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), mWidth * mHeight);
mScanSize = mWidth;
mOffset = 0;
}
else if (mSourcePixels.getClass() != pPixels.getClass()) {
throw new IllegalStateException("Only one pixel type allowed");
}
// AOI stuff
if (pY < mY) {
int diff = mY - pY;
if (diff >= pHeight) {
return;
}
pOffset += pScanSize * diff;
pY += diff;
pHeight -= diff;
}
if (pY + pHeight > mY + mHeight) {
pHeight = (mY + mHeight) - pY;
if (pHeight <= 0) {
return;
}
}
if (pX < mX) {
int diff = mX - pX;
if (diff >= pWidth) {
return;
}
pOffset += diff;
pX += diff;
pWidth -= diff;
}
if (pX + pWidth > mX + mWidth) {
pWidth = (mX + mWidth) - pX;
if (pWidth <= 0) {
return;
}
}
int dstOffset = mOffset + (pY - mY) * mScanSize + (pX - mX);
// Do the pixel copying
for (int i = pHeight; i > 0; i--) {
System.arraycopy(pPixels, pOffset, mSourcePixels, dstOffset, pWidth);
pOffset += pScanSize;
dstOffset += mScanSize;
}
processProgress(pY + pHeight);
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, short[] pPixels, int pOffset, int pScanSize) {
setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize);
}
private void setColorModelOnce(ColorModel pModel) {
// NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it
// first passes the original colormodel through in setColorModel, then
// later replaces it with the default RGB in the first setPixels call
// (this is probably allowed according to the spec, but it's a waste of
// time and space).
if (mSourceColorModel != pModel) {
if (/*mSourceColorModel == null ||*/ mSourcePixels == null) {
mSourceColorModel = pModel;
}
else {
throw new IllegalStateException("Change of ColorModel after pixel delivery not supported");
}
}
// If color model is all we ask for, stop now
if (mReadColorModelOnly) {
mConsumer.imageComplete(ImageConsumer.IMAGEABORTED);
}
}
/** {@code ImageConsumer} implementation, do not invoke */
public void imageComplete(int pStatus) {
mFetching = false;
if (mProducer != null) {
mProducer.removeConsumer(this);
}
switch (pStatus) {
case IMAGEERROR:
new Error().printStackTrace();
mError = true;
break;
}
synchronized (BufferedImageFactory.this) {
BufferedImageFactory.this.notifyAll();
}
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setColorModel(ColorModel pModel) {
//System.out.println("SetColorModel: " + pModel);
setColorModelOnce(pModel);
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setDimensions(int pWidth, int pHeight) {
//System.out.println("Setting dimensions: " + pWidth + ", " + pHeight);
if (mWidth < 0) {
mWidth = pWidth - mX;
}
if (mHeight < 0) {
mHeight = pHeight - mY;
}
// Hmm.. Special case, but is it a good idea?
if (mWidth <= 0 || mHeight <= 0) {
imageComplete(STATICIMAGEDONE);
}
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setHints(int pHintflags) {
// ignore
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) {
/*if (pModel.getPixelSize() < 8) {
// Byte packed
setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize);
}
/*
else if (pModel.getPixelSize() > 8) {
// Byte interleaved
setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize);
}
*/
//else {
// Default, pixelSize == 8, one byte pr pixel
setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize);
//}
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) {
if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) {
// NOTE: Workaround for limitation in ImageConsumer API
// Convert int[] to short[], to be compatible with the ColorModel
setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize);
}
else {
setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, pPixels, pOffset, pScanSize);
}
}
/** {@code ImageConsumer} implementation, do not invoke directly */
public void setProperties(Hashtable pProperties) {
mSourceProperties = pProperties;
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import javax.swing.Icon;
import java.awt.image.BufferedImage;
import java.awt.*;
import java.awt.geom.AffineTransform;
/**
* An {@code Icon} implementation backed by a {@code BufferedImage}.
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java#2 $
*/
public class BufferedImageIcon implements Icon {
private final BufferedImage mImage;
private int mWidth;
private int mHeight;
private final boolean mFast;
public BufferedImageIcon(BufferedImage pImage) {
this(pImage, pImage.getWidth(), pImage.getHeight());
}
public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) {
if (pImage == null) {
throw new IllegalArgumentException("image == null");
}
if (pWidth <= 0 || pHeight <= 0) {
throw new IllegalArgumentException("Icon size must be positive");
}
mImage = pImage;
mWidth = pWidth;
mHeight = pHeight;
mFast = pImage.getWidth() == mWidth && pImage.getHeight() == mHeight;
}
public int getIconHeight() {
return mHeight;
}
public int getIconWidth() {
return mWidth;
}
public void paintIcon(Component c, Graphics g, int x, int y) {
if (mFast || !(g instanceof Graphics2D)) {
//System.out.println("Scaling fast");
g.drawImage(mImage, x, y, mWidth, mHeight, null);
}
else {
//System.out.println("Scaling using interpolation");
Graphics2D g2 = (Graphics2D) g;
AffineTransform xform = AffineTransform.getTranslateInstance(x, y);
xform.scale(mWidth / (double) mImage.getWidth(), mHeight / (double) mImage.getHeight());
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(mImage, xform, null);
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
/**
* This class implements a convolution from the source
* to the destination.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java#1 $
*
* @see java.awt.image.ConvolveOp
*/
public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp {
/**
* Alias for {@link ConvolveOp#EDGE_ZERO_FILL}.
* @see #EDGE_REFLECT
*/
public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL;
/**
* Alias for {@link ConvolveOp#EDGE_NO_OP}.
* @see #EDGE_REFLECT
*/
public static final int EDGE_NO_OP = ConvolveOp.EDGE_NO_OP;
/**
* Adds a border to the image while convolving. The border will reflect the
* edges of the original image. This is usually a good default.
* Note that while this mode typically provides better quality than the
* standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
* at the expense of higher memory consumption and considerable more computation.
*/
public static final int EDGE_REFLECT = 2; // as JAI BORDER_REFLECT
/**
* Adds a border to the image while convolving. The border will wrap the
* edges of the original image. This is usually the best choice for tiles.
* Note that while this mode typically provides better quality than the
* standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
* at the expense of higher memory consumption and considerable more computation.
* @see #EDGE_REFLECT
*/
public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP
private final Kernel mKernel;
private final int mEdgeCondition;
private final ConvolveOp mConvolve;
public ConvolveWithEdgeOp(final Kernel pKernel, final int pEdgeCondition, final RenderingHints pHints) {
// Create convolution operation
int edge;
switch (pEdgeCondition) {
case EDGE_REFLECT:
case EDGE_WRAP:
edge = ConvolveOp.EDGE_NO_OP;
break;
default:
edge = pEdgeCondition;
break;
}
mKernel = pKernel;
mEdgeCondition = pEdgeCondition;
mConvolve = new ConvolveOp(pKernel, edge, pHints);
}
public ConvolveWithEdgeOp(final Kernel pKernel) {
this(pKernel, EDGE_ZERO_FILL, null);
}
public BufferedImage filter(BufferedImage pSource, BufferedImage pDestination) {
if (pSource == null) {
throw new NullPointerException("source image is null");
}
if (pSource == pDestination) {
throw new IllegalArgumentException("source image cannot be the same as the destination image");
}
int borderX = mKernel.getWidth() / 2;
int borderY = mKernel.getHeight() / 2;
BufferedImage original = addBorder(pSource, borderX, borderY);
// Workaround for what seems to be a Java2D bug:
// ConvolveOp needs explicit destination image type for some "uncommon"
// image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO
// normally returns for color JPEGs... :-/
BufferedImage destination = pDestination;
if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) {
destination = ImageUtil.createBuffered(
pSource.getWidth(), pSource.getHeight(),
pSource.getType(), pSource.getColorModel().getTransparency(),
null
);
}
// Do the filtering (if destination is null, a new image will be created)
destination = mConvolve.filter(original, destination);
if (pSource != original) {
// Remove the border
destination = destination.getSubimage(borderX, borderY, pSource.getWidth(), pSource.getHeight());
}
return destination;
}
private BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY) {
if ((mEdgeCondition & 2) == 0) {
return pOriginal;
}
// TODO: Might be faster if we could clone raster and stretch it...
int w = pOriginal.getWidth();
int h = pOriginal.getHeight();
ColorModel cm = pOriginal.getColorModel();
WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY);
BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
Graphics2D g = bordered.createGraphics();
try {
g.setComposite(AlphaComposite.Src);
g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
// Draw original in center
g.drawImage(pOriginal, pBorderX, pBorderY, null);
// TODO: I guess we need the top/left etc, if the corner pixels are covered by the kernel
switch (mEdgeCondition) {
case EDGE_REFLECT:
// Top/left (empty)
g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center
// Top/right (empty)
g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left
// Center/center (already drawn)
g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right
// Bottom/left (empty)
g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center
// Bottom/right (empty)
break;
case EDGE_WRAP:
g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left
g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center
g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right
g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left
// Center/center (already drawn)
g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right
g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left
g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center
g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right
break;
default:
throw new IllegalArgumentException("Illegal edge operation " + mEdgeCondition);
}
}
finally {
g.dispose();
}
return bordered;
}
/**
* Returns the edge condition.
* @return the edge condition of this {@code ConvolveOp}.
* @see #EDGE_NO_OP
* @see #EDGE_ZERO_FILL
* @see #EDGE_REFLECT
* @see #EDGE_WRAP
*/
public int getEdgeCondition() {
return mEdgeCondition;
}
public WritableRaster filter(final Raster pSource, final WritableRaster pDestination) {
return mConvolve.filter(pSource, pDestination);
}
public BufferedImage createCompatibleDestImage(final BufferedImage pSource, final ColorModel pDesinationColorModel) {
return mConvolve.createCompatibleDestImage(pSource, pDesinationColorModel);
}
public WritableRaster createCompatibleDestRaster(final Raster pSource) {
return mConvolve.createCompatibleDestRaster(pSource);
}
public Rectangle2D getBounds2D(final BufferedImage pSource) {
return mConvolve.getBounds2D(pSource);
}
public Rectangle2D getBounds2D(final Raster pSource) {
return mConvolve.getBounds2D(pSource);
}
public Point2D getPoint2D(final Point2D pSourcePoint, final Point2D pDestinationPoint) {
return mConvolve.getPoint2D(pSourcePoint, pDestinationPoint);
}
public RenderingHints getRenderingHints() {
return mConvolve.getRenderingHints();
}
public Kernel getKernel() {
return mConvolve.getKernel();
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.awt.image.WritableRaster;
/**
* This BufferedImageOp simply copies pixels, converting to a
* {@code IndexColorModel}.
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java#1 $
*
*/
public class CopyDither implements BufferedImageOp, RasterOp {
protected IndexColorModel mIndexColorModel = null;
/**
* Creates a {@code CopyDither}, using the given
* {@code IndexColorModel} for dithering into.
*
* @param pICM an IndexColorModel.
*/
public CopyDither(IndexColorModel pICM) {
// Store colormodel
mIndexColorModel = pICM;
}
/**
* Creates a {@code CopyDither}, with no fixed
* {@code IndexColorModel}. The colormodel will be generated for each
* filtering, unless the dest image allready has an
* {@code IndexColorModel}.
*/
public CopyDither() {
}
/**
* Creates a compatible {@code BufferedImage} to dither into.
* Only {@code IndexColorModel} allowed.
*
* @return a compatible {@code BufferedImage}
*
* @throws ImageFilterException if {@code pDestCM} is not {@code null} or
* an instance of {@code IndexColorModel}.
*/
public final BufferedImage createCompatibleDestImage(BufferedImage pSource,
ColorModel pDestCM) {
if (pDestCM == null) {
return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
mIndexColorModel);
}
else if (pDestCM instanceof IndexColorModel) {
return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
(IndexColorModel) pDestCM);
}
else {
throw new ImageFilterException("Only IndexColorModel allowed.");
}
}
/**
* Creates a compatible {@code Raster} to dither into.
* Only {@code IndexColorModel} allowed.
*
* @param pSrc
*
* @return a {@code WritableRaster}
*/
public final WritableRaster createCompatibleDestRaster(Raster pSrc) {
return createCompatibleDestRaster(pSrc, getICM(pSrc));
}
public final WritableRaster createCompatibleDestRaster(Raster pSrc,
IndexColorModel pIndexColorModel) {
/*
return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
pIndexColorModel).getRaster();
*/
return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight());
}
/**
* Returns the bounding box of the filtered destination image. Since
* this is not a geometric operation, the bounding box does not
* change.
* @param pSrc the {@code BufferedImage} to be filtered
* @return the bounds of the filtered definition image.
*/
public final Rectangle2D getBounds2D(BufferedImage pSrc) {
return getBounds2D(pSrc.getRaster());
}
/**
* Returns the bounding box of the filtered destination Raster. Since
* this is not a geometric operation, the bounding box does not
* change.
* @param pSrc the {@code Raster} to be filtered
* @return the bounds of the filtered definition {@code Raster}.
*/
public final Rectangle2D getBounds2D(Raster pSrc) {
return pSrc.getBounds();
}
/**
* Returns the location of the destination point given a
* point in the source. If {@code dstPt} is not
* {@code null}, it will be used to hold the return value.
* Since this is not a geometric operation, the {@code srcPt}
* will equal the {@code dstPt}.
* @param pSrcPt a {@code Point2D} that represents a point
* in the source image
* @param pDstPt a {@code Point2D}that represents the location
* in the destination
* @return the {@code Point2D} in the destination that
* corresponds to the specified point in the source.
*/
public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) {
// Create new Point, if needed
if (pDstPt == null) {
pDstPt = new Point2D.Float();
}
// Copy location
pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY());
// Return dest
return pDstPt;
}
/**
* Returns the rendering mHints for this op.
* @return the {@code RenderingHints} object associated
* with this op.
*/
public final RenderingHints getRenderingHints() {
return null;
}
/**
* Converts a int triplet to int ARGB.
*/
private static int toIntARGB(int[] pRGB) {
return 0xff000000 // All opaque
| (pRGB[0] << 16)
| (pRGB[1] << 8)
| (pRGB[2]);
/*
| ((int) (pRGB[0] << 16) & 0x00ff0000)
| ((int) (pRGB[1] << 8) & 0x0000ff00)
| ((int) (pRGB[2] ) & 0x000000ff);
*/
}
/**
* Performs a single-input/single-output dither operation, applying basic
* Floyd-Steinberg error-diffusion to the image.
*
* @param pSource the source image
* @param pDest the destiantion image
*
* @return the destination image, or a new image, if {@code pDest} was
* {@code null}.
*/
public final BufferedImage filter(BufferedImage pSource,
BufferedImage pDest) {
// Create destination image, if none provided
if (pDest == null) {
pDest = createCompatibleDestImage(pSource, getICM(pSource));
}
else if (!(pDest.getColorModel() instanceof IndexColorModel)) {
throw new ImageFilterException("Only IndexColorModel allowed.");
}
// Filter rasters
filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel());
return pDest;
}
/**
* Performs a single-input/single-output dither operation, applying basic
* Floyd-Steinberg error-diffusion to the image.
*
* @param pSource
* @param pDest
*
* @return the destination raster, or a new raster, if {@code pDest} was
* {@code null}.
*/
public final WritableRaster filter(final Raster pSource, WritableRaster pDest) {
return filter(pSource, pDest, getICM(pSource));
}
private IndexColorModel getICM(BufferedImage pSource) {
return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY));
}
private IndexColorModel getICM(Raster pSource) {
return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource));
}
private IndexColorModel createIndexColorModel(Raster pSource) {
BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_INT_ARGB);
image.setData(pSource);
return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY);
}
/**
* Performs a single-input/single-output pixel copy operation.
*
* @param pSource
* @param pDest
* @param pColorModel
*
* @return the destination raster, or a new raster, if {@code pDest} was
* {@code null}.
*/
public final WritableRaster filter(final Raster pSource, WritableRaster pDest,
IndexColorModel pColorModel) {
int width = pSource.getWidth();
int height = pSource.getHeight();
if (pDest == null) {
pDest = createCompatibleDestRaster(pSource, pColorModel);
}
// temp buffers
final int[] inRGB = new int[4];
Object pixel = null;
// TODO: Use getPixels instead of getPixel for better performance?
// Loop through image data
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Get rgb from original raster
// DON'T KNOW IF THIS WILL WORK FOR ALL TYPES..?
pSource.getPixel(x, y, inRGB);
// Get pixel value...
// It is VERY important that we are using an IndexColorModel that
// support reverse color lookup for speed.
pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel);
// And set it
pDest.setDataElements(x, y, pixel);
}
}
return pDest;
}
}

View File

@@ -0,0 +1,465 @@
package com.twelvemonkeys.image;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.awt.image.WritableRaster;
import java.util.Random;
/**
* This {@code BufferedImageOp/RasterOp} implements basic
* Floyd-Steinberg error-diffusion algorithm for dithering.
* <P/>
* The weights used are 7/16 3/16 5/16 1/16, distributed like this:
* <!-- - -
* | |x|7|
* - - - -
* |3|5|1|
* - - -->
* <P/>
* <TABLE border="1" cellpadding="4" cellspacing="0">
* <TR><TD bgcolor="#000000">&nbsp;</TD><TD class="TableHeadingColor"
* align="center">X</TD><TD>7/16</TD></TR>
* <TR><TD>3/16</TD><TD>5/16</TD><TD>1/16</TD></TR>
* </TABLE>
* <P/>
* See <A href="http://www.awprofessional.com/bookstore/product.asp?isbn=0201848406&rl=1">Computer Graphics (Foley et al.)</a>
* for more information.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java#1 $
*
*/
public class DiffusionDither implements BufferedImageOp, RasterOp {
protected IndexColorModel mIndexColorModel = null;
private boolean mAlternateScans = true;
private static final int FS_SCALE = 1 << 8;
private static final Random RANDOM = new Random();
/**
* Creates a {@code DiffusionDither}, using the given
* {@code IndexColorModel} for dithering into.
*
* @param pICM an IndexColorModel.
*/
public DiffusionDither(IndexColorModel pICM) {
// Store colormodel
mIndexColorModel = pICM;
}
/**
* Creates a {@code DiffusionDither}, with no fixed
* {@code IndexColorModel}. The colormodel will be generated for each
* filtering, unless the dest image allready has an
* {@code IndexColorModel}.
*/
public DiffusionDither() {
}
/**
* Sets the scan mode. If the parameter is true, error distribution for
* every even line will be left-to-right, while odd lines will be
* right-to-left.
*
* @param pUse {@code true} if scan mode should be alternating left/right
*/
public void setAlternateScans(boolean pUse) {
mAlternateScans = pUse;
}
/**
* Creates a compatible {@code BufferedImage} to dither into.
* Only {@code IndexColorModel} allowed.
*
* @return a compatible {@code BufferedImage}
*
* @throws ImageFilterException if {@code pDestCM} is not {@code null} or
* an instance of {@code IndexColorModel}.
*/
public final BufferedImage createCompatibleDestImage(BufferedImage pSource,
ColorModel pDestCM) {
if (pDestCM == null) {
return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
getICM(pSource));
}
else if (pDestCM instanceof IndexColorModel) {
return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
(IndexColorModel) pDestCM);
}
else {
throw new ImageFilterException("Only IndexColorModel allowed.");
}
}
/**
* Creates a compatible {@code Raster} to dither into.
* Only {@code IndexColorModel} allowed.
*
* @param pSrc
*
* @return a {@code WritableRaster}
*/
public final WritableRaster createCompatibleDestRaster(Raster pSrc) {
return createCompatibleDestRaster(pSrc, getICM(pSrc));
}
public final WritableRaster createCompatibleDestRaster(Raster pSrc,
IndexColorModel pIndexColorModel) {
return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight());
/*
return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
pIndexColorModel).getRaster();
*/
}
/**
* Returns the bounding box of the filtered destination image. Since
* this is not a geometric operation, the bounding box does not
* change.
* @param pSrc the {@code BufferedImage} to be filtered
* @return the bounds of the filtered definition image.
*/
public final Rectangle2D getBounds2D(BufferedImage pSrc) {
return getBounds2D(pSrc.getRaster());
}
/**
* Returns the bounding box of the filtered destination Raster. Since
* this is not a geometric operation, the bounding box does not
* change.
* @param pSrc the {@code Raster} to be filtered
* @return the bounds of the filtered definition {@code Raster}.
*/
public final Rectangle2D getBounds2D(Raster pSrc) {
return pSrc.getBounds();
}
/**
* Returns the location of the destination point given a
* point in the source. If {@code dstPt} is not
* {@code null}, it will be used to hold the return value.
* Since this is not a geometric operation, the {@code srcPt}
* will equal the {@code dstPt}.
* @param pSrcPt a {@code Point2D} that represents a point
* in the source image
* @param pDstPt a {@code Point2D}that represents the location
* in the destination
* @return the {@code Point2D} in the destination that
* corresponds to the specified point in the source.
*/
public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) {
// Create new Point, if needed
if (pDstPt == null) {
pDstPt = new Point2D.Float();
}
// Copy location
pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY());
// Return dest
return pDstPt;
}
/**
* Returns the rendering mHints for this op.
* @return the {@code RenderingHints} object associated
* with this op.
*/
public final RenderingHints getRenderingHints() {
return null;
}
/**
* Converts an int ARGB to int triplet.
*/
private static int[] toRGBArray(int pARGB, int[] pBuffer) {
pBuffer[0] = ((pARGB & 0x00ff0000) >> 16);
pBuffer[1] = ((pARGB & 0x0000ff00) >> 8);
pBuffer[2] = ((pARGB & 0x000000ff));
//pBuffer[3] = ((pARGB & 0xff000000) >> 24); // alpha
return pBuffer;
}
/**
* Converts a int triplet to int ARGB.
*/
private static int toIntARGB(int[] pRGB) {
return 0xff000000 // All opaque
| (pRGB[0] << 16)
| (pRGB[1] << 8)
| (pRGB[2]);
/*
| ((int) (pRGB[0] << 16) & 0x00ff0000)
| ((int) (pRGB[1] << 8) & 0x0000ff00)
| ((int) (pRGB[2] ) & 0x000000ff);
*/
}
/**
* Performs a single-input/single-output dither operation, applying basic
* Floyd-Steinberg error-diffusion to the image.
*
* @param pSource the source image
* @param pDest the destiantion image
*
* @return the destination image, or a new image, if {@code pDest} was
* {@code null}.
*/
public final BufferedImage filter(BufferedImage pSource,
BufferedImage pDest) {
// Create destination image, if none provided
if (pDest == null) {
pDest = createCompatibleDestImage(pSource, getICM(pSource));
}
else if (!(pDest.getColorModel() instanceof IndexColorModel)) {
throw new ImageFilterException("Only IndexColorModel allowed.");
}
// Filter rasters
filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel());
return pDest;
}
/**
* Performs a single-input/single-output dither operation, applying basic
* Floyd-Steinberg error-diffusion to the image.
*
* @param pSource
* @param pDest
*
* @return the destination raster, or a new raster, if {@code pDest} was
* {@code null}.
*/
public final WritableRaster filter(final Raster pSource, WritableRaster pDest) {
return filter(pSource, pDest, getICM(pSource));
}
private IndexColorModel getICM(BufferedImage pSource) {
return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK));
}
private IndexColorModel getICM(Raster pSource) {
return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource));
}
private IndexColorModel createIndexColorModel(Raster pSource) {
BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(),
BufferedImage.TYPE_INT_ARGB);
image.setData(pSource);
return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK);
}
/**
* Performs a single-input/single-output dither operation, applying basic
* Floyd-Steinberg error-diffusion to the image.
*
* @param pSource
* @param pDest
* @param pColorModel
*
* @return the destination raster, or a new raster, if {@code pDest} was
* {@code null}.
*/
public final WritableRaster filter(final Raster pSource, WritableRaster pDest,
IndexColorModel pColorModel) {
int width = pSource.getWidth();
int height = pSource.getHeight();
// Create destination raster if needed
if (pDest == null) {
pDest = createCompatibleDestRaster(pSource, pColorModel);
}
// Initialize Floyd-Steinberg error vectors.
// +2 to handle the previous pixel and next pixel case minimally
// When reference for column, add 1 to reference as this buffer is
// offset from actual column position by one to allow FS to not check
// left/right edge conditions
int[][] mCurrErr = new int[width + 2][3];
int[][] mNextErr = new int[width + 2][3];
// Random errors in [-1 .. 1] - for first row
for (int i = 0; i < width + 2; i++) {
// Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE
/*
mCurrErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
mCurrErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
mCurrErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
*/
mCurrErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
mCurrErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
mCurrErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
}
// Temp buffers
final int[] diff = new int[3]; // No alpha
final int[] inRGB = new int[4];
final int[] outRGB = new int[4];
Object pixel = null;
boolean forward = true;
// Loop through image data
for (int y = 0; y < height; y++) {
// Clear out next error rows for colour errors
for (int i = mNextErr.length; --i >= 0;) {
mNextErr[i][0] = 0;
mNextErr[i][1] = 0;
mNextErr[i][2] = 0;
}
// Set up start column and limit
int x;
int limit;
if (forward) {
x = 0;
limit = width;
}
else {
x = width - 1;
limit = -1;
}
// TODO: Use getPixels instead of getPixel for better performance?
// Loop over row
while (true) {
// Get RGB from original raster
// DON'T KNOW IF THIS WILL WORK FOR ALL TYPES.
pSource.getPixel(x, y, inRGB);
// Get error for this pixel & add error to rgb
for (int i = 0; i < 3; i++) {
// Make a 28.4 FP number, add Error (with fraction),
// rounding and truncate to int
inRGB[i] = ((inRGB[i] << 4) + mCurrErr[x + 1][i] + 0x08) >> 4;
// Clamp
if (inRGB[i] > 255) {
inRGB[i] = 255;
}
else if (inRGB[i] < 0) {
inRGB[i] = 0;
}
}
// Get pixel value...
// It is VERY important that we are using a IndexColorModel that
// support reverse color lookup for speed.
pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel);
// ...set it...
pDest.setDataElements(x, y, pixel);
// ..and get back the closet match
pDest.getPixel(x, y, outRGB);
// Convert the value to default sRGB
// Should work for all transfertypes supported by IndexColorModel
toRGBArray(pColorModel.getRGB(outRGB[0]), outRGB);
// Find diff
diff[0] = inRGB[0] - outRGB[0];
diff[1] = inRGB[1] - outRGB[1];
diff[2] = inRGB[2] - outRGB[2];
// Apply F-S error diffusion
// Serpentine scan: left-right
if (forward) {
// Row 1 (y)
// Update error in this pixel (x + 1)
mCurrErr[x + 2][0] += diff[0] * 7;
mCurrErr[x + 2][1] += diff[1] * 7;
mCurrErr[x + 2][2] += diff[2] * 7;
// Row 2 (y + 1)
// Update error in this pixel (x - 1)
mNextErr[x][0] += diff[0] * 3;
mNextErr[x][1] += diff[1] * 3;
mNextErr[x][2] += diff[2] * 3;
// Update error in this pixel (x)
mNextErr[x + 1][0] += diff[0] * 5;
mNextErr[x + 1][1] += diff[1] * 5;
mNextErr[x + 1][2] += diff[2] * 5;
// Update error in this pixel (x + 1)
// TODO: Consider calculating this using
// error term = error - sum(error terms 1, 2 and 3)
// See Computer Graphics (Foley et al.), p. 573
mNextErr[x + 2][0] += diff[0]; // * 1;
mNextErr[x + 2][1] += diff[1]; // * 1;
mNextErr[x + 2][2] += diff[2]; // * 1;
// Next
x++;
// Done?
if (x >= limit) {
break;
}
}
else {
// Row 1 (y)
// Update error in this pixel (x - 1)
mCurrErr[x][0] += diff[0] * 7;
mCurrErr[x][1] += diff[1] * 7;
mCurrErr[x][2] += diff[2] * 7;
// Row 2 (y + 1)
// Update error in this pixel (x + 1)
mNextErr[x + 2][0] += diff[0] * 3;
mNextErr[x + 2][1] += diff[1] * 3;
mNextErr[x + 2][2] += diff[2] * 3;
// Update error in this pixel (x)
mNextErr[x + 1][0] += diff[0] * 5;
mNextErr[x + 1][1] += diff[1] * 5;
mNextErr[x + 1][2] += diff[2] * 5;
// Update error in this pixel (x - 1)
// TODO: Consider calculating this using
// error term = error - sum(error terms 1, 2 and 3)
// See Computer Graphics (Foley et al.), p. 573
mNextErr[x][0] += diff[0]; // * 1;
mNextErr[x][1] += diff[1]; // * 1;
mNextErr[x][2] += diff[2]; // * 1;
// Previous
x--;
// Done?
if (x <= limit) {
break;
}
}
}
// Make next error info current for next iteration
int[][] temperr;
temperr = mCurrErr;
mCurrErr = mNextErr;
mNextErr = temperr;
// Toggle direction
if (mAlternateScans) {
forward = !forward;
}
}
return pDest;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.*;
/**
* GraphicsUtil
*
* @author <a href="mailto:harald.kuhr@gmail.no">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java#1 $
*/
public final class GraphicsUtil {
/**
* Enables anti-aliasing in the {@code Graphics} object.
* <p/>
* Anti-aliasing is enabled by casting to {@code Graphics2D} and setting
* the rendering hint {@code RenderingHints.KEY_ANTIALIASING} to
* {@code RenderingHints.VALUE_ANTIALIAS_ON}.
*
* @param pGraphics the graphics object
* @throws ClassCastException if {@code pGraphics} is not an instance of
* {@code Graphics2D}.
*
* @see java.awt.RenderingHints#KEY_ANTIALIASING
*/
public static void enableAA(final Graphics pGraphics) {
((Graphics2D) pGraphics).setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
);
}
/**
* Sets the alpha in the {@code Graphics} object.
* <p/>
* Alpha is set by casting to {@code Graphics2D} and setting the composite
* to the rule {@code AlphaComposite.SRC_OVER} multiplied by the given
* alpha.
*
* @param pGraphics the graphics object
* @param pAlpha the alpha level, {@code alpha} must be a floating point
* number in the inclusive range [0.0,&nbsp;1.0].
* @throws ClassCastException if {@code pGraphics} is not an instance of
* {@code Graphics2D}.
*
* @see java.awt.AlphaComposite#SRC_OVER
* @see java.awt.AlphaComposite#getInstance(int, float)
*/
public static void setAlpha(final Graphics pGraphics, final float pAlpha) {
((Graphics2D) pGraphics).setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pAlpha)
);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.*;
/**
* This class represents a 256 color fixed grayscale IndexColorModel.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java#1 $
*
*/
public class GrayColorModel extends IndexColorModel {
private final static byte[] sGrays = createGrayScale();
public GrayColorModel() {
super(8, sGrays.length, sGrays, sGrays, sGrays);
}
private static byte[] createGrayScale() {
byte[] grays = new byte[256];
for (int i = 0; i < 256; i++) {
grays[i] = (byte) i;
}
return grays;
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.*;
/**
* This class can convert a color image to grayscale.
* <P/>
* Uses ITU standard conversion: (222 * Red + 707 * Green + 71 * Blue) / 1000.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java#1 $
*
*/
public class GrayFilter extends RGBImageFilter {
// This filter can filter IndexColorModel
{
canFilterIndexColorModel = true;
}
private int mLow = 0;
private float mRange = 1.0f;
/**
* Constructs a GrayFilter using ITU color-conversion.
*/
public GrayFilter() {
}
/**
* Constructs a GrayFilter using ITU color-conversion, and a dynamic range between
* pLow and pHigh.
*
* @param pLow float in the range 0..1
* @param pHigh float in the range 0..1 and >= pLow
*/
public GrayFilter(float pLow, float pHigh) {
if (pLow > pHigh) {
pLow = 0f;
}
// Make sure high and low are inside range
if (pLow < 0f) {
pLow = 0f;
}
else if (pLow > 1f) {
pLow = 1f;
}
if (pHigh < 0f) {
pHigh = 0f;
}
else if (pHigh > 1f) {
pHigh = 1f;
}
mLow = (int) (pLow * 255f);
mRange = pHigh - pLow;
}
/**
* Constructs a GrayFilter using ITU color-conversion, and a dynamic
* range between pLow and pHigh.
*
* @param pLow integer in the range 0..255
* @param pHigh inteeger in the range 0..255 and >= pLow
*/
public GrayFilter(int pLow, int pHigh) {
this(pLow / 255f, pHigh / 255f);
}
/**
* Filters one pixel using ITU color-conversion.
*
* @param pX x
* @param pY y
* @param pARGB pixel value in default color space
*
* @return the filtered pixel value in the default color space
*/
public int filterRGB(int pX, int pY, int pARGB) {
// Get color components
int r = pARGB >> 16 & 0xFF;
int g = pARGB >> 8 & 0xFF;
int b = pARGB & 0xFF;
// ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000
int gray = (222 * r + 707 * g + 71 * b) / 1000;
//int gray = (int) ((float) (r + g + b) / 3.0f);
if (mRange != 1.0f) {
// Apply range
gray = mLow + (int) (gray * mRange);
}
// Return ARGB pixel
return (pARGB & 0xFF000000) | (gray << 16) | (gray << 8) | gray;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
/**
* This class wraps IllegalArgumentException, and is thrown by the ImageUtil
* class, when trying to convert images read from {@code null}-sources etc.
*
* @author Harald Kuhr
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java#1 $
*/
public class ImageConversionException extends ImageFilterException {
public ImageConversionException(String pMessage) {
super(pMessage);
}
public ImageConversionException(String pMessage, Throwable pCause) {
super(pMessage, pCause);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
/**
* This class wraps IllegalArgumentException as thrown by the
* BufferedImageOp interface for more fine-grained control.
*
* @author Harald Kuhr
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java#1 $
*/
public class ImageFilterException extends IllegalArgumentException {
private Throwable mCause = null;
public ImageFilterException(String pStr) {
super(pStr);
}
public ImageFilterException(Throwable pT) {
initCause(pT);
}
public ImageFilterException(String pStr, Throwable pT) {
super(pStr);
initCause(pT);
}
public Throwable initCause(Throwable pThrowable) {
if (mCause != null) {
// May only be called once
throw new IllegalStateException();
}
else if (pThrowable == this) {
throw new IllegalArgumentException();
}
mCause = pThrowable;
// Hmmm...
return this;
}
public Throwable getCause() {
return mCause;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
/**
* Inverse Colormap to provide efficient lookup of any given input color
* to the closest match to the given color map.
* <p/>
* Based on "Efficient Inverse Color Map Computation" by Spencer W. Thomas
* in "Graphics Gems Volume II"
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Robin Luiten (Java port)
* @author Spencer W. Thomas (original c version).
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java#1 $
*/
class InverseColorMap {
/**
* Number of high bits of each color channel to use to lookup near match
*/
final static int QUANTBITS = 5;
/**
* Truncated bits of each color channel
*/
final static int TRUNCBITS = 8 - QUANTBITS;
/**
* BITMASK representing the bits for blue in the color lookup
*/
final static int QUANTMASK_BLUE = (1 << 5) - 1;
/**
* BITMASK representing the bits for green in the color lookup
*/
final static int QUANTMASK_GREEN = (QUANTMASK_BLUE << QUANTBITS);
/**
* BITMASK representing the bits for red in the color lookup
*/
final static int QUANTMASK_RED = (QUANTMASK_GREEN << QUANTBITS);
/**
* Maximum value a quantised color channel can have
*/
final static int MAXQUANTVAL = 1 << 5;
byte[] mRGBMapByte;
int[] mRGBMapInt;
int mNumColors;
int mMaxColor;
byte[] mInverseRGB; // inverse rgb color map
int mTransparentIndex = -1;
/**
* @param pRGBColorMap the rgb color map to create inverse color map for.
*/
InverseColorMap(byte[] pRGBColorMap) {
this(pRGBColorMap, -1);
}
/**
* @param pRGBColorMap the rgb color map to create inverse color map for.
*/
// HaraldK 20040801: Added support for int[]
InverseColorMap(int[] pRGBColorMap) {
this(pRGBColorMap, -1);
}
/**
* @param pRGBColorMap the rgb color map to create inverse color map for.
* @param pTransparent the index of the transparent pixel in the map
*/
InverseColorMap(byte[] pRGBColorMap, int pTransparent) {
mRGBMapByte = pRGBColorMap;
mNumColors = mRGBMapByte.length / 4;
mTransparentIndex = pTransparent;
mInverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL];
initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]);
}
/**
* @param pRGBColorMap the rgb color map to create inverse color map for.
* @param pTransparent the index of the transparent pixel in the map
*/
InverseColorMap(int[] pRGBColorMap, int pTransparent) {
mRGBMapInt = pRGBColorMap;
mNumColors = mRGBMapInt.length;
mTransparentIndex = pTransparent;
mInverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL];
initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]);
}
/**
* Simple inverse color table creation method.
* @param pTemp temp array
*/
void initIRGB(int[] pTemp) {
final int x = (1 << TRUNCBITS); // 8 the size of 1 Dimension of each quantized cell
final int xsqr = 1 << (TRUNCBITS * 2); // 64 - twice the smallest step size vale of quantized colors
final int xsqr2 = xsqr + xsqr;
for (int i = 0; i < mNumColors; ++i) {
if (i == mTransparentIndex) {
// Skip the transparent pixel
continue;
}
int red, r, rdist, rinc, rxx;
int green, g, gdist, ginc, gxx;
int blue, b, bdist, binc, bxx;
// HaraldK 20040801: Added support for int[]
if (mRGBMapByte != null) {
red = mRGBMapByte[i * 4] & 0xFF;
green = mRGBMapByte[i * 4 + 1] & 0xFF;
blue = mRGBMapByte[i * 4 + 2] & 0xFF;
}
else if (mRGBMapInt != null) {
red = (mRGBMapInt[i] >> 16) & 0xFF;
green = (mRGBMapInt[i] >> 8) & 0xFF;
blue = mRGBMapInt[i] & 0xFF;
}
else {
throw new IllegalStateException("colormap == null");
}
rdist = red - x / 2; // distance of red to center of current cell
gdist = green - x / 2; // green
bdist = blue - x / 2; // blue
rdist = rdist * rdist + gdist * gdist + bdist * bdist;
rinc = 2 * (xsqr - (red << TRUNCBITS));
ginc = 2 * (xsqr - (green << TRUNCBITS));
binc = 2 * (xsqr - (blue << TRUNCBITS));
int rgbI = 0;
for (r = 0, rxx = rinc; r < MAXQUANTVAL; rdist += rxx, ++r, rxx += xsqr2) {
for (g = 0, gdist = rdist, gxx = ginc; g < MAXQUANTVAL; gdist += gxx, ++g, gxx += xsqr2) {
for (b = 0, bdist = gdist, bxx = binc; b < MAXQUANTVAL; bdist += bxx, ++b, ++rgbI, bxx += xsqr2) {
if (i == 0 || pTemp[rgbI] > bdist) {
pTemp[rgbI] = bdist;
mInverseRGB[rgbI] = (byte) i;
}
}
}
}
}
}
/**
* Gets the index of the nearest color to from the color map.
*
* @param pColor the color to get the nearest color to from color map
* color must be of format {@code 0x00RRGGBB} - standard default RGB
* @return index of color which closest matches input color by using the
* created inverse color map.
*/
public final int getIndexNearest(int pColor) {
return mInverseRGB[((pColor >> (3 * TRUNCBITS)) & QUANTMASK_RED) +
((pColor >> (2 * TRUNCBITS)) & QUANTMASK_GREEN) +
((pColor >> (/* 1 * */ TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF;
}
/**
* Gets the index of the nearest color to from the color map.
*
* @param pRed red component of the color to get the nearest color to from color map
* @param pGreen green component of the color to get the nearest color to from color map
* @param pBlue blue component of the color to get the nearest color to from color map
* @return index of color which closest matches input color by using the
* created inverse color map.
*/
public final int getIndexNearest(int pRed, int pGreen, int pBlue) {
// NOTE: the third line in expression for blue is shifting DOWN not UP.
return mInverseRGB[((pRed << (2 * QUANTBITS - TRUNCBITS)) & QUANTMASK_RED) +
((pGreen << (/* 1 * */ QUANTBITS - TRUNCBITS)) & QUANTMASK_GREEN) +
((pBlue >> (TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF;
}
}

View File

@@ -0,0 +1,310 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import com.twelvemonkeys.lang.StringUtil;
import java.awt.*;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
/**
* A faster implementation of {@code IndexColorModel}, that is backed by an
* inverse color-map, for fast lookups.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java#1 $
*
*/
public class InverseColorMapIndexColorModel extends IndexColorModel {
protected int mRGBs[];
protected int mMapSize;
protected InverseColorMap mInverseMap = null;
private final static int ALPHA_THRESHOLD = 0x80;
private int mWhiteIndex = -1;
private final static int WHITE = 0x00FFFFFF;
private final static int RGB_MASK = 0x00FFFFFF;
/**
* Creates an {@code InverseColorMapIndexColorModel} from an existing
* {@code IndexColorModel}.
*
* @param pColorModel the colormodel to create from
*/
public InverseColorMapIndexColorModel(IndexColorModel pColorModel) {
this(pColorModel, getRGBs(pColorModel));
}
// NOTE: The pRGBs parameter is used to get around invoking getRGBs two
// times. What is wrong with protected?!
private InverseColorMapIndexColorModel(IndexColorModel pColorModel, int[] pRGBs) {
super(pColorModel.getComponentSize()[0], pColorModel.getMapSize(),
pRGBs, 0,
ImageUtil.getTransferType(pColorModel),
pColorModel.getValidPixels());
mRGBs = pRGBs;
mMapSize = mRGBs.length;
mInverseMap = new InverseColorMap(mRGBs);
mWhiteIndex = getWhiteIndex();
}
/**
* Creates a defensive copy of the RGB colormap in the given
* {@code IndexColorModel}.
*
* @param pColorModel the indec colormodel to get RGB values from
* @return the RGB colormap
*/
private static int[] getRGBs(IndexColorModel pColorModel) {
int[] rgb = new int[pColorModel.getMapSize()];
pColorModel.getRGBs(rgb);
return rgb;
}
/**
* Creates an {@code InverseColorMapIndexColorModel} from the given array
* of RGB components, plus one transparent index.
*
* @param pNumBits the number of bits each pixel occupies
* @param pSize the size of the color component arrays
* @param pRGBs the array of packed RGB color components
* @param pStart the starting offset of the first color component
* @param pAlpha indicates whether alpha values are contained in {@code pRGBs}
* @param pTransparentIndex the index of the transparent pixel
* @param pTransferType the data type of the array used to represent pixels
*
* @throws IllegalArgumentException if bits is less than 1 or greater than 16,
* or if size is less than 1
*
* @see IndexColorModel#IndexColorModel(int, int, int[], int, boolean, int, int)
*/
public InverseColorMapIndexColorModel(int pNumBits, int pSize, int[] pRGBs,
int pStart, boolean pAlpha, int pTransparentIndex,
int pTransferType) {
super(pNumBits, pSize, pRGBs, pStart, pAlpha, pTransparentIndex, pTransferType);
mRGBs = getRGBs(this);
mMapSize = mRGBs.length;
mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex);
mWhiteIndex = getWhiteIndex();
}
/**
* Creates an {@code InverseColorMapIndexColorModel} from the given arrays
* of red, green, and blue components, plus one transparent index.
*
* @param pNumBits the number of bits each pixel occupies
* @param pSize the size of the color component arrays
* @param pReds the array of red color components
* @param pGreens the array of green color components
* @param pBlues the array of blue color components
* @param pTransparentIndex the index of the transparent pixel
*
* @throws IllegalArgumentException if bits is less than 1 or greater than 16,
* or if size is less than 1
*
* @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[], int)
*/
public InverseColorMapIndexColorModel(int pNumBits, int pSize,
byte[] pReds, byte[] pGreens, byte[] pBlues,
int pTransparentIndex) {
super(pNumBits, pSize, pReds, pGreens, pBlues, pTransparentIndex);
mRGBs = getRGBs(this);
mMapSize = mRGBs.length;
mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex);
mWhiteIndex = getWhiteIndex();
}
/**
* Creates an {@code InverseColorMapIndexColorModel} from the given arrays
* of red, green, and blue components.
*
* @param pNumBits the number of bits each pixel occupies
* @param pSize the size of the color component arrays
* @param pReds the array of red color components
* @param pGreens the array of green color components
* @param pBlues the array of blue color components
*
* @throws IllegalArgumentException if bits is less than 1 or greater than 16,
* or if size is less than 1
*
* @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[])
*/
public InverseColorMapIndexColorModel(int pNumBits, int pSize,
byte[] pReds, byte[] pGreens, byte[] pBlues) {
super(pNumBits, pSize, pReds, pGreens, pBlues);
mRGBs = getRGBs(this);
mMapSize = mRGBs.length;
mInverseMap = new InverseColorMap(mRGBs);
mWhiteIndex = getWhiteIndex();
}
private int getWhiteIndex() {
for (int i = 0; i < mRGBs.length; i++) {
int color = mRGBs[i];
if ((color & RGB_MASK) == WHITE) {
return i;
}
}
return -1;
}
/**
* Creates an {@code IndexColorModel} optimized for the given {@code Image}.
*
* @param pImage the {@code Image} containing the RGB samples
* @param pNumCols the maximum number of colors in the {@code IndexColorModel}
* @param pFlags flags
*
* @return a new optimized {@code IndexColorModel}
*/
public static IndexColorModel create(Image pImage, int pNumCols, int pFlags) {
// TODO: Inline and deprecate IndexImage.getIndexColorModel!?
IndexColorModel icm = IndexImage.getIndexColorModel(pImage, pNumCols, pFlags);
InverseColorMapIndexColorModel cm;
if (icm instanceof InverseColorMapIndexColorModel) {
cm = (InverseColorMapIndexColorModel) icm;
}
else {
cm = new InverseColorMapIndexColorModel(icm);
}
return cm;
}
/**
* Returns a data element array representation of a pixel in this
* ColorModel, given an integer pixel representation in the
* default RGB color model. This array can then be passed to the
* {@link java.awt.image.WritableRaster#setDataElements(int, int, Object) setDataElements}
* method of a {@link java.awt.image.WritableRaster} object. If the pixel variable is
* {@code null}, a new array is allocated. If {@code pixel}
* is not {@code null}, it must be
* a primitive array of type {@code transferType}; otherwise, a
* {@code ClassCastException} is thrown. An
* {@code ArrayIndexOutOfBoundsException} is
* thrown if {@code pixel} is not large enough to hold a pixel
* value for this {@code ColorModel}. The pixel array is returned.
* <p>
* Since {@code OpaqueIndexColorModel} can be subclassed, subclasses
* inherit the implementation of this method and if they don't
* override it then they throw an exception if they use an
* unsupported {@code transferType}.
*
* #param rgb the integer pixel representation in the default RGB
* color model
* #param pixel the specified pixel
* #return an array representation of the specified pixel in this
* {@code OpaqueIndexColorModel}.
* #throws ClassCastException if {@code pixel}
* is not a primitive array of type {@code transferType}
* #throws ArrayIndexOutOfBoundsException if
* {@code pixel} is not large enough to hold a pixel value
* for this {@code ColorModel}
* #throws UnsupportedOperationException if {@code transferType}
* is invalid
* @see java.awt.image.WritableRaster#setDataElements
* @see java.awt.image.SampleModel#setDataElements
*
*/
public Object getDataElements(int rgb, Object pixel) {
int alpha = (rgb>>>24);
int pix;
if (alpha < ALPHA_THRESHOLD && getTransparentPixel() != -1) {
pix = getTransparentPixel();
}
else {
int color = rgb & RGB_MASK;
if (color == WHITE && mWhiteIndex != -1) {
pix = mWhiteIndex;
}
else {
pix = mInverseMap.getIndexNearest(color);
}
}
return installpixel(pixel, pix);
}
private Object installpixel(Object pixel, int pix) {
switch (transferType) {
case DataBuffer.TYPE_INT:
int[] intObj;
if (pixel == null) {
pixel = intObj = new int[1];
}
else {
intObj = (int[]) pixel;
}
intObj[0] = pix;
break;
case DataBuffer.TYPE_BYTE:
byte[] byteObj;
if (pixel == null) {
pixel = byteObj = new byte[1];
}
else {
byteObj = (byte[]) pixel;
}
byteObj[0] = (byte) pix;
break;
case DataBuffer.TYPE_USHORT:
short[] shortObj;
if (pixel == null) {
pixel = shortObj = new short[1];
}
else {
shortObj = (short[]) pixel;
}
shortObj[0] = (short) pix;
break;
default:
throw new UnsupportedOperationException("This method has not been " +
"implemented for transferType " + transferType);
}
return pixel;
}
public String toString() {
// Just a workaround to ease debugging
return StringUtil.replace(super.toString(), "IndexColorModel: ", getClass().getName() + ": ");
}
}

View File

@@ -0,0 +1,24 @@
package com.twelvemonkeys.image;
/**
* Magick
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/Magick.java#1 $
*/
final class Magick {
static final boolean DEBUG = useDebug();
private static boolean useDebug() {
try {
return "TRUE".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.magick.debug"));
}
catch (Throwable t) {
// Most probably in case of a SecurityManager
return false;
}
}
private Magick() {}
}

View File

@@ -0,0 +1,184 @@
package com.twelvemonkeys.image;
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.twelvemonkeys.lang.SystemUtil;
import magick.MagickImage;
import java.awt.image.*;
/**
* This class accelerates certain graphics operations, using
* JMagick and ImageMagick, if available.
* If those libraries are not installed, this class silently does nothing.
* <p/>
* Set the system property {@code "com.twelvemonkeys.image.accel"} to
* {@code false}, to disable, even if JMagick is installed.
* Set the system property {@code "com.twelvemonkeys.image.magick.debug"} to
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java#3 $
*/
final class MagickAccelerator {
private static final boolean DEBUG = Magick.DEBUG;
private static final boolean USE_MAGICK = useMagick();
private static final int RESAMPLE_OP = 0;
private static Class[] sNativeOp = new Class[1];
static {
try {
sNativeOp[RESAMPLE_OP] = Class.forName("com.twelvemonkeys.image.ResampleOp");
}
catch (ClassNotFoundException e) {
System.err.println("Could not find class: " + e);
}
}
private static boolean useMagick() {
try {
boolean available = SystemUtil.isClassAvailable("magick.MagickImage");
if (DEBUG && !available) {
System.err.print("ImageMagick bindings not available.");
}
boolean useMagick =
available && !"FALSE".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.accel"));
if (DEBUG) {
System.err.println(
useMagick
? "Will use ImageMagick bindings to accelerate image resampling operations."
: "Will not use ImageMagick to accelerate image resampling operations."
);
}
return useMagick;
}
catch (Throwable t) {
// Most probably in case of a SecurityManager
System.err.println("Could not enable ImageMagick bindings: " + t);
return false;
}
}
private static int getNativeOpIndex(Class pOpClass) {
for (int i = 0; i < sNativeOp.length; i++) {
if (pOpClass == sNativeOp[i]) {
return i;
}
}
return -1;
}
public static BufferedImage filter(BufferedImageOp pOperation, BufferedImage pInput, BufferedImage pOutput) {
if (!USE_MAGICK) {
return null;
}
BufferedImage result = null;
switch (getNativeOpIndex(pOperation.getClass())) {
case RESAMPLE_OP:
ResampleOp resample = (ResampleOp) pOperation;
result = resampleMagick(pInput, resample.mWidth, resample.mHeight, resample.mFilterType);
// NOTE: If output parameter is non-null, we have to return that
// image, instead of result
if (pOutput != null) {
//pOutput.setData(result.getRaster()); // Fast, but less compatible
// NOTE: For some reason, this is sometimes super-slow...?
ImageUtil.drawOnto(pOutput, result);
result = pOutput;
}
break;
default:
// Simply fall through, allowing acceleration to be added later
break;
}
return result;
}
private static BufferedImage resampleMagick(BufferedImage pSrc, int pWidth, int pHeight, int pFilterType) {
// Convert to Magick, scale and convert back
MagickImage image = null;
MagickImage scaled = null;
try {
image = MagickUtil.toMagick(pSrc);
long start = 0;
if (DEBUG) {
start = System.currentTimeMillis();
}
// NOTE: setFilter affects zoomImage, NOT scaleImage
image.setFilter(pFilterType);
scaled = image.zoomImage(pWidth, pHeight);
//scaled = image.scaleImage(pWidth, pHeight); // AREA_AVERAGING
if (DEBUG) {
long time = System.currentTimeMillis() - start;
System.out.println("Filtered: " + time + " ms");
}
return MagickUtil.toBuffered(scaled);
}
//catch (MagickException e) {
catch (Exception e) {
// NOTE: Stupid workaround: If MagickException is caught, a
// NoClassDefFoundError is thrown, when MagickException class is
// unavailable...
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new ImageConversionException(e.getMessage(), e);
}
finally {
// NOTE: ImageMagick might be unstable after a while, if image data
// is not deallocated. The GC/finalize method handles this, but in
// special circumstances, it's not triggered often enough.
if (image != null) {
image.destroyImages();
}
if (scaled != null) {
scaled.destroyImages();
}
}
}
}

View File

@@ -0,0 +1,549 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import magick.*;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
/**
* Utility for converting JMagick {@code MagickImage}s to standard Java
* {@code BufferedImage}s and back.
* <p/>
* <em>NOTE: This class is considered an implementation detail and not part of
* the public API. This class is subject to change without further notice.
* You have been warned. :-)</em>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java#4 $
*/
public final class MagickUtil {
// IMPORTANT NOTE: Disaster happens if any of these constants are used outside this class
// because you then have a dependency on MagickException (this is due to Java class loading
// and initialization magic).
// Do not use outside this class. If the constants need to be shared, move to Magick or ImageUtil.
/** Color Model usesd for bilevel (B/W) */
private static final IndexColorModel CM_MONOCHROME = MonochromeColorModel.getInstance();
/** Color Model usesd for raw ABGR */
private static final ColorModel CM_COLOR_ALPHA =
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8, 8},
true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
/** Color Model usesd for raw BGR */
private static final ColorModel CM_COLOR_OPAQUE =
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8},
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
/** Color Model usesd for raw RGB */
//private static final ColorModel CM_COLOR_RGB = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x0);
/** Color Model usesd for raw GRAY + ALPHA */
private static final ColorModel CM_GRAY_ALPHA =
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
/** Color Model usesd for raw GRAY */
private static final ColorModel CM_GRAY_OPAQUE =
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
/** Band offsets for raw ABGR */
private static final int[] BAND_OFF_TRANS = new int[] {3, 2, 1, 0};
/** Band offsets for raw BGR */
private static final int[] BAND_OFF_OPAQUE = new int[] {2, 1, 0};
/** The point at {@code 0, 0} */
private static final Point LOCATION_UPPER_LEFT = new Point(0, 0);
private static final boolean DEBUG = Magick.DEBUG;
// Only static members and methods
private MagickUtil() {}
/**
* Converts a {@code MagickImage} to a {@code BufferedImage}.
* <p/>
* The conversion depends on {@code pImage}'s {@code ImageType}:
* <dl>
* <dt>{@code ImageType.BilevelType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_BYTE_BINARY}</dd>
*
* <dt>{@code ImageType.GrayscaleType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_BYTE_GRAY}</dd>
* <dt>{@code ImageType.GrayscaleMatteType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_USHORT_GRAY}</dd>
*
* <dt>{@code ImageType.PaletteType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images
* with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}</dd>
* <dt>{@code ImageType.PaletteMatteType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images
* with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}</dd>
*
* <dt>{@code ImageType.TrueColorType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_3BYTE_BGR}</dd>
* <dt>{@code ImageType.TrueColorPaletteType}</dt>
* <dd>{@code BufferedImage} of type {@code TYPE_4BYTE_ABGR}</dd>
*
* @param pImage the original {@code MagickImage}
* @return a new {@code BufferedImage}
*
* @throws IllegalArgumentException if {@code pImage} is {@code null}
* or if the {@code ImageType} is not one mentioned above.
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
public static BufferedImage toBuffered(MagickImage pImage) throws MagickException {
if (pImage == null) {
throw new IllegalArgumentException("image == null");
}
long start = 0L;
if (DEBUG) {
start = System.currentTimeMillis();
}
BufferedImage image = null;
try {
switch (pImage.getImageType()) {
case ImageType.BilevelType:
image = bilevelToBuffered(pImage);
break;
case ImageType.GrayscaleType:
image = grayToBuffered(pImage, false);
break;
case ImageType.GrayscaleMatteType:
image = grayToBuffered(pImage, true);
break;
case ImageType.PaletteType:
image = paletteToBuffered(pImage, false);
break;
case ImageType.PaletteMatteType:
image = paletteToBuffered(pImage, true);
break;
case ImageType.TrueColorType:
image = rgbToBuffered(pImage, false);
break;
case ImageType.TrueColorMatteType:
image = rgbToBuffered(pImage, true);
break;
case ImageType.ColorSeparationType:
case ImageType.ColorSeparationMatteType:
case ImageType.OptimizeType:
default:
throw new IllegalArgumentException("Unknown JMagick image type: " + pImage.getImageType());
}
}
finally {
if (DEBUG) {
long time = System.currentTimeMillis() - start;
System.out.println("Converted JMagick image type: " + pImage.getImageType() + " to BufferedImage: " + image);
System.out.println("Conversion to BufferedImage: " + time + " ms");
}
}
return image;
}
/**
* Converts a {@code BufferedImage} to a {@code MagickImage}.
* <p/>
* The conversion depends on {@code pImage}'s {@code ColorModel}:
* <dl>
* <dt>{@code IndexColorModel} with 1 bit b/w</dt>
* <dd>{@code MagickImage} of type {@code ImageType.BilevelType}</dd>
* <dt>{@code IndexColorModel} &gt; 1 bit,</dt>
* <dd>{@code MagickImage} of type {@code ImageType.PaletteType}
* or {@code MagickImage} of type {@code ImageType.PaletteMatteType}
* depending on <tt>ColorModel.getAlpha()</dd>
*
* <dt>{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY}</dt>
* <dd>{@code MagickImage} of type {@code ImageType.GrayscaleType}
* or {@code MagickImage} of type {@code ImageType.GrayscaleMatteType}
* depending on <tt>ColorModel.getAlpha()</dd>
*
* <dt>{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_RGB}</dt>
* <dd>{@code MagickImage} of type {@code ImageType.TrueColorType}
* or {@code MagickImage} of type {@code ImageType.TrueColorPaletteType}</dd>
*
* @param pImage the original {@code BufferedImage}
* @return a new {@code MagickImage}
*
* @throws IllegalArgumentException if {@code pImage} is {@code null}
* or if the {@code ColorModel} is not one mentioned above.
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
public static MagickImage toMagick(BufferedImage pImage) throws MagickException {
if (pImage == null) {
throw new IllegalArgumentException("image == null");
}
long start = 0L;
if (DEBUG) {
start = System.currentTimeMillis();
}
try {
ColorModel cm = pImage.getColorModel();
if (cm instanceof IndexColorModel) {
// Handles both BilevelType, PaletteType and PaletteMatteType
return indexedToMagick(pImage, (IndexColorModel) cm, cm.hasAlpha());
}
switch (cm.getColorSpace().getType()) {
case ColorSpace.TYPE_GRAY:
// Handles GrayType and GrayMatteType
return grayToMagick(pImage, cm.hasAlpha());
case ColorSpace.TYPE_RGB:
// Handles TrueColorType and TrueColorMatteType
return rgbToMagic(pImage, cm.hasAlpha());
case ColorSpace.TYPE_CMY:
case ColorSpace.TYPE_CMYK:
case ColorSpace.TYPE_HLS:
case ColorSpace.TYPE_HSV:
// Other types not supported yet
default:
throw new IllegalArgumentException("Unknown buffered image type: " + pImage);
}
}
finally {
if (DEBUG) {
long time = System.currentTimeMillis() - start;
System.out.println("Conversion to MagickImage: " + time + " ms");
}
}
}
private static MagickImage rgbToMagic(BufferedImage pImage, boolean pAlpha) throws MagickException {
MagickImage image = new MagickImage();
BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
// Need to get data of sub raster, not the full data array, this is
// just a convenient way
Raster raster;
if (buffered.getRaster().getParent() != null) {
raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight()));
}
else {
raster = buffered.getRaster();
}
image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "BGR",
((DataBufferByte) raster.getDataBuffer()).getData());
return image;
}
private static MagickImage grayToMagick(BufferedImage pImage, boolean pAlpha) throws MagickException {
MagickImage image = new MagickImage();
// TODO: Make a fix for TYPE_USHORT_GRAY
// The code below does not seem to work (JMagick issues?)...
/*
if (pImage.getType() == BufferedImage.TYPE_USHORT_GRAY) {
short[] data = ((DataBufferUShort) pImage.getRaster().getDataBuffer()).getData();
int[] intData = new int[data.length];
for (int i = 0; i < data.length; i++) {
intData[i] = (data[i] & 0xffff) * 0xffff;
}
image.constituteImage(pImage.getWidth(), pImage.getHeight(), "I", intData);
System.out.println("storageClass: " + image.getStorageClass());
System.out.println("depth: " + image.getDepth());
System.out.println("imageType: " + image.getImageType());
}
else {
*/
BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_BYTE_GRAY);
// Need to get data of sub raster, not the full data array, this is
// just a convenient way
Raster raster;
if (buffered.getRaster().getParent() != null) {
raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight()));
}
else {
raster = buffered.getRaster();
}
image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "I", ((DataBufferByte) raster.getDataBuffer()).getData());
//}
return image;
}
private static MagickImage indexedToMagick(BufferedImage pImage, IndexColorModel pColorModel, boolean pAlpha) throws MagickException {
MagickImage image = rgbToMagic(pImage, pAlpha);
int mapSize = pColorModel.getMapSize();
image.setNumberColors(mapSize);
return image;
}
/*
public static MagickImage toMagick(BufferedImage pImage) throws MagickException {
if (pImage == null) {
throw new IllegalArgumentException("image == null");
}
final int width = pImage.getWidth();
final int height = pImage.getHeight();
// int ARGB -> byte RGBA conversion
// NOTE: This is ImageMagick Q16 compatible raw RGBA format with 16 bits/sample...
// For a Q8 build, we could probably go with half the space...
// NOTE: This is close to insanity, as it wastes extreme ammounts of memory
final int[] argb = new int[width];
final byte[] raw16 = new byte[width * height * 8];
for (int y = 0; y < height; y++) {
// Fetch one line of ARGB data
pImage.getRGB(0, y, width, 1, argb, 0, width);
for (int x = 0; x < width; x++) {
int pixel = (x + (y * width)) * 8;
raw16[pixel ] = (byte) ((argb[x] >> 16) & 0xff); // R
raw16[pixel + 2] = (byte) ((argb[x] >> 8) & 0xff); // G
raw16[pixel + 4] = (byte) ((argb[x] ) & 0xff); // B
raw16[pixel + 6] = (byte) ((argb[x] >> 24) & 0xff); // A
}
}
// Create magick image
ImageInfo info = new ImageInfo();
info.setMagick("RGBA"); // Raw RGBA samples
info.setSize(width + "x" + height); // String?!?
MagickImage image = new MagickImage(info);
image.setImageAttribute("depth", "8");
// Set pixel data in 16 bit raw RGBA format
image.blobToImage(info, raw16);
return image;
}
*/
/**
* Converts a bi-level {@code MagickImage} to a {@code BufferedImage}, of
* type {@code TYPE_BYTE_BINARY}.
*
* @param pImage the original {@code MagickImage}
* @return a new {@code BufferedImage}
*
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
private static BufferedImage bilevelToBuffered(MagickImage pImage) throws MagickException {
// As there is no way to get the binary representation of the image,
// convert to gray, and the create a binary image from it
BufferedImage temp = grayToBuffered(pImage, false);
BufferedImage image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, CM_MONOCHROME);
ImageUtil.drawOnto(image, temp);
return image;
}
/**
* Converts a gray {@code MagickImage} to a {@code BufferedImage}, of
* type {@code TYPE_USHORT_GRAY} or {@code TYPE_BYTE_GRAY}.
*
* @param pImage the original {@code MagickImage}
* @param pAlpha keep alpha channel
* @return a new {@code BufferedImage}
*
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
private static BufferedImage grayToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException {
Dimension size = pImage.getDimension();
int length = size.width * size.height;
int bands = pAlpha ? 2 : 1;
byte[] pixels = new byte[length * bands];
// TODO: Make a fix for 16 bit TYPE_USHORT_GRAY?!
// Note: The ordering AI or I corresponds to BufferedImage
// TYPE_CUSTOM and TYPE_BYTE_GRAY respectively
pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "AI" : "I", pixels);
// Init databuffer with array, to avoid allocation of empty array
DataBuffer buffer = new DataBufferByte(pixels, pixels.length);
int[] bandOffsets = pAlpha ? new int[] {1, 0} : new int[] {0};
WritableRaster raster =
Raster.createInterleavedRaster(buffer, size.width, size.height,
size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT);
return new BufferedImage(pAlpha ? CM_GRAY_ALPHA : CM_GRAY_OPAQUE, raster, pAlpha, null);
}
/**
* Converts a palette-based {@code MagickImage} to a
* {@code BufferedImage}, of type {@code TYPE_BYTE_BINARY} (for images
* with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}.
*
* @param pImage the original {@code MagickImage}
* @param pAlpha keep alpha channel
* @return a new {@code BufferedImage}
*
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
private static BufferedImage paletteToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException {
// Create indexcolormodel for the image
IndexColorModel cm;
try {
cm = createIndexColorModel(pImage.getColormap(), pAlpha);
}
catch (MagickException e) {
// NOTE: Some MagickImages incorrecly (?) reports to be paletteType,
// but does not have a colormap, this is a workaround.
return rgbToBuffered(pImage, pAlpha);
}
// As there is no way to get the indexes of an indexed image, convert to
// RGB, and the create an indexed image from it
BufferedImage temp = rgbToBuffered(pImage, pAlpha);
BufferedImage image;
if (cm.getMapSize() <= 16) {
image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, cm);
}
else {
image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm);
}
// Create transparent background for images containing alpha
if (pAlpha) {
Graphics2D g = image.createGraphics();
try {
g.setComposite(AlphaComposite.Clear);
g.fillRect(0, 0, temp.getWidth(), temp.getHeight());
}
finally {
g.dispose();
}
}
// NOTE: This is (surprisingly) much faster than using g2d.drawImage()..
// (Tests shows 20-30ms, vs. 600-700ms on the same image)
BufferedImageOp op = new CopyDither(cm);
op.filter(temp, image);
return image;
}
/**
* Creates an {@code IndexColorModel} from an array of
* {@code PixelPacket}s.
*
* @param pColormap the original colormap as a {@code PixelPacket} array
* @param pAlpha keep alpha channel
*
* @return a new {@code IndexColorModel}
*/
public static IndexColorModel createIndexColorModel(PixelPacket[] pColormap, boolean pAlpha) {
int[] colors = new int[pColormap.length];
// TODO: Verify if this is correct for alpha...?
int trans = pAlpha ? colors.length - 1 : -1;
//for (int i = 0; i < pColormap.length; i++) {
for (int i = pColormap.length - 1; i != 0; i--) {
PixelPacket color = pColormap[i];
if (pAlpha) {
colors[i] = (0xff - (color.getOpacity() & 0xff)) << 24 |
(color.getRed() & 0xff) << 16 |
(color.getGreen() & 0xff) << 8 |
(color.getBlue() & 0xff);
}
else {
colors[i] = (color.getRed() & 0xff) << 16 |
(color.getGreen() & 0xff) << 8 |
(color.getBlue() & 0xff);
}
}
return new InverseColorMapIndexColorModel(8, colors.length, colors, 0, pAlpha, trans, DataBuffer.TYPE_BYTE);
}
/**
* Converts an (A)RGB {@code MagickImage} to a {@code BufferedImage}, of
* type {@code TYPE_4BYTE_ABGR} or {@code TYPE_3BYTE_BGR}.
*
* @param pImage the original {@code MagickImage}
* @param pAlpha keep alpha channel
* @return a new {@code BufferedImage}
*
* @throws MagickException if an exception occurs during conversion
*
* @see BufferedImage
*/
private static BufferedImage rgbToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException {
Dimension size = pImage.getDimension();
int length = size.width * size.height;
int bands = pAlpha ? 4 : 3;
byte[] pixels = new byte[length * bands];
// TODO: If we do multiple dispatches (one per line, typically), we could provide listener
// feedback. But it's currently a lot slower than fetching all the pixels in one go.
// Note: The ordering ABGR or BGR corresponds to BufferedImage
// TYPE_4BYTE_ABGR and TYPE_3BYTE_BGR respectively
pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ABGR" : "BGR", pixels);
// Init databuffer with array, to avoid allocation of empty array
DataBuffer buffer = new DataBufferByte(pixels, pixels.length);
int[] bandOffsets = pAlpha ? BAND_OFF_TRANS : BAND_OFF_OPAQUE;
WritableRaster raster =
Raster.createInterleavedRaster(buffer, size.width, size.height,
size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT);
return new BufferedImage(pAlpha ? CM_COLOR_ALPHA : CM_COLOR_OPAQUE, raster, pAlpha, null);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.*;
/**
* Monochrome B/W color model.
*
* @author Harald Kuhr
*/
public class MonochromeColorModel extends IndexColorModel {
private final static int[] MONO_PALETTE = {0x00000000, 0x00FFFFFF};
private static MonochromeColorModel sInstance = new MonochromeColorModel();
private MonochromeColorModel() {
super(1, 2, MONO_PALETTE, 0, false, -1, DataBuffer.TYPE_BYTE);
}
public static IndexColorModel getInstance() {
return sInstance;
}
public synchronized Object getDataElements(int pRGB, Object pPixel) {
// Get color components
int r = pRGB >> 16 & 0xFF;
int g = pRGB >> 8 & 0xFF;
int b = pRGB & 0xFF;
// ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000
int gray = (222 * r + 707 * g + 71 * b) / 1000;
byte[] pixel;
if (pPixel != null) {
pixel = (byte[]) pPixel;
}
else {
pixel = new byte[1];
}
if (gray <= 0x80) {
pixel[0] = 0;
}
else {
pixel[0] = 1;
}
return pixel;
}
}

View File

@@ -0,0 +1,378 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
/**
* PixelizeOp
*
* @author <a href="mailto:harald.kuhr@gmail.no">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/PixelizeOp.java#2 $
*/
public class PixelizeOp implements BufferedImageOp, RasterOp {
// TODO: support more raster types/color models
// TODO: This is actually an implementation of Area Averaging, without the scale... Let's extract it...
final private int mPixelSizeX;
final private int mPixelSizeY;
private Rectangle mSourceRegion;
public PixelizeOp(final int pPixelSize) {
this(pPixelSize, pPixelSize);
}
public PixelizeOp(final int pPixelSizeX, final int pPixelSizeY) {
mPixelSizeX = pPixelSizeX;
mPixelSizeY = pPixelSizeY;
}
public Rectangle getSourceRegion() {
if (mSourceRegion == null) {
return null;
}
return new Rectangle(mSourceRegion);
}
public void setSourceRegion(final Rectangle pSourceRegion) {
if (pSourceRegion == null) {
mSourceRegion = null;
}
else {
if (mSourceRegion == null) {
mSourceRegion = new Rectangle(pSourceRegion);
}
else {
mSourceRegion.setBounds(pSourceRegion);
}
}
}
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
BufferedImage result = dest != null ? dest : createCompatibleDestImage(src, null);
// TODO: Do some type checking here..
// Should work with
// * all BYTE types, unless sub-byte packed rasters/IndexColorModel
// * all INT types (even custom, as long as they use 8bit/componnet)
// * all USHORT types (even custom)
// TODO: Also check if the images are really compatible!?
filterImpl(src.getRaster(), result.getRaster());
return result;
}
public WritableRaster filter(Raster src, WritableRaster dest) {
WritableRaster result = dest != null ? dest : createCompatibleDestRaster(src);
return filterImpl(src, result);
}
private WritableRaster filterImpl(Raster src, WritableRaster dest) {
//System.out.println("src: " + src);
//System.out.println("dest: " + dest);
if (mSourceRegion != null) {
int cx = mSourceRegion.x;
int cy = mSourceRegion.y;
int cw = mSourceRegion.width;
int ch = mSourceRegion.height;
boolean same = src == dest;
dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null);
src = same ? dest : src.createChild(cx, cy, cw, ch, 0, 0, null);
//System.out.println("src: " + src);
//System.out.println("dest: " + dest);
}
final int width = src.getWidth();
final int height = src.getHeight();
int w = (width + mPixelSizeX - 1) / mPixelSizeX;
int h = (height + mPixelSizeY - 1) / mPixelSizeY;
final boolean oddX = width % w != 0;
final boolean oddY = height % h != 0;
final int dataElements = src.getNumDataElements();
final int bands = src.getNumBands();
final int dataType = src.getTransferType();
Object data = null;
int scanW;
int scanH;
// TYPE_USHORT setup
int[] bitMasks = null;
int[] bitOffsets = null;
if (src.getTransferType() == DataBuffer.TYPE_USHORT) {
if (src.getSampleModel() instanceof SinglePixelPackedSampleModel) {
// DIRECT
SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel) src.getSampleModel();
bitMasks = sampleModel.getBitMasks();
bitOffsets = sampleModel.getBitOffsets();
}
else {
// GRAY
bitMasks = new int[] {0xffff};
bitOffsets = new int[] {0};
}
}
for (int y = 0; y < h; y++) {
if (!oddY || y + 1 < h) {
scanH = mPixelSizeY;
}
else {
scanH = height - (y * mPixelSizeY);
}
for (int x = 0; x < w; x++) {
if (!oddX || x + 1 < w) {
scanW = mPixelSizeX;
}
else {
scanW = width - (x * mPixelSizeX);
}
final int pixelCount = scanW * scanH;
final int pixelLength = pixelCount * dataElements;
data = src.getDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data);
// NOTE: These are not neccessarily ARGB..
double valueA = 0.0;
double valueR = 0.0;
double valueG = 0.0;
double valueB = 0.0;
switch (dataType) {
case DataBuffer.TYPE_BYTE:
// TODO: Doesn't hold for index color models...
byte[] bytePixels = (byte[]) data;
for (int i = 0; i < pixelLength; i += dataElements) {
valueA += bytePixels[i] & 0xff;
if (bands > 1) {
valueR += bytePixels[i + 1] & 0xff;
valueG += bytePixels[i + 2] & 0xff;
if (bands > 3) {
valueB += bytePixels[i + 3] & 0xff;
}
}
}
// Average
valueA /= pixelCount;
if (bands > 1) {
valueR /= pixelCount;
valueG /= pixelCount;
if (bands > 3) {
valueB /= pixelCount;
}
}
for (int i = 0; i < pixelLength; i += dataElements) {
bytePixels[i] = (byte) clamp((int) valueA);
if (bands > 1) {
bytePixels[i + 1] = (byte) clamp((int) valueR);
bytePixels[i + 2] = (byte) clamp((int) valueG);
if (bands > 3) {
bytePixels[i + 3] = (byte) clamp((int) valueB);
}
}
}
break;
case DataBuffer.TYPE_INT:
int[] intPixels = (int[]) data;
for (int i = 0; i < pixelLength; i += dataElements) {
valueA += (intPixels[i] & 0xff000000) >> 24;
valueR += (intPixels[i] & 0xff0000) >> 16;
valueG += (intPixels[i] & 0xff00) >> 8;
valueB += (intPixels[i] & 0xff);
}
// Average
valueA /= pixelCount;
valueR /= pixelCount;
valueG /= pixelCount;
valueB /= pixelCount;
for (int i = 0; i < pixelLength; i += dataElements) {
intPixels[i] = clamp((int) valueA) << 24;
intPixels[i] |= clamp((int) valueR) << 16;
intPixels[i] |= clamp((int) valueG) << 8;
intPixels[i] |= clamp((int) valueB);
}
break;
case DataBuffer.TYPE_USHORT:
if (bitMasks != null) {
short[] shortPixels = (short[]) data;
for (int i = 0; i < pixelLength; i += dataElements) {
valueA += (shortPixels[i] & bitMasks[0]) >> bitOffsets[0];
if (bitMasks.length > 1) {
valueR += (shortPixels[i] & bitMasks[1]) >> bitOffsets[1];
valueG += (shortPixels[i] & bitMasks[2]) >> bitOffsets[2];
if (bitMasks.length > 3) {
valueB += (shortPixels[i] & bitMasks[3]) >> bitOffsets[3];
}
}
}
// Average
valueA /= pixelCount;
valueR /= pixelCount;
valueG /= pixelCount;
valueB /= pixelCount;
for (int i = 0; i < pixelLength; i += dataElements) {
shortPixels[i] = (short) (((int) valueA << bitOffsets[0]) & bitMasks[0]);
if (bitMasks.length > 1) {
shortPixels[i] |= (short) (((int) valueR << bitOffsets[1]) & bitMasks[1]);
shortPixels[i] |= (short) (((int) valueG << bitOffsets[2]) & bitMasks[2]);
if (bitMasks.length > 3) {
shortPixels[i] |= (short) (((int) valueB << bitOffsets[3]) & bitMasks[3]);
}
}
}
break;
}
default:
throw new IllegalArgumentException("TransferType not supported: " + dataType);
}
dest.setDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data);
}
}
/*/
// This is a very naive way of pixelizing (but it works)...
// Thanks to the awsome speed of AffineTransformOp, it's also fast
double sx = w / (double) src.getWidth();
double sy = h / (double) src.getHeight();
WritableRaster temp = src.createCompatibleWritableRaster(w, h);
new AffineTransformOp(AffineTransform.getScaleInstance(sx, sy), 3)
.filter(src, temp);
new AffineTransformOp(AffineTransform.getScaleInstance(1 / sx, 1 / sy),
AffineTransformOp.TYPE_NEAREST_NEIGHBOR)
.filter(temp, dest);
//*/
return dest;
}
private static int clamp(final int pValue) {
return pValue > 255 ? 255 : pValue;
}
public RenderingHints getRenderingHints() {
return null;
}
// TODO: Refactor boilerplate to AbstractBufferedImageOp or use a delegate?
// Delegate is maybe better as we won't always implement both BIOp and RasterOP
// (but are there ever any time we want to implemnet RasterOp and not BIOp?)
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
ColorModel cm = destCM != null ? destCM : src.getColorModel();
return new BufferedImage(cm,
ImageUtil.createCompatibleWritableRaster(src, cm, src.getWidth(), src.getHeight()),
cm.isAlphaPremultiplied(), null);
}
public WritableRaster createCompatibleDestRaster(Raster src) {
return src.createCompatibleWritableRaster();
}
public Rectangle2D getBounds2D(Raster src) {
return new Rectangle(src.getWidth(), src.getHeight());
}
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle(src.getWidth(), src.getHeight());
}
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
if (dstPt == null) {
if (srcPt instanceof Point2D.Double) {
dstPt = new Point2D.Double();
}
else {
dstPt = new Point2D.Float();
}
}
dstPt.setLocation(srcPt);
return dstPt;
}
public static void main(String[] pArgs) throws IOException {
BufferedImage image = ImageIO.read(new File("2006-Lamborghini-Gallardo-Spyder-Y-T-1600x1200.png"));
//BufferedImage image = ImageIO.read(new File("focus-rs.jpg"));
//BufferedImage image = ImageIO.read(new File("blauesglas_16_bitmask444.bmp"));
//image = ImageUtil.toBuffered(image, BufferedImage.TYPE_USHORT_GRAY);
for (int i = 0; i < 10; i++) {
//new PixelizeOp(10).filter(image, null);
//new AffineTransformOp(AffineTransform.getScaleInstance(.1, .1), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
//ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
//new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_BOX).filter(image, null);
new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_QUADRATIC).filter(image, null);
}
long start = System.currentTimeMillis();
//PixelizeOp pixelizer = new PixelizeOp(image.getWidth() / 10, 1);
//pixelizer.setSourceRegion(new Rectangle(0, 2 * image.getHeight() / 3, image.getWidth(), image.getHeight() / 4));
//PixelizeOp pixelizer = new PixelizeOp(4);
//image = pixelizer.filter(image, image); // Filter in place, that's cool
//image = new AffineTransformOp(AffineTransform.getScaleInstance(.25, .25), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
//image = ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
//image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_BOX).filter(image, null);
image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_QUADRATIC).filter(image, null);
long time = System.currentTimeMillis() - start;
System.out.println("time: " + time + " ms");
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(new JScrollPane(new JLabel(new BufferedImageIcon(image))));
frame.pack();
frame.setVisible(true);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.image;
import java.awt.image.ReplicateScaleFilter;
/**
* An {@code ImageFilter} class for subsampling images.
* <p/>
* It is meant to be used in conjunction with a {@code FilteredImageSource}
* object to produce subsampled versions of existing images.
*
* @see java.awt.image.FilteredImageSource
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java#1 $
*/
public class SubsamplingFilter extends ReplicateScaleFilter {
private int mXSub;
private int mYSub;
/**
* Creates a {@code SubsamplingFilter}.
*
* @param pXSub
* @param pYSub
*
* @throws IllegalArgumentException if {@code pXSub} or {@code pYSub} is
* less than 1.
*/
public SubsamplingFilter(int pXSub, int pYSub) {
super(1, 1); // These are NOT REAL values, but we have to defer setting
// until w/h is available, in setDimensions below
if (pXSub < 1 || pYSub < 1) {
throw new IllegalArgumentException("Subsampling factors must be positive.");
}
mXSub = pXSub;
mYSub = pYSub;
}
/** {@code ImageFilter} implementation, do not invoke. */
public void setDimensions(int pWidth, int pHeight) {
destWidth = (pWidth + mXSub - 1) / mXSub;
destHeight = (pHeight + mYSub - 1) / mYSub;
//System.out.println("Subsampling: " + mXSub + "," + mYSub + "-> " + destWidth + ", " + destHeight);
super.setDimensions(pWidth, pHeight);
}
}

View File

@@ -0,0 +1,524 @@
/*
* This software is copyrighted as noted below. It may be freely copied,
* modified, and redistributed, provided that the copyright notice is
* preserved on all copies.
*
* There is no warranty or other guarantee of fitness for this software,
* it is provided solely "as is". Bug reports or fixes may be sent
* to the author, who may or may not act on them as he desires.
*
* You may not include this software in a program or other software product
* without supplying the source, or without informing the end-user that the
* source is available for no extra charge.
*
* If you modify this software, you should include a notice giving the
* name of the person performing the modification, the date of modification,
* and the reason for such modification.
*/
/*
* inv_cmap.c - Compute an inverse colormap.
*
* Author: Spencer W. Thomas
* EECS Dept.
* University of Michigan
* Date: Thu Sep 20 1990
* Copyright (c) 1990, University of Michigan
*
* $Id: inv_cmap.c,v 3.0.1.3 1992/04/30 14:07:28 spencer Exp $
*/
#include <math.h>
#include <stdio.h>
static int bcenter, gcenter, rcenter;
static long gdist, rdist, cdist;
static long cbinc, cginc, crinc;
static unsigned long *gdp, *rdp, *cdp;
static unsigned char *grgbp, *rrgbp, *crgbp;
static gstride, rstride;
static long x, xsqr, colormax;
static int cindex;
#ifdef USE_PROTOTYPES
static void maxfill( unsigned long *, long );
static int redloop( void );
static int greenloop( int );
static int blueloop( int );
#else
static void maxfill();
static int redloop();
static int greenloop();
static int blueloop();
#endif
/*****************************************************************
* TAG( inv_cmap )
*
* Compute an inverse colormap efficiently.
* Inputs:
* colors: Number of colors in the forward colormap.
* colormap: The forward colormap.
* bits: Number of quantization bits. The inverse
* colormap will have (2^bits)^3 entries.
* dist_buf: An array of (2^bits)^3 long integers to be
* used as scratch space.
* Outputs:
* rgbmap: The output inverse colormap. The entry
* rgbmap[(r<<(2*bits)) + (g<<bits) + b]
* is the colormap entry that is closest to the
* (quantized) color (r,g,b).
* Assumptions:
* Quantization is performed by right shift (low order bits are
* truncated). Thus, the distance to a quantized color is
* actually measured to the color at the center of the cell
* (i.e., to r+.5, g+.5, b+.5, if (r,g,b) is a quantized color).
* Algorithm:
* Uses a "distance buffer" algorithm:
* The distance from each representative in the forward color map
* to each point in the rgb space is computed. If it is less
* than the distance currently stored in dist_buf, then the
* corresponding entry in rgbmap is replaced with the current
* representative (and the dist_buf entry is replaced with the
* new distance).
*
* The distance computation uses an efficient incremental formulation.
*
* Distances are computed "outward" from each color. If the
* colors are evenly distributed in color space, the expected
* number of cells visited for color I is N^3/I.
* Thus, the complexity of the algorithm is O(log(K) N^3),
* where K = colors, and N = 2^bits.
*/
/*
* Here's the idea: scan from the "center" of each cell "out"
* until we hit the "edge" of the cell -- that is, the point
* at which some other color is closer -- and stop. In 1-D,
* this is simple:
* for i := here to max do
* if closer then buffer[i] = this color
* else break
* repeat above loop with i := here-1 to min by -1
*
* In 2-D, it's trickier, because along a "scan-line", the
* region might start "after" the "center" point. A picture
* might clarify:
* | ...
* | ... .
* ... .
* ... | .
* . + .
* . .
* . .
* .........
*
* The + marks the "center" of the above region. On the top 2
* lines, the region "begins" to the right of the "center".
*
* Thus, we need a loop like this:
* detect := false
* for i := here to max do
* if closer then
* buffer[..., i] := this color
* if !detect then
* here = i
* detect = true
* else
* if detect then
* break
*
* Repeat the above loop with i := here-1 to min by -1. Note that
* the "detect" value should not be reinitialized. If it was
* "true", and center is not inside the cell, then none of the
* cell lies to the left and this loop should exit
* immediately.
*
* The outer loops are similar, except that the "closer" test
* is replaced by a call to the "next in" loop; its "detect"
* value serves as the test. (No assignment to the buffer is
* done, either.)
*
* Each time an outer loop starts, the "here", "min", and
* "max" values of the next inner loop should be
* re-initialized to the center of the cell, 0, and cube size,
* respectively. Otherwise, these values will carry over from
* one "call" to the inner loop to the next. This tracks the
* edges of the cell and minimizes the number of
* "unproductive" comparisons that must be made.
*
* Finally, the inner-most loop can have the "if !detect"
* optimized out of it by splitting it into two loops: one
* that finds the first color value on the scan line that is
* in this cell, and a second that fills the cell until
* another one is closer:
* if !detect then {needed for "down" loop}
* for i := here to max do
* if closer then
* buffer[..., i] := this color
* detect := true
* break
* for i := i+1 to max do
* if closer then
* buffer[..., i] := this color
* else
* break
*
* In this implementation, each level will require the
* following variables. Variables labelled (l) are local to each
* procedure. The ? should be replaced with r, g, or b:
* cdist: The distance at the starting point.
* ?center: The value of this component of the color
* c?inc: The initial increment at the ?center position.
* ?stride: The amount to add to the buffer
* pointers (dp and rgbp) to get to the
* "next row".
* min(l): The "low edge" of the cell, init to 0
* max(l): The "high edge" of the cell, init to
* colormax-1
* detect(l): True if this row has changed some
* buffer entries.
* i(l): The index for this row.
* ?xx: The accumulated increment value.
*
* here(l): The starting index for this color. The
* following variables are associated with here,
* in the sense that they must be updated if here
* is changed.
* ?dist: The current distance for this level. The
* value of dist from the previous level (g or r,
* for level b or g) initializes dist on this
* level. Thus gdist is associated with here(b)).
* ?inc: The initial increment for the row.
* ?dp: Pointer into the distance buffer. The value
* from the previous level initializes this level.
* ?rgbp: Pointer into the rgb buffer. The value
* from the previous level initializes this level.
*
* The blue and green levels modify 'here-associated' variables (dp,
* rgbp, dist) on the green and red levels, respectively, when here is
* changed.
*/
void
inv_cmap( colors, colormap, bits, dist_buf, rgbmap )
int colors, bits;
unsigned char *colormap[3], *rgbmap;
unsigned long *dist_buf;
{
int nbits = 8 - bits;
colormax = 1 << bits;
x = 1 << nbits;
xsqr = 1 << (2 * nbits);
/* Compute "strides" for accessing the arrays. */
gstride = colormax;
rstride = colormax * colormax;
maxfill( dist_buf, colormax );
for ( cindex = 0; cindex < colors; cindex++ )
{
/*
* Distance formula is
* (red - map[0])^2 + (green - map[1])^2 + (blue - map[2])^2
*
* Because of quantization, we will measure from the center of
* each quantized "cube", so blue distance is
* (blue + x/2 - map[2])^2,
* where x = 2^(8 - bits).
* The step size is x, so the blue increment is
* 2*x*blue - 2*x*map[2] + 2*x^2
*
* Now, b in the code below is actually blue/x, so our
* increment will be 2*(b*x^2 + x^2 - x*map[2]). For
* efficiency, we will maintain this quantity in a separate variable
* that will be updated incrementally by adding 2*x^2 each time.
*/
/* The initial position is the cell containing the colormap
* entry. We get this by quantizing the colormap values.
*/
rcenter = colormap[0][cindex] >> nbits;
gcenter = colormap[1][cindex] >> nbits;
bcenter = colormap[2][cindex] >> nbits;
rdist = colormap[0][cindex] - (rcenter * x + x/2);
gdist = colormap[1][cindex] - (gcenter * x + x/2);
cdist = colormap[2][cindex] - (bcenter * x + x/2);
cdist = rdist*rdist + gdist*gdist + cdist*cdist;
crinc = 2 * ((rcenter + 1) * xsqr - (colormap[0][cindex] * x));
cginc = 2 * ((gcenter + 1) * xsqr - (colormap[1][cindex] * x));
cbinc = 2 * ((bcenter + 1) * xsqr - (colormap[2][cindex] * x));
/* Array starting points. */
cdp = dist_buf + rcenter * rstride + gcenter * gstride + bcenter;
crgbp = rgbmap + rcenter * rstride + gcenter * gstride + bcenter;
(void)redloop();
}
}
/* redloop -- loop up and down from red center. */
static int
redloop()
{
int detect;
int r;
int first;
long txsqr = xsqr + xsqr;
static long rxx;
detect = 0;
/* Basic loop up. */
for ( r = rcenter, rdist = cdist, rxx = crinc,
rdp = cdp, rrgbp = crgbp, first = 1;
r < colormax;
r++, rdp += rstride, rrgbp += rstride,
rdist += rxx, rxx += txsqr, first = 0 )
{
if ( greenloop( first ) )
detect = 1;
else if ( detect )
break;
}
/* Basic loop down. */
for ( r = rcenter - 1, rxx = crinc - txsqr, rdist = cdist - rxx,
rdp = cdp - rstride, rrgbp = crgbp - rstride, first = 1;
r >= 0;
r--, rdp -= rstride, rrgbp -= rstride,
rxx -= txsqr, rdist -= rxx, first = 0 )
{
if ( greenloop( first ) )
detect = 1;
else if ( detect )
break;
}
return detect;
}
/* greenloop -- loop up and down from green center. */
static int
greenloop( restart )
int restart;
{
int detect;
int g;
int first;
long txsqr = xsqr + xsqr;
static int here, min, max;
static long ginc, gxx, gcdist; /* "gc" variables maintain correct */
static unsigned long *gcdp; /* values for bcenter position, */
static unsigned char *gcrgbp; /* despite modifications by blueloop */
/* to gdist, gdp, grgbp. */
if ( restart )
{
here = gcenter;
min = 0;
max = colormax - 1;
ginc = cginc;
}
detect = 0;
/* Basic loop up. */
for ( g = here, gcdist = gdist = rdist, gxx = ginc,
gcdp = gdp = rdp, gcrgbp = grgbp = rrgbp, first = 1;
g <= max;
g++, gdp += gstride, gcdp += gstride, grgbp += gstride, gcrgbp += gstride,
gdist += gxx, gcdist += gxx, gxx += txsqr, first = 0 )
{
if ( blueloop( first ) )
{
if ( !detect )
{
/* Remember here and associated data! */
if ( g > here )
{
here = g;
rdp = gcdp;
rrgbp = gcrgbp;
rdist = gcdist;
ginc = gxx;
}
detect = 1;
}
}
else if ( detect )
{
break;
}
}
/* Basic loop down. */
for ( g = here - 1, gxx = ginc - txsqr, gcdist = gdist = rdist - gxx,
gcdp = gdp = rdp - gstride, gcrgbp = grgbp = rrgbp - gstride,
first = 1;
g >= min;
g--, gdp -= gstride, gcdp -= gstride, grgbp -= gstride, gcrgbp -= gstride,
gxx -= txsqr, gdist -= gxx, gcdist -= gxx, first = 0 )
{
if ( blueloop( first ) )
{
if ( !detect )
{
/* Remember here! */
here = g;
rdp = gcdp;
rrgbp = gcrgbp;
rdist = gcdist;
ginc = gxx;
detect = 1;
}
}
else if ( detect )
{
break;
}
}
return detect;
}
/* blueloop -- loop up and down from blue center. */
static int
blueloop( restart )
int restart;
{
int detect;
register unsigned long *dp;
register unsigned char *rgbp;
register long bdist, bxx;
register int b, i = cindex;
register long txsqr = xsqr + xsqr;
register int lim;
static int here, min, max;
static long binc;
if ( restart )
{
here = bcenter;
min = 0;
max = colormax - 1;
binc = cbinc;
}
detect = 0;
/* Basic loop up. */
/* First loop just finds first applicable cell. */
for ( b = here, bdist = gdist, bxx = binc, dp = gdp, rgbp = grgbp, lim = max;
b <= lim;
b++, dp++, rgbp++,
bdist += bxx, bxx += txsqr )
{
if ( *dp > bdist )
{
/* Remember new 'here' and associated data! */
if ( b > here )
{
here = b;
gdp = dp;
grgbp = rgbp;
gdist = bdist;
binc = bxx;
}
detect = 1;
break;
}
}
/* Second loop fills in a run of closer cells. */
for ( ;
b <= lim;
b++, dp++, rgbp++,
bdist += bxx, bxx += txsqr )
{
if ( *dp > bdist )
{
*dp = bdist;
*rgbp = i;
}
else
{
break;
}
}
/* Basic loop down. */
/* Do initializations here, since the 'find' loop might not get
* executed.
*/
lim = min;
b = here - 1;
bxx = binc - txsqr;
bdist = gdist - bxx;
dp = gdp - 1;
rgbp = grgbp - 1;
/* The 'find' loop is executed only if we didn't already find
* something.
*/
if ( !detect )
for ( ;
b >= lim;
b--, dp--, rgbp--,
bxx -= txsqr, bdist -= bxx )
{
if ( *dp > bdist )
{
/* Remember here! */
/* No test for b against here necessary because b <
* here by definition.
*/
here = b;
gdp = dp;
grgbp = rgbp;
gdist = bdist;
binc = bxx;
detect = 1;
break;
}
}
/* The 'update' loop. */
for ( ;
b >= lim;
b--, dp--, rgbp--,
bxx -= txsqr, bdist -= bxx )
{
if ( *dp > bdist )
{
*dp = bdist;
*rgbp = i;
}
else
{
break;
}
}
/* If we saw something, update the edge trackers. */
return detect;
}
static void
maxfill( buffer, side )
unsigned long *buffer;
long side;
{
register unsigned long maxv = ~0L;
register long i;
register unsigned long *bp;
for ( i = side * side * side, bp = buffer;
i > 0;
i--, bp++ )
*bp = maxv;
}

View File

@@ -0,0 +1,9 @@
/**
* Classes for image manipulation.
* <p/>
* See the class {@link com.twelvemonkeys.image.ImageUtil}.
*
* @version 1.0
* @author <a href="mailto:harald@escenic.com">Harald Kuhr</a>
*/
package com.twelvemonkeys.image;

View File

@@ -0,0 +1,574 @@
package com.twelvemonkeys.image;
import junit.framework.TestCase;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
/**
* Created by IntelliJ IDEA.
*
* @author $author wmhakur$
* @version $id: $
* To change this template use Options | File Templates.
*/
public class ImageUtilTestCase extends TestCase {
private final static String IMAGE_NAME = "/sunflower.jpg";
private BufferedImage mOriginal;
private BufferedImage mImage;
private Image mScaled;
public ImageUtilTestCase() throws Exception {
mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST);
// Read image from class path
InputStream is = getClass().getResourceAsStream(IMAGE_NAME);
mOriginal = ImageIO.read(is);
assertNotNull(mOriginal);
}
/*
public void setUp() throws Exception {
mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST);
// Read image from class path
InputStream is = ClassLoader.getSystemResourceAsStream(IMAGE_NAME);
mOriginal = ImageIO.read(is);
assertNotNull(mOriginal);
}
protected void tearDown() throws Exception {
mOriginal = null;
}
*/
public void testToBufferedImageNull() {
BufferedImage img = null;
boolean threwRuntimeException = false;
try {
img = ImageUtil.toBuffered((Image) null);
}
catch (RuntimeException ne) {
threwRuntimeException = true;
}
// No input should return null
assertNull(img);
// Should have thrown an exception
assertTrue(threwRuntimeException);
}
public void testToBufferedImageTypeNull() {
BufferedImage img = null;
boolean threwRuntimeException = false;
try {
img = ImageUtil.toBuffered(null, BufferedImage.TYPE_INT_ARGB);
}
catch (RuntimeException ne) {
threwRuntimeException = true;
}
// No input should return null
assertNull(img);
// Should have thrown an exception
assertTrue(threwRuntimeException);
}
public void testImageIsNotBufferedImage() {
// Should not be a buffered image
assertFalse(
"FOR SOME IMPLEMENTATIONS THIS MIGHT FAIL!\nIn that case, testToBufferedImage() will fail too.",
mScaled instanceof BufferedImage
);
}
public void testToBufferedImage() {
BufferedImage sameAsImage = ImageUtil.toBuffered((RenderedImage) mImage);
BufferedImage bufferedScaled = ImageUtil.toBuffered(mScaled);
// Should be no need to convert
assertSame(mImage, sameAsImage);
// Should have same dimensions
assertEquals(mScaled.getWidth(null), bufferedScaled.getWidth());
assertEquals(mScaled.getHeight(null), bufferedScaled.getHeight());
// Hmmm...
assertTrue(new Integer(42).equals(bufferedScaled.getProperty("lucky-number"))
|| bufferedScaled.getPropertyNames() == null
|| bufferedScaled.getPropertyNames().length == 0);
}
public void testToBufferedImageType() {
// Assumes mImage is TYPE_INT_ARGB
BufferedImage converted = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_INDEXED);
BufferedImage convertedToo = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_BINARY);
// Should not be the same
assertNotSame(mImage, converted);
assertNotSame(mImage, convertedToo);
// Correct type
assertTrue(converted.getType() == BufferedImage.TYPE_BYTE_INDEXED);
assertTrue(convertedToo.getType() == BufferedImage.TYPE_BYTE_BINARY);
// Should have same dimensions
assertEquals(mImage.getWidth(), converted.getWidth());
assertEquals(mImage.getHeight(), converted.getHeight());
assertEquals(mImage.getWidth(), convertedToo.getWidth());
assertEquals(mImage.getHeight(), convertedToo.getHeight());
}
public void testBrightness() {
final BufferedImage original = mOriginal;
assertNotNull(original);
final BufferedImage notBrightened = ImageUtil.toBuffered(ImageUtil.brightness(original, 0f));
// Assumed: Images should be equal
if (original != notBrightened) { // Don't care to test if images are same
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertEquals(original.getRGB(x, y), notBrightened.getRGB(x, y));
}
}
}
// Assumed: All pixels should be brighter or equal to original
final BufferedImage brightened = ImageUtil.toBuffered(ImageUtil.brightness(original, 0.4f));
final BufferedImage brightenedMore = ImageUtil.toBuffered(ImageUtil.brightness(original, 0.9f));
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertTrue(original.getRGB(x, y) <= brightened.getRGB(x, y));
assertTrue(brightened.getRGB(x, y) <= brightenedMore.getRGB(x, y));
}
}
// Assumed: Image should be all white
final BufferedImage brightenedMax = ImageUtil.toBuffered(ImageUtil.brightness(original, 2f));
for (int y = 0; y < brightenedMax.getHeight(); y++) {
for (int x = 0; x < brightenedMax.getWidth(); x++) {
assertEquals(0x00FFFFFF, brightenedMax.getRGB(x, y) & 0x00FFFFFF);
}
}
// Assumed: All pixels should be darker or equal to originial
final BufferedImage brightenedNegative = ImageUtil.toBuffered(ImageUtil.brightness(original, -0.4f));
final BufferedImage brightenedNegativeMore = ImageUtil.toBuffered(ImageUtil.brightness(original, -0.9f));
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertTrue(original.getRGB(x, y) >= brightenedNegative.getRGB(x, y));
assertTrue(brightenedNegative.getRGB(x, y) >= brightenedNegativeMore.getRGB(x, y));
}
}
// Assumed: Image should be all black
final BufferedImage brightenedMaxNegative = ImageUtil.toBuffered(ImageUtil.brightness(original, -2f));
for (int y = 0; y < brightenedMaxNegative.getHeight(); y++) {
for (int x = 0; x < brightenedMaxNegative.getWidth(); x++) {
assertEquals(0x0, brightenedMaxNegative.getRGB(x, y) & 0x00FFFFFF);
}
}
/*
JFrame frame = new JFrame("Sunflower - brightness");
frame.setSize(sunflower.getWidth() * 4, sunflower.getHeight() * 2);
Canvas canvas = new Canvas() {
public void paint(Graphics g) {
// Draw original for comparison
g.drawImage(original, 0, 0, null);
// This should look like original
g.drawImage(notBrightened, 0, original.getHeight(), null);
// Different versions
g.drawImage(brightened, original.getWidth(), 0, null);
g.drawImage(brightenedMore, original.getWidth() * 2, 0, null);
g.drawImage(brightenedMax, original.getWidth() * 3, 0, null);
g.drawImage(brightenedNegative, original.getWidth(), original.getHeight(), null);
g.drawImage(brightenedNegativeMore, original.getWidth() * 2, original.getHeight(), null);
g.drawImage(brightenedMaxNegative, original.getWidth() * 3, original.getHeight(), null);
}
};
frame.getContentPane().add(canvas);
frame.setVisible(true);
assertTrue(true);
*/
}
public void testContrast() {
final BufferedImage original = mOriginal;
assertNotNull(original);
final BufferedImage notContrasted = ImageUtil.toBuffered(ImageUtil.contrast(original, 0f));
// Assumed: Images should be equal
if (original != notContrasted) { // Don't care to test if images are same
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertEquals("0 constrast should not change image", original.getRGB(x, y), notContrasted.getRGB(x, y));
}
}
}
// Assumed: Contrast should be greater or equal to original
final BufferedImage contrasted = ImageUtil.toBuffered(ImageUtil.contrast(original));
final BufferedImage contrastedDefault = ImageUtil.toBuffered(ImageUtil.contrast(original, 0.5f));
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
int oRGB = original.getRGB(x, y);
int cRGB = contrasted.getRGB(x, y);
int dRGB = contrastedDefault.getRGB(x, y);
int oR = oRGB >> 16 & 0xFF;
int oG = oRGB >> 8 & 0xFF;
int oB = oRGB & 0xFF;
int cR = cRGB >> 16 & 0xFF;
int cG = cRGB >> 8 & 0xFF;
int cB = cRGB & 0xFF;
int dR = dRGB >> 16 & 0xFF;
int dG = dRGB >> 8 & 0xFF;
int dB = dRGB & 0xFF;
// RED
if (oR < 127) {
assertTrue("Contrast should be decreased or same", oR >= cR && cR >= dR);
}
else {
assertTrue("Contrast should be increased or same", oR <= cR && cR <= dR);
}
// GREEN
if (oG < 127) {
assertTrue("Contrast should be decreased or same", oG >= cG && cG >= dG);
}
else {
assertTrue("Contrast should be increased or same", oG <= cG && cG <= dG);
}
// BLUE
if (oB < 127) {
assertTrue("Contrast should be decreased or same", oB >= cB && cB >= dB);
}
else {
assertTrue("Contrast should be increased or same", oB <= cB && cB <= dB);
}
}
}
// Assumed: Only primary colors (w/b/r/g/b/c/y/m)
final BufferedImage contrastedMax = ImageUtil.toBuffered(ImageUtil.contrast(original, 1f));
for (int y = 0; y < contrastedMax.getHeight(); y++) {
for (int x = 0; x < contrastedMax.getWidth(); x++) {
int rgb = contrastedMax.getRGB(x, y);
int r = rgb >> 16 & 0xFF;
int g = rgb >> 8 & 0xFF;
int b = rgb & 0xFF;
assertTrue("Max contrast should only produce primary colors", r == 0 || r == 255);
assertTrue("Max contrast should only produce primary colors", g == 0 || g == 255);
assertTrue("Max contrast should only produce primary colors", b == 0 || b == 255);
}
}
// Assumed: Contrasts should be less than or equal to original
final BufferedImage contrastedNegative = ImageUtil.toBuffered(ImageUtil.contrast(original, -0.5f));
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
int oRGB = original.getRGB(x, y);
int cRGB = contrastedNegative.getRGB(x, y);
int oR = oRGB >> 16 & 0xFF;
int oG = oRGB >> 8 & 0xFF;
int oB = oRGB & 0xFF;
int cR = cRGB >> 16 & 0xFF;
int cG = cRGB >> 8 & 0xFF;
int cB = cRGB & 0xFF;
// RED
if (oR >= 127) {
assertTrue("Contrast should be decreased or same", oR >= cR);
}
else {
assertTrue("Contrast should be increased or same", oR <= cR);
}
// GREEN
if (oG >= 127) {
assertTrue("Contrast should be decreased or same", oG >= cG);
}
else {
assertTrue("Contrast should be increased or same", oG <= cG);
}
// BLUE
if (oB >= 127) {
assertTrue("Contrast should be decreased or same", oB >= cB);
}
else {
assertTrue("Contrast should be increased or same", oB <= cB);
}
}
}
// Assumed: All gray (127)!
final BufferedImage contrastedMoreNegative = ImageUtil.toBuffered(ImageUtil.contrast(original, -1.0f));
for (int y = 0; y < contrastedMoreNegative.getHeight(); y++) {
for (int x = 0; x < contrastedMoreNegative.getWidth(); x++) {
int rgb = contrastedMoreNegative.getRGB(x, y);
int r = rgb >> 16 & 0xFF;
int g = rgb >> 8 & 0xFF;
int b = rgb & 0xFF;
assertTrue("Minimum contrast should be all gray", r == 127 && g == 127 &&b == 127);
}
}
/*
JFrame frame = new JFrame("Sunflower - contrast");
frame.setSize(sunflower.getWidth() * 4, sunflower.getHeight() * 2);
Canvas canvas = new Canvas() {
public void paint(Graphics g) {
// Draw original for comparison
g.drawImage(original, 0, 0, null);
// This should look like original
g.drawImage(notContrasted, 0, original.getHeight(), null);
// Different versions
g.drawImage(contrasted, original.getWidth(), 0, null);
g.drawImage(contrastedDefault, original.getWidth() * 2, 0, null);
g.drawImage(contrastedMax, original.getWidth() * 3, 0, null);
g.drawImage(contrastedNegative, original.getWidth() * 2, original.getHeight(), null);
g.drawImage(contrastedMoreNegative, original.getWidth() * 3, original.getHeight(), null);
}
};
frame.getContentPane().add(canvas);
frame.setVisible(true);
assertTrue(true);
*/
}
public void testSharpen() {
final BufferedImage original = mOriginal;
assertNotNull(original);
final BufferedImage notSharpened = ImageUtil.sharpen(original, 0f);
// Assumed: Images should be equal
if (original != notSharpened) { // Don't care to test if images are same
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertEquals("0 sharpen should not change image", original.getRGB(x, y), notSharpened.getRGB(x, y));
}
}
}
// Assumed: Difference between neighbouring pixels should increase for higher sharpen values
// Assumed: Dynamics of entire image should not change
final BufferedImage sharpened = ImageUtil.sharpen(original);
final BufferedImage sharpenedDefault = ImageUtil.sharpen(original, 0.3f);
final BufferedImage sharpenedMore = ImageUtil.sharpen(original, 1.3f);
long diffOriginal = 0;
long diffSharpened = 0;
long diffDefault = 0;
long diffMore = 0;
long absDiffOriginal = 0;
long absDiffSharpened = 0;
long absDiffDefault = 0;
long absDiffMore = 0;
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 1; x < original.getWidth(); x++) {
int oRGB = 0x00FFFFFF & original.getRGB(x, y);
int sRGB = 0x00FFFFFF & sharpened.getRGB(x, y);
int dRGB = 0x00FFFFFF & sharpenedDefault.getRGB(x, y);
int mRGB = 0x00FFFFFF & sharpenedMore.getRGB(x, y);
int poRGB = 0x00FFFFFF & original.getRGB(x - 1, y);
int psRGB = 0x00FFFFFF & sharpened.getRGB(x - 1, y);
int pdRGB = 0x00FFFFFF & sharpenedDefault.getRGB(x - 1, y);
int pmRGB = 0x00FFFFFF & sharpenedMore.getRGB(x - 1, y);
diffOriginal += poRGB - oRGB;
diffSharpened += psRGB - sRGB;
diffDefault += pdRGB - dRGB;
diffMore += pmRGB - mRGB;
absDiffOriginal += Math.abs(poRGB - oRGB);
absDiffSharpened += Math.abs(psRGB - sRGB);
absDiffDefault += Math.abs(pdRGB - dRGB);
absDiffMore += Math.abs(pmRGB - mRGB);
}
}
//*
showEm(original, notSharpened, sharpened, sharpenedDefault, sharpenedMore, "sharpen");
//*/
// assertEquals("Difference should not change", diffOriginal, diffSharpened);
assertTrue("Abs difference should increase", absDiffOriginal < absDiffSharpened);
// assertEquals("Difference should not change", diffOriginal, diffDefault);
assertTrue("Abs difference should increase", absDiffOriginal < absDiffDefault);
// assertEquals("Difference should not change", diffOriginal, diffMore);
assertTrue("Abs difference should increase", absDiffOriginal < absDiffMore);
// assertEquals("Difference should not change", diffSharpened, diffMore);
assertTrue("Abs difference should increase", absDiffSharpened < absDiffMore);
}
private void showEm(final BufferedImage pOriginal, final BufferedImage pNotSharpened, final BufferedImage pSharpened, final BufferedImage pSharpenedDefault, final BufferedImage pSharpenedMore, final String pTitle) {
if (pOriginal != null) {
return;
}
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
JFrame frame = new JFrame("Sunflower - " + pTitle);
frame.setSize(pOriginal.getWidth() * 4, pOriginal.getHeight() * 2);
Canvas canvas = new Canvas() {
public void paint(Graphics g) {
// Draw original for comparison
g.drawImage(pOriginal, 0, 0, null);
// This should look like original
g.drawImage(pNotSharpened, 0, pOriginal.getHeight(), null);
// Different versions
g.drawImage(pSharpened, pOriginal.getWidth(), 0, null);
g.drawImage(pSharpenedDefault, pOriginal.getWidth() * 2, 0, null);
g.drawImage(pSharpenedMore, pOriginal.getWidth() * 3, 0, null);
}
};
frame.getContentPane().add(canvas);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
synchronized (ImageUtilTestCase.this) {
ImageUtilTestCase.this.notify();
}
}
});
frame.setVisible(true);
}
});
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
synchronized (ImageUtilTestCase.this) {
try {
ImageUtilTestCase.this.wait();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void testBlur() {
final BufferedImage original = mOriginal;
assertNotNull(original);
final BufferedImage notBlurred = ImageUtil.blur(original, 0f);
// Assumed: Images should be equal
if (original != notBlurred) { // Don't care to test if images are same
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 0; x < original.getWidth(); x++) {
assertEquals("0 blur should not change image", original.getRGB(x, y), notBlurred.getRGB(x, y));
}
}
}
// Assumed: Difference between neighbouring pixels should decrease for higher blur values
// Assumed: Dynamics of entire image should not change
final BufferedImage blurred = ImageUtil.blur(original);
final BufferedImage blurredDefault = ImageUtil.blur(original, 1.5f);
final BufferedImage blurredMore = ImageUtil.blur(original, 3f);
long diffOriginal = 0;
long diffBlurred = 0;
long diffDefault = 0;
long diffMore = 0;
long absDiffOriginal = 0;
long absDiffBlurred = 0;
long absDiffDefault = 0;
long absDiffMore = 0;
for (int y = 0; y < original.getHeight(); y++) {
for (int x = 1; x < original.getWidth(); x++) {
int oRGB = 0x00FFFFFF & original.getRGB(x, y);
int bRGB = 0x00FFFFFF & blurred.getRGB(x, y);
int dRGB = 0x00FFFFFF & blurredDefault.getRGB(x, y);
int mRGB = 0x00FFFFFF & blurredMore.getRGB(x, y);
int poRGB = 0x00FFFFFF & original.getRGB(x - 1, y);
int pbRGB = 0x00FFFFFF & blurred.getRGB(x - 1, y);
int pdRGB = 0x00FFFFFF & blurredDefault.getRGB(x - 1, y);
int pmRGB = 0x00FFFFFF & blurredMore.getRGB(x - 1, y);
diffOriginal += poRGB - oRGB;
diffBlurred += pbRGB - bRGB;
diffDefault += pdRGB - dRGB;
diffMore += pmRGB - mRGB;
absDiffOriginal += Math.abs(poRGB - oRGB);
absDiffBlurred += Math.abs(pbRGB - bRGB);
absDiffDefault += Math.abs(pdRGB - dRGB);
absDiffMore += Math.abs(pmRGB - mRGB);
}
}
showEm(original, notBlurred, blurred, blurredDefault, blurredMore, "blur");
// assertEquals("Difference should not change", diffOriginal, diffBlurred);
assertTrue(String.format("Abs difference should decrease: %s <= %s", absDiffOriginal, absDiffBlurred), absDiffOriginal > absDiffBlurred);
// assertEquals("Difference should not change", diffOriginal, diffDefault);
assertTrue("Abs difference should decrease", absDiffOriginal > absDiffDefault);
// assertEquals("Difference should not change", diffOriginal, diffMore);
assertTrue("Abs difference should decrease", absDiffOriginal > absDiffMore);
// assertEquals("Difference should not change", diffBlurred, diffMore);
assertTrue("Abs difference should decrease", absDiffBlurred > absDiffMore);
}
public void testIndexImage() {
BufferedImage sunflower = mOriginal;
assertNotNull(sunflower);
BufferedImage image = ImageUtil.createIndexed(sunflower);
assertNotNull("Image was null", image);
assertTrue(image.getColorModel() instanceof IndexColorModel);
}
}

View File

@@ -0,0 +1,263 @@
package com.twelvemonkeys.image;
import junit.framework.TestCase;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ImagingOpException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* ResampleOpTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java#1 $
*/
public class ResampleOpTestCase extends TestCase {
protected BufferedImage createImage(final int pWidth, final int pHeigth) {
return createImage(pWidth, pHeigth, BufferedImage.TYPE_INT_ARGB);
}
protected BufferedImage createImage(final int pWidth, final int pHeigth, final int pType) {
BufferedImage image = new BufferedImage(pWidth, pHeigth, pType);
Graphics2D g = image.createGraphics();
try {
g.setPaint(new GradientPaint(0, 0, Color.RED, pWidth, pHeigth, new Color(0x00000000, true)));
g.fillRect(0, 0, pWidth, pHeigth);
}
finally {
g.dispose();
}
return image;
}
public void testCreateImage() {
// Sanity test the create method
BufferedImage image = createImage(79, 84);
assertNotNull(image);
assertEquals(79, image.getWidth());
assertEquals(84, image.getHeight());
}
private void assertResample(final BufferedImage pImage, final int pWidth, final int pHeight, final int pFilterType) {
BufferedImage result = new ResampleOp(pWidth, pHeight, pFilterType).filter(pImage, null);
assertNotNull(result);
assertEquals(pWidth, result.getWidth());
assertEquals(pHeight, result.getHeight());
result = new ResampleOp(pImage.getWidth(), pImage.getHeight(), pFilterType).filter(createImage(pWidth, pHeight), pImage);
assertNotNull(result);
assertEquals(pImage.getType(), result.getType());
assertSame(pImage, result);
assertEquals(pImage.getWidth(), result.getWidth());
assertEquals(pImage.getHeight(), result.getHeight());
result = new ResampleOp(pImage.getWidth(), pImage.getHeight(), pFilterType).filter(createImage(pWidth, pHeight), createImage(pWidth, pHeight, pImage.getType()));
assertNotNull(result);
assertEquals(pImage.getType(), result.getType());
assertEquals(pWidth, result.getWidth());
assertEquals(pHeight, result.getHeight());
}
private void assertResampleBufferedImageTypes(final int pFilterType) {
List<String> exceptions = new ArrayList<String>();
// Test all image types in BufferedImage
for (int type = BufferedImage.TYPE_INT_ARGB; type <= BufferedImage.TYPE_BYTE_INDEXED; type++) {
// TODO: Does not currently work with TYPE_BYTE_GRAY or TYPE_USHORT_GRAY
// TODO: FixMe!
if ((pFilterType == ResampleOp.FILTER_POINT || pFilterType == ResampleOp.FILTER_TRIANGLE) &&
(type == BufferedImage.TYPE_BYTE_GRAY || type == BufferedImage.TYPE_USHORT_GRAY)) {
continue;
}
BufferedImage image = createImage(10, 10, type);
try {
assertResample(image, 15, 5, pFilterType);
}
catch (ImagingOpException e) {
// NOTE: It is currently allowed for filters to throw this exception and it is PLATFORM DEPENDENT..
System.err.println("WARNING: " + e.getMessage() + ", image: " + image);
//e.printStackTrace();
}
catch (Throwable t) {
exceptions.add(t.toString() + ": " + image.toString());
}
}
assertEquals("Filter threw exceptions: ", Collections.EMPTY_LIST, exceptions);
}
// 1x1
public void testResample1x1Point() {
assertResample(createImage(1, 1), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample1x1Box() {
assertResample(createImage(1, 1), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample1x1Triangle() {
assertResample(createImage(1, 1), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample1x1Lanczos() {
assertResample(createImage(1, 1), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample1x1Gaussian() {
assertResample(createImage(1, 1), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample1x1Sinc() {
assertResample(createImage(1, 1), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// 2x2
public void testResample2x2Point() {
assertResample(createImage(2, 2), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample2x2Box() {
assertResample(createImage(2, 2), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample2x2Triangle() {
assertResample(createImage(2, 2), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample2x2Lanczos() {
assertResample(createImage(2, 2), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample2x2Gaussian() {
assertResample(createImage(2, 2), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample2x2Sinc() {
assertResample(createImage(2, 2), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// 3x3
public void testResample3x3Point() {
assertResample(createImage(3, 3), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample3x3Box() {
assertResample(createImage(3, 3), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample3x3Triangle() {
assertResample(createImage(3, 3), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample3x3Lanczos() {
assertResample(createImage(3, 3), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample3x3Gaussian() {
assertResample(createImage(3, 3), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample3x3Sinc() {
assertResample(createImage(3, 3), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// 4x4
public void testResample4x4Point() {
assertResample(createImage(4, 4), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample4x4Box() {
assertResample(createImage(4, 4), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample4x4Triangle() {
assertResample(createImage(4, 4), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample4x4Lanczos() {
assertResample(createImage(4, 4), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample4x4Gaussian() {
assertResample(createImage(4, 4), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample4x4Sinc() {
assertResample(createImage(4, 4), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// 20x20
public void testResample20x20Point() {
assertResample(createImage(20, 20), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample20x20Box() {
assertResample(createImage(20, 20), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample20x20Triangle() {
assertResample(createImage(20, 20), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample20x20Lanczos() {
assertResample(createImage(20, 20), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample20x20Gaussian() {
assertResample(createImage(20, 20), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample20x20Sinc() {
assertResample(createImage(20, 20), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// 200x160
public void testResample200x160Point() {
assertResample(createImage(200, 160), 10, 11, ResampleOp.FILTER_POINT);
}
public void testResample200x160Box() {
assertResample(createImage(200, 160), 10, 11, ResampleOp.FILTER_BOX);
}
public void testResample200x160Triangle() {
assertResample(createImage(200, 160), 19, 13, ResampleOp.FILTER_TRIANGLE);
}
public void testResample200x160Lanczos() {
assertResample(createImage(200, 160), 7, 49, ResampleOp.FILTER_LANCZOS);
}
public void testResample200x160Gaussian() {
assertResample(createImage(200, 160), 11, 34, ResampleOp.FILTER_GAUSSIAN);
}
public void testResample200x160Sinc() {
assertResample(createImage(200, 160), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC);
}
// Test 10x10 -> 15x5 with different algorithms and types
public void testResamplePoint() {
assertResampleBufferedImageTypes(ResampleOp.FILTER_POINT);
}
public void testResampleBox() {
assertResampleBufferedImageTypes(ResampleOp.FILTER_BOX);
}
public void testResampleTriangle() {
assertResampleBufferedImageTypes(ResampleOp.FILTER_TRIANGLE);
}
public void testResampleLanczos() {
assertResampleBufferedImageTypes(ResampleOp.FILTER_LANCZOS);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB