com.day.image.EmbossOp Maven / Gradle / Ivy
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2012 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.image;
import java.awt.image.BandCombineOp;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
/**
* The EmbossOp
implements embossing of an image against another
* image, the so called bump image. The embossing algorithm works like this :
*
* - Define the position of the light source using two angles, the azimut and
* the elevation. The azimut defines the direction in the image plane from
* where the light shines and the elevation defines the height above the
* image plane of the light source.
*
- Define a filtersize defining the spread of the embossing effect. We could
* also say, this is sort of the size of the light.
*
- Define the bump image. This image is scaled to gray and then used to
* define the embossing piece.
*
- For each pixel position in the images :
*
* - Define a plane
E: ax + bx + cx + d = 0
where a is
* the luminance difference between pixels above and below the
* current pixel, b is the luminance difference between pixels left
* and right of the current pixel and c is constant relative to
* the filtersize.
* - Calculate the scale factor to apply to the pixel in the image as
* follows :
*
* - If factors a and b both are zero, the plane E is no plane
* at all and the scale factor is the height of the light
* source.
*
- Else it is the distance of the light source to the above
* defined plane.
*
* - Multiply each pixel with the scale factor, ignoring the alpha
* channels.
*
*
*
* The emobossing area is the smaller of the bump and the image to emboss. You
* will get the best results from using an image as the bump image and some
* monochromatic image as the image to emboss.
*
* @version $Revision$
* @author fmeschbe
* @since coati
* @audience wad
*/
public class EmbossOp extends AbstractBufferedImageOp {
/** The bump image in terms of grayscale sample values for the pixels */
private final int[] bump;
/** The width of the bump image */
private final int width;
/** The height of the bump image */
private final int height;
/** The direction of the light in the image plane in degrees */
private final float azimut;
/** The elevation of the light source above the image plane in degrees */
private final float elevation;
/** Size of the light filter defining the size of the light source */
private final float filtersize;
/**
* Creates a new EmbossOp
filter instance with the given
* parameters for embossing. The bump image is copied into the instance
* so that subsequent changes to the bump image do not have an effect on
* the embossing operation.
*
* @param bump the image being used for the 'bump'
* @param azimut the light direction azimut in degrees.
* @param elevation the light direction elevation in degrees.
* @param filtersize the filtersize for the embossing.
*
* @throws IllegalArgumentException if any of the azimut, elevation or
* filtersize arguments is negative or Float.NaN
.
* @throws NullPointerException if bump is null.
*/
public EmbossOp(BufferedImage bump, float azimut, float elevation,
float filtersize) {
// initialize the base class
super(null);
// will throw IllegalArgumentException if NaN or negative
checkValue("azimut", azimut);
checkValue("elevation", elevation);
checkValue("filtersize", filtersize);
// copy and scale to gray the bump image, throw if null
this.bump = GreyScalerHelper.filter(bump.getRaster());
this.width = bump.getWidth();
this.height = bump.getHeight();
// set the light source configuration
this.azimut = azimut;
this.elevation = elevation;
this.filtersize = filtersize;
}
//---------- BufferedImageOp -----------------------------------------------
// no methods of the base class to overwrite
//---------- internal ------------------------------------------------------
/**
* Checks the float value whether it is NaN or negative. If so, the
* IllegalArgumentException
is thrown with the name as a hint
* on what argument is illegal.
*
* @param parName The name of the parameter to name in the exception if
* thrown
* @param parValue The value of the parameter to check
*
* @throws IllegalArgumentException if parValue is NaN or negative
*/
private void checkValue(String parName, float parValue) {
if (Float.isNaN(parValue) || parValue < 0f) {
throw new IllegalArgumentException("Argument " + parName +
" must not be negative or NaN");
}
}
//---------- protected -----------------------------------------------------
/**
* Does the real embossing of the source image into the destination image.
*
* @param src The source image containing the image data to be embossed.
* @param dst The destination image to emboss the source image into.
*/
protected void doFilter(BufferedImage src, BufferedImage dst) {
int width = src.getWidth();
int height = src.getHeight();
// Get the raster we operate on
Raster srcRas = src.getRaster();
WritableRaster dstRas = dst.getRaster();
double pixelScale = 255.9;
double azi = Math.toRadians(azimut);
double ele = Math.toRadians(elevation);
// coordinates of the light source
int lx = (int) (Math.cos(azi) * Math.cos(ele) * pixelScale);
int ly = (int) (Math.sin(azi) * Math.cos(ele) * pixelScale);
int lz = (int) (Math.sin(ele) * pixelScale);
// constant z component of image surface normal - this depends on the
// image slope we wish to associate with an angle of 45 degrees, which
// depends on the width of the filter used to produce the source image.
int nz = (int)((6 * 255) / filtersize);
int nz2 = nz * nz;
int nzlz = nz * lz;
// The bump image dimensions
int bw = this.width;
int bh = this.height;
// The texture dimensions
int tw = width;
int th = height;
// The operational dimensions are the smaller of src and bump
int nw = (tw < bw) ? tw : bw;
int nh = (th < bh) ? th : bh;
// data buffer for one scan line
int[] srcRed = new int[ nw ];
int[] srcGrn = new int[ nw ];
int[] srcBlu = new int[ nw ];
// optimization for vertical normals: L.[0 0 1]
int background = lz;
for (int y=0; y < nh; y++) {
// get the next chunk if needed
srcRas.getSamples(0, y, nw, 1, 0, srcRed);
srcRas.getSamples(0, y, nw, 1, 1, srcGrn);
srcRas.getSamples(0, y, nw, 1, 2, srcBlu);
// chunkY = 0;
// chunkTop = y;
int s1 = bw * y + 1;
int s2 = s1 + ((y < nh-1) ? bw : 0);
int s3 = s2 + ((y < nh-2) ? bw : 0);
for (int x=1, toff=1; x < nw-1; x++, s1++, s2++, s3++) {
int nx = bump[s1-1] + bump[s2-1] + bump[s3-1]
- bump[s1+1] - bump[s2+1] - bump[s3+1];
int ny = bump[s3-1] + bump[s3] + bump[s3+1]
- bump[s1-1] - bump[s1] - bump[s1+1];
int ndotl = 0;
int shade = 0;
// shade with distant light source
if ( nx == 0 && ny == 0 ) {
shade = background;
} else {
ndotl = nx*lx + ny*ly + nzlz;
if (ndotl < 0 ) {
shade = 0;
} else {
shade = (int) (ndotl / Math.sqrt(nx*nx + ny*ny + nz2));
}
}
// do something with the shading result
srcRed[toff] = (srcRed[toff] * shade) >>> 8;
srcBlu[toff] = (srcBlu[toff] * shade) >>> 8;
srcGrn[toff] = (srcGrn[toff] * shade) >>> 8;
toff++; // Next pixel
}
dstRas.setSamples(0, y, nw, 1, 0, srcRed);
dstRas.setSamples(0, y, nw, 1, 1, srcBlu);
dstRas.setSamples(0, y, nw, 1, 2, srcGrn);
// copy alpha from src using srcRed array as buffer
dstRas.setSamples(0, y, nw, 1, 3,
srcRas.getSamples(0, y, nw, 1, 3, srcRed));
}
}
/**
* The GreyScaleHelper
class is a simple helper class to
* implement a simple gray scaling algorithm for the bump image.
*/
private static final class GreyScalerHelper {
/**
* The matrix for the grayscale operation has the luminance factors
* for RGB in a linear color space.
*
* @see Matrix
* Operations for Image Processing, Converting to
* Luminance
*/
private static final float[][] bopEl = {
{ 0.0f, 0.0f, 0.0f, 0.0f, 0x0},
{ 0.0f, 0.0f, 0.0f, 0.0f, 0x0},
{ 0.0f, 0.0f, 0.0f, 0.0f, 0x0},
{ -.3086f, -.6094f, -.0820f, 0.0f, 255}
};
/** The grayscaling operation based on above matrix */
private static final BandCombineOp op = new BandCombineOp(bopEl, null);
/**
* Performs the grayscaling operation.
* @param input The input Raster to scale to gray. This raster is not
* changed.
* @return The Alpha channel of the gray scaled image containing the
* grey levels.
* @throws NullPointerException if input is null
.
*/
private static final int[] filter(Raster input) {
Raster tmp = op.filter(input, null);
return tmp.getSamples(0, 0, tmp.getWidth(),
tmp.getHeight(), 3, (int[])null);
}
}
}