/*
* 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 of the copyright holder 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 HOLDER 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
pixelSizeX;
final private int
pixelSizeY;
private
Rectangle sourceRegion;
public
PixelizeOp(final int
pPixelSize) {
this(
pPixelSize,
pPixelSize);
}
public
PixelizeOp(final int
pPixelSizeX, final int
pPixelSizeY) {
pixelSizeX =
pPixelSizeX;
pixelSizeY =
pPixelSizeY;
}
public
Rectangle getSourceRegion() {
if (
sourceRegion == null) {
return null;
}
return new
Rectangle(
sourceRegion);
}
public void
setSourceRegion(final
Rectangle pSourceRegion) {
if (
pSourceRegion == null) {
sourceRegion = null;
}
else {
if (
sourceRegion == null) {
sourceRegion = new
Rectangle(
pSourceRegion);
}
else {
sourceRegion.
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 (
sourceRegion != null) {
int
cx =
sourceRegion.
x;
int
cy =
sourceRegion.
y;
int
cw =
sourceRegion.
width;
int
ch =
sourceRegion.
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 +
pixelSizeX - 1) /
pixelSizeX;
int
h = (
height +
pixelSizeY - 1) /
pixelSizeY;
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 =
pixelSizeY;
}
else {
scanH =
height - (
y *
pixelSizeY);
}
for (int
x = 0;
x <
w;
x++) {
if (!
oddX ||
x + 1 <
w) {
scanW =
pixelSizeX;
}
else {
scanW =
width - (
x *
pixelSizeX);
}
final int
pixelCount =
scanW *
scanH;
final int
pixelLength =
pixelCount *
dataElements;
data =
src.
getDataElements(
x *
pixelSizeX,
y *
pixelSizeY,
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 *
pixelSizeX,
y *
pixelSizeY,
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);
}
}