Convolution implementation does not work as expected - java

I spent the whole day trying to implement the "convolution algorithm" in Java, but this last does not seem to work properly with all kernels, it works great with the blur kernel with a factor of 1/9, but not with the other ones.
For example, if I use the {{0.1.0},{0,0,0},{0,0,0}} matrix which is supposed to shift the image up by 1 pixel, surprisingly, it stretches the image all the way down.
Example of what I get:
And here is the code:
public class ConvolutionTest {
static BufferedImage bfimg;
static BufferedImage outputimg;
static File output = new File("/home/bz/Pictures/Selection_003_mod");
static File input = new File("/home/bz/Pictures/Selection_003.png");
static WritableRaster wr;
static int tempColor;
static double [] newColor = {0,0,0};
static double red=0, green=0, blue=0;
static double sumR=0, sumG=0, sumB=0;
public static void main(String[] args) throws IOException {
int tempIcoor;
int tempJcoor;
double[][] matConv = {
{0d, 1d, 0d},
{0d, 0d, 0d},
{0d, 0d, 0d}
};
bfimg = ImageIO.read(input);
outputimg = bfimg;
wr = outputimg.getRaster();
for (int i = 1; i < bfimg.getHeight()-1; i++) {
for (int j = 1; j < bfimg.getWidth()-1; j++) {
tempIcoor = i - 1;
tempJcoor = j - 1;
for (int tempI = 0; tempI < 3; tempI++) {
for (int tempJ = 0; tempJ < 3; tempJ++) {
tempColor = bfimg.getRGB(tempJcoor, tempIcoor);
red = tempColor >> 16 & 0xff;
red = red * matConv[tempI][tempJ];
green = tempColor >> 8 & 0xff;
green = green * matConv[tempI][tempJ];
blue = tempColor & 0xff;
blue = blue * matConv[tempI][tempJ];;
sumR = red + sumR;
sumG = green + sumG;
sumB = blue + sumB;
tempJcoor++;
}
newColor[0] = sumR;
newColor[1] = sumG;
newColor[2] = sumB;
tempIcoor++;
tempJcoor=j-1;
}
wr.setPixel(j, i, newColor);
sumR=0;
sumG=0;
sumB=0;
}
}
ImageIO.write(sortie, "png", output);
}
}

With
outputimg = bfimg;
you are setting the output image to be the same as the input image. When you perform the convolution of the first row, then (as you said) the first row of pixels from the input image will be written into the the second row of the output image. But they are identical - so you end up with all rows of the output image being copies of the first row of the input image.
Just replace this line with
outputimg = new BufferedImage(
bfimg.getWidth(), bfimg.getHeight(),
BufferedImage.TYPE_INT_ARGB);
to create a new output image to write to.
By the way: All this is already available in the standard API. You might want to have a look at the classes related to http://docs.oracle.com/javase/7/docs/api/java/awt/image/ConvolveOp.html

Related

ImageWriter creates smaller picture ( in kb size ) [duplicate]

This question already has answers here:
Hiding message in JPG image
(2 answers)
Closed 6 years ago.
I have the following problem, I want to create simple steganography "program" by coding message in LSB.
I extract ARGB from picture ( each in it's own array ), encode message in LSB of blue color, and try to create new image using a those new values ( I join ARGB arrays back in int array ).
The obvious problem I have is when I change LSB and try to write them to picture , I can see that ImageWriter is creating picture that is much smaller in kb and I can't extract my message anymore.
This is the code :
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class Steganography {
int [][] alpha;
int [][] red;
int [][] green;
int [][] blue;
public int [][] readPixels (String image) throws IOException {
//load image into img buffer
BufferedImage img = ImageIO.read(new File(image));
//make matrix according to picture height and width
int [][] pixels = new int[img.getHeight()][img.getWidth()];
// load matrix with image pixels
for(int i=0;i<pixels.length;i++) {
for (int j = 0; j < pixels[0].length; j++) {
pixels[i][j]=(img.getRGB(j, i));
}
}
/* reminder to myself
values will be negative because of packing the 4 byte values into a 4-byte
The getRGB method returns an int whose 4 bytes are the alpha, red, green, and blue components in that order.
Assuming that the pixel is not transparent, the alpha is 255 (0xFF).
It's the most significant byte in the int, and the first bit is set in that value.
Because in Java int values are signed according to Two's Complement,
the value is actually negative because that first bit is on.
*/
return pixels ;
}
// extracts colors and alpha into their own matrix so we can reconstruct image later
public void extractColors(int [][] pixel){
this.alpha = new int[pixel.length][pixel[0].length];
this.red = new int[pixel.length][pixel[0].length];
this.green = new int[pixel.length][pixel[0].length];
this.blue = new int[pixel.length][pixel[0].length];
for(int i=0;i<pixel.length;i++) {
for(int j=0;j<pixel[i].length;j++){
int clr = pixel[i][j];
alpha[i][j] = (clr & 0xff000000) >> 24;
red[i][j] = (clr & 0x00ff0000) >> 16;
green[i][j] = (clr & 0x0000ff00) >> 8;
blue [i][j] = clr & 0x000000ff;
}
}
} // closed method
//reconstruct image
// need to make 32 bit integer again in correct order
public void reconstructImage () throws IOException{
int height = alpha.length;
int width= alpha[0].length;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < width; y++) {
for (int x = 0; x < height; x++) {
int rgb= red[x][y];
rgb = (rgb << 8) + green[x][y];
rgb = (rgb << 8) + blue[x][y];
image.setRGB(y, x, rgb);
}
}
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // Needed see javadoc
param.setCompressionQuality(1.0F); // Highest quality
File file = new File("output.jpg");
ImageOutputStream ios = ImageIO.createImageOutputStream(file);
writer.setOutput(ios);
writer.write(image);
}
public void codeMessage (String message){
//first turn string into binary representation
// each character should have 7 bits
// ASCII uses 7 bit
message="START"+message.length()+message+"STOP";
String binaryMessage ="";
for(int i =0;i<message.length();i++){
//adding zeros if string has less than 8 characters
String binaryString= Integer.toBinaryString(message.charAt(i));
while (binaryString.length() !=7)
binaryString = "0"+binaryString;
binaryMessage+=binaryString;
}
//binaryMessage is binary representation of string
// change value of LSB in blue color according to binaryMessage
//actually coding message into LSB is done here
int k=0;
for (int i = 0; i < blue.length; i++) {
for (int j = 0; j < blue[i].length; j++) {
if(k>=binaryMessage.length())
break;
else if (binaryMessage.charAt(k) == '0') {
blue[i][j] = blue[i][j] & 0b1111110;
k++;
}
else {
blue[i][j] = blue[i][j] | 0b0000001;
k++;
}
}
}
} //closed codeMessage
public void readMessage(){
String LSB ="";
char charLSB;
String messageBinary ="";
for(int i=0;i<blue.length;i++){
for(int j=0;j<blue[i].length;j++){
LSB = Integer.toBinaryString(blue[i][j]);
charLSB = LSB.charAt(LSB.length()-1);
messageBinary+=charLSB;
}
}
char ArrayOfChars [] = new char [blue[0].length*blue.length];
int k =0;
for(int i=0;i<messageBinary.length()-7;i+=7){
String letter=(messageBinary.substring(i,i+7));
int valueOfASCIIcharacter = Integer.parseInt(letter,2);
char c = (char)(valueOfASCIIcharacter);
System.out.println(c);
ArrayOfChars[k]=c;
k++;
}
}
}
I have also tried to use ARGB instead of RGB for BufferedImage, without luck (only messes up colors, picture gets kinda pink ).
This is how I call function in main class
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException{
Steganography img = new Steganography();
int pixels [][] =img.readPixels("image.jpg");
img.extractColors(pixels);
img.codeMessage("Some message");
img.reconstructImage();
/*reading message from here on */
int pixels2 [][] = img.readPixels("output.jpg");
img.extractColors(pixels2);
img.readMessage();
}
}
Original picture has 83,3 kb ,and recreated picture has only 24,3 kb.
I have found solution.
For anyone having same problem as me and possible searching for solution in future:
This algorithm can't survive .jpg extension. Changed picture to bmp, takes bit longer but everything works as expected.
If you want to use steganography on jpg you have to use something else than LSB.

Convolution Blur Filter doesn't work and brightens the image

I've read about Convolution Filters recently and decided to try it out. I wanted to make code that blurs the image but what it ends up doing is brightening it. I've been looking at my code for some time now and can't find any mistakes. Could anyone help?
Here is my code:
final static int filterHeight =3;
final static int filterWidth = 3;
static double filter[][] = new double[][]{
{1,1,1},
{1,1,1},
{1,1,1}
};
public static void main(String[] args) {
BufferedImage img;
BufferedImage result;
try
{ File in = new File("in.jpg");
File out = new File("out.jpg");
img = ImageIO.read(in);
Color[][] pixels = new Color[img.getWidth()][img.getHeight()];
for(int i=0;i<img.getWidth();i++){
for(int j=0;j<img.getHeight();j++){
pixels[i][j]=new Color(img.getRGB(i,j),true);
}
}
result = new BufferedImage(img.getWidth(), img.getHeight(), img.getType());
for(int x=0;x<img.getWidth();x++){
for(int y=0;y<img.getHeight();y++){
int r=0,g=0,b=0;
for(int i=0;i<filterWidth;i++){
for(int j=0;j<filterHeight;j++){
int imageX = (int)(x - filterWidth / 2 + i + img.getWidth()) % img.getWidth();
int imageY = (int)(y - filterHeight / 2 + j + img.getHeight()) % img.getHeight();
if(imageX<0 || imageY<0) System.out.println("ERROR: "+imageX+" "+imageY);
r+=pixels[imageX][imageY].getRed()*filter[i][j];
g+=pixels[imageX][imageY].getGreen()*filter[i][j];
b+=pixels[imageX][imageY].getBlue()*filter[i][j];
}
if(r>255) r=255;
if(r<0) r=0;
if(g>255) g=255;
if(g<0) g=0;
if(b>255) b=255;
if(b<0) b=0;
Color color = new Color(img.getRGB(x,y),true)
Color colorBlur = new Color(r,g,b,color.getAlpha());
result.setRGB(x, y, colorBlur.getRGB());
}
}
}
ImageIO.write(result, "JPG", out );
}
catch (IOException e)
{
e.printStackTrace();
}
And here is image before aplying filter:
And after:
There are two options in order to get the result you desire.
Either you create a filter matrix with a sum of 1 (Like #Spektre mentioned in his comment above) or multiply the pixel's new value with a factor of 1 / sum(filterMatrix).
For a nice beginner tutorial of the concept of blurring I would recommend:
Concept of Blurring - www.TutorialsPoint.com

Why BufferedImage is not working well?!! Is it because I misused it?

I want to copy a gray image using BufferedImage from getRGB() to int[][] and then to setRGB(). The problem is that the size of image is different from the size of the one that the program outputs it. The original image has file size = 176 KB, whereas the output image has file size = 154 KB. I have to say that when you see the two image, all of the human-being would say it is the same, but in terms of binary bits, there are different in something that I would like to know.
Maybe some of you will say it doesn't matter, as long as image is the same when you look at it. In fact, during the processing of some noise project, this is a huge problem, and I suspect that this is the reason why I have the problem.
I just want to know if there are other method than BufferedImage to produce int[][] and then to create the output?
This is the code that I'm using:
public int[][] Read_Image(BufferedImage image)
{
width = image.getWidth();
height = image.getHeight();
int[][] result = new int[height][width];
for (int row = 0; row < height; row++)
for (int col = 0; col < width; col++)
result[row][col] = image.getRGB(row, col);
return result;
}
public BufferedImage Create_Gray_Image(int [][] pixels)
{
BufferedImage Ima = new BufferedImage(512,512, BufferedImage.TYPE_BYTE_GRAY);
for (int x = 0; x < 512; x++)
{
for (int y = 0; y < 512; y++)
{
int rgb = pixels[x][y];
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = (rgb & 0xFF);
int grayLevel = (r + g + b) / 3;
int gray = (grayLevel << 16) + (grayLevel << 8) + grayLevel;
Ima.setRGB(x, y, pixels[x][y]);
}
}
return Ima;
}
public void Write_Image(int [][] pixels) throws IOException
{
File outputfile;
outputfile = new File("Y0111.png");
BufferedImage BI = this.Create_Gray_Image(pixels);
ImageIO.write(BI, "png", outputfile);
System.out.println("We finished writing the file");
}
See the figure, you see file size = 176 KB (this is the original image) and file size = 154 KB (this is the output image).
The difference of size is not a problem. It's certainly because of different compression/encoding.
A BufferedImage is in fact a 1D array of size width * height * channel. getRGB is not the easiest/fastest way to manipulate a BufferedImage. You can use the Raster (faster than getRGB, not the fastest, but it takes care of the encoding for you). For a gray level image:
int[][] my array = new int[myimage.getHeight()][myimage.getWidth()] ;
for (int y=0 ; y < myimage.getHeight() ; y++)
for (int x=0 ; x < myimage.getWidth() ; x++)
myarray[y][x] = myimage.getRaster().getSample(x, y, 0) ;
The opposite way:
for (int y=0 ; y < myimage.getHeight() ; y++)
for (int x=0 ; x < myimage.getWidth() ; x++)
myimage.getRaster().setSample(x, y, 0, myarray[y][x]) ;
The fastest way to do it is to use the DataBuffer, but then you have to handle the image encoding.

Why raster.setPixels() is returning a grayscale image

Here i'm trying to do a fastest method to save 3 matrix(R, G and B) into a BufferedImage.
I've found this method here at StackExchange, but it doesn't work for me because the image it's being saved in a grayscale color.
If I'm doing something wrong or if there's a way of doing this faster than bufferimage.setRGB(), please help me. Thanks!
public static BufferedImage array_rasterToBuffer(int[][] imgR,
int[][]imgG, int[][] imgB) {
final int width = imgR[0].length;
final int height = imgR.length;
int numBandas = 3;
int[] pixels = new int[width*height*numBandas];
int cont=0;
System.out.println("max: "+width*height*3);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
for (int band = 0; band < numBandas; band++) {
pixels[(((i*width)+j)*numBandas +band)] =Math.abs(( (imgR[i][j] & 0xff) >> 16 | (imgG[i][j] & 0xff) >> 8 | (imgB[i][j] & 0xff)));
cont+=1;
}
}
}
BufferedImage bufferImg = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);
WritableRaster rast = (WritableRaster) bufferImg.getData();
rast.setPixels(0, 0, width, height, pixels);
bufferImg.setData(rast);
return bufferImg;
}
I think you are getting grey because the expression
Math.abs(( (imgR[i][j] & 0xff) >> 16 | (imgG[i][j] & 0xff) >> 8 | (imgB[i][j] & 0xff)));
does not depend on band, so your rgb values are all the same.
The expression looks dodgy anyway because you normally use the left shift operator << when packing rgb values into a single int.
I don't know for sure, as I'm not familiar with the classes you are using, but I'm guessing something like this might work
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
pixels[(((i*width)+j)*numBandas)] = imgR[i][j] & 0xFF;
pixels[(((i*width)+j)*numBandas + 1)] = imgG[i][j] & 0xFF;
pixels[(((i*width)+j)*numBandas + 2)] = imgB[i][j] & 0xFF;
}
}
If you want a faster approach, you need to get the "live" WritableRaster from the BufferedImage and set pixels in the "native" format of the image, which is "pixel packed" for TYPE_INT_RGB. This will save you multiple (at least two) array copies and some data conversion. It will also save you 2/3rds of the memory used for the conversion, as we only need a single array component per pixel.
The below method should be quite a bit faster:
public static BufferedImage array_rasterToBuffer(int[][] imgR, int[][] imgG, int[][] imgB) {
final int width = imgR[0].length;
final int height = imgR.length;
// The bands are "packed" for TYPE_INT_RGB Raster,
// so we need only one array component per pixel
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// "Pack" RGB values to native TYPE_INT_RGB format
// (NOTE: Do not use Math.abs on these values, and without alpha there won't be negative values)
pixels[((y * width) + x)] = ((imgR[y][x] & 0xff) << 16 | (imgG[y][x] & 0xff) << 8 | (imgB[y][x] & 0xff));
}
}
BufferedImage bufferImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// NOTE: getRaster rather than getData for "live" view
WritableRaster rast = bufferImg.getRaster();
// NOTE: setDataElements rather than setPixels to avoid conversion
// This requires pixels to be in "native" packed RGB format (as above)
rast.setDataElements(0, 0, width, height, pixels);
// No need for setData as we were already working on the live data
// thus saving at least two expensive array copies
return bufferImg;
}
// Test method, displaying red/green/blue stripes
public static void main(String[] args) {
int[][] fooR = new int[99][99];
int[][] fooG = new int[99][99];
int[][] fooB = new int[99][99];
for (int i = 0; i < 33; i++) {
Arrays.fill(fooR[i], 0xff);
Arrays.fill(fooG[i + 33], 0xff);
Arrays.fill(fooB[i + 66], 0xff);
}
BufferedImage image = array_rasterToBuffer(fooR, fooG, fooB);
showIt(image);
}
// For demonstration only
private static void showIt(final BufferedImage image) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("JPEGTest");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JScrollPane scroll = new JScrollPane(new JLabel(new ImageIcon(image)));
scroll.setBorder(BorderFactory.createEmptyBorder());
frame.add(scroll);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
It is possible to optimize this further, if you don't need a "managed" (possible hardware accelerated for display) image. The trick is to create the image directly "around" your pixels array, thus saving one more array allocation and array copy in setDataElements. The downside is that in some cases the image will be a little slower to draw onto the screen. This is mainly a concern for games or smooth animations though.
Replace the lines from BufferedImage bufferImg = new BufferedImage... until the return statement, with the following code:
DataBufferInt buffer = new DataBufferInt(pixels, pixels.length);
int[] bandMasks = {0xFF0000, 0xFF00, 0xFF}; // RGB (no alpha)
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, width, bandMasks, null);
ColorModel cm = new DirectColorModel(32,
0x00ff0000, // Red
0x0000ff00, // Green
0x000000ff, // Blue
0x00000000 // No Alpha
);
BufferedImage bufferImg = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
PS: Note that I also changed the shifts inside the x/y loop, from right to left shifts. Might have been just a minor typo. :-)

Color quantization with N out of M predefined colors

I am having a slightly odd problem trying to quantize and dither an RGB image. Ideally, I should be able to implement a suitable algorithm in Java or use a Java library, but references to implementations in other languages may be helpful as well.
The following is given as input:
image: 24-bit RGB bitmap
palette: a list of colors defined with their RGB values
max_cols: the maximum number of colours to be used in the output image
It is perhaps important, that both the size of the palette as well as the maximum number of allowed colours is not necessarily a power of 2 and may be greater than 255.
So, the goal is to take the image, select up to max_cols colours from the provided palette and output an image using only the picked colours and rendered using some kind of error-diffusion dithering. Which dithering algorithm to use is not that important, but it should be an error-diffusion variant (e.g. Floyd-Steinberg) and not simple halftone or ordered dithering.
Performance is not particularly important and the size of the expected data input is relatively small. The images would rarely be larger than 500x500 pixel, the provided palette may contain some 3-400 colours and the number of colours will usually be limited to less than 100. It is also safe to assume that the palette contains a wide selection of colours, covering variations of both hue, saturation and brightness.
The palette selection and dithering used by scolorq would be ideal, but it does not seem easy to adapt the algorithm to select colours from an already defined palette instead of arbitrary colours.
To be more precise, the problem where I am stuck is the selection of suitable colours from the provided palette. Assume that I e.g. use scolorq to create a palette with N colours and later replace the colours defined by scolorq with the closest colours from the provided palette, and then use these colours combined with error-diffused dithering. This will produce a result at least similar to the input image, but due to the unpredictable hues of the selected colours, the output image may get a strong, undesired colour cast. E.g. when using a grey-scale input image and a palette with only few neutral gray tones, but a great range of brown tones (or more generally, many colours with the same hue, low saturation and a great variation in the brightness), my colour selection algorithm seem to prefer these colours above the neutral greys since the brown tones are at least mathematically closer to the desired colour than the greys. The same problem remains even if I convert the RGB values to HSB and use different weights for the H, S and B channels when trying to find the nearest available colour.
Any suggestions how to implement this properly, or even better a library I can use to perform the task?
Since Xabster asked, I can also explain the goal with this excercise, although it has nothing to do with how the actual problem can be solved. The target for the output image is an embroidery or tapestry pattern. In the most simplest case, each pixel in the output image corresponds to a stitch made on some kind of carrier fabric. The palette corresponds to the available yarns, which usually come in several hundred colours. For practical reasons, it is however necessary to limit the number of colours used in the actual work. Googling for gobelin embroideries will give several examples.
And to clarify where the problem exactly lies... The solution can indeed be split into two separate steps:
selecting the optimal subset of the original palette
using the subset to render the output image
Here, the first step is the actual problem. If the palette selection works properly, I could simply use the selected colours and e.g. Floyd-Steinberg dithering to produce a reasonable result (which is rather trivial to implement).
If I understand the implementation of scolorq correctly, scolorq however combines these two steps, using knowledge of the dithering algorithm in the palette selection to create an even better result. That would of course be a preferred solution, but the algorithms used in scolorq work slightly beyond my mathematical knowledge.
OVERVIEW
This is a possible approach to the problem:
1) Each color from the input pixels is mapped to the closest color from the input color palette.
2) If the resulting palette is greater than the allowed maximum number of colors, the palette gets reduced to the maximum allowed number, by removing the colors, that are most similar with each other from the computed palette (I did choose the nearest distance for removal, so the resulting image remains high in contrast).
3) If the resulting palette is smaller than the allowed maximum number of colors, it gets filled with the most similar colors from the remaining colors of the input palette until the allowed number of colors is reached. This is done in the hope, that the dithering algorithm could make use of these colors during dithering. Note though that I didn't see much difference between filling or not filling the palette for the Floyd-Steinberg algorithm...
4) As a last step the input pixels get dithered with the computed palette.
IMPLEMENTATION
Below is an implementation of this approach.
If you want to run the source code, you will need this class: ImageFrame.java. You can set the input image as the only program argument, all other parameters must be set in the main method. The used Floyd-Steinberg algorithm is from Floyd-Steinberg dithering.
One can choose between 3 different reduction strategies for the palette reduction algorithm:
1) ORIGINAL_COLORS: This algorithm tries to stay as true to the input pixel colors as possible by searching for the two colors in the palette, that have the least distance. From these two colors it removes the one with the fewest mappings to pixels in the input map.
2) BETTER_CONTRAST: Works like ORIGINAL_COLORS, with the difference, that from the two colors it removes the one with the lowest average distance to the rest of the palette.
3) AVERAGE_DISTANCE: This algorithm always removes the colors with the lowest average distance from the pool. This setting can especially improve the quality of the resulting image for grayscale palettes.
Here is the complete code:
import java.awt.Color;
import java.awt.Image;
import java.awt.image.PixelGrabber;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
public class Quantize {
public static class RGBTriple {
public final int[] channels;
public RGBTriple() { channels = new int[3]; }
public RGBTriple(int color) {
int r = (color >> 16) & 0xFF;
int g = (color >> 8) & 0xFF;
int b = (color >> 0) & 0xFF;
channels = new int[]{(int)r, (int)g, (int)b};
}
public RGBTriple(int R, int G, int B)
{ channels = new int[]{(int)R, (int)G, (int)B}; }
}
/* The authors of this work have released all rights to it and placed it
in the public domain under the Creative Commons CC0 1.0 waiver
(http://creativecommons.org/publicdomain/zero/1.0/).
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Retrieved from: http://en.literateprograms.org/Floyd-Steinberg_dithering_(Java)?oldid=12476
*/
public static class FloydSteinbergDither
{
private static int plus_truncate_uchar(int a, int b) {
if ((a & 0xff) + b < 0)
return 0;
else if ((a & 0xff) + b > 255)
return (int)255;
else
return (int)(a + b);
}
private static int findNearestColor(RGBTriple color, RGBTriple[] palette) {
int minDistanceSquared = 255*255 + 255*255 + 255*255 + 1;
int bestIndex = 0;
for (int i = 0; i < palette.length; i++) {
int Rdiff = (color.channels[0] & 0xff) - (palette[i].channels[0] & 0xff);
int Gdiff = (color.channels[1] & 0xff) - (palette[i].channels[1] & 0xff);
int Bdiff = (color.channels[2] & 0xff) - (palette[i].channels[2] & 0xff);
int distanceSquared = Rdiff*Rdiff + Gdiff*Gdiff + Bdiff*Bdiff;
if (distanceSquared < minDistanceSquared) {
minDistanceSquared = distanceSquared;
bestIndex = i;
}
}
return bestIndex;
}
public static int[][] floydSteinbergDither(RGBTriple[][] image, RGBTriple[] palette)
{
int[][] result = new int[image.length][image[0].length];
for (int y = 0; y < image.length; y++) {
for (int x = 0; x < image[y].length; x++) {
RGBTriple currentPixel = image[y][x];
int index = findNearestColor(currentPixel, palette);
result[y][x] = index;
for (int i = 0; i < 3; i++)
{
int error = (currentPixel.channels[i] & 0xff) - (palette[index].channels[i] & 0xff);
if (x + 1 < image[0].length) {
image[y+0][x+1].channels[i] =
plus_truncate_uchar(image[y+0][x+1].channels[i], (error*7) >> 4);
}
if (y + 1 < image.length) {
if (x - 1 > 0) {
image[y+1][x-1].channels[i] =
plus_truncate_uchar(image[y+1][x-1].channels[i], (error*3) >> 4);
}
image[y+1][x+0].channels[i] =
plus_truncate_uchar(image[y+1][x+0].channels[i], (error*5) >> 4);
if (x + 1 < image[0].length) {
image[y+1][x+1].channels[i] =
plus_truncate_uchar(image[y+1][x+1].channels[i], (error*1) >> 4);
}
}
}
}
}
return result;
}
public static void generateDither(int[] pixels, int[] p, int w, int h){
RGBTriple[] palette = new RGBTriple[p.length];
for (int i = 0; i < palette.length; i++) {
int color = p[i];
palette[i] = new RGBTriple(color);
}
RGBTriple[][] image = new RGBTriple[w][h];
for (int x = w; x-- > 0; ) {
for (int y = h; y-- > 0; ) {
int index = y * w + x;
int color = pixels[index];
image[x][y] = new RGBTriple(color);
}
}
int[][] result = floydSteinbergDither(image, palette);
convert(result, pixels, p, w, h);
}
public static void convert(int[][] result, int[] pixels, int[] p, int w, int h){
for (int x = w; x-- > 0; ) {
for (int y = h; y-- > 0; ) {
int index = y * w + x;
int index2 = result[x][y];
pixels[index] = p[index2];
}
}
}
}
private static class PaletteColor{
final int color;
public PaletteColor(int color) {
super();
this.color = color;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + color;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PaletteColor other = (PaletteColor) obj;
if (color != other.color)
return false;
return true;
}
public List<Integer> indices = new ArrayList<>();
}
public static int[] getPixels(Image image) throws IOException {
int w = image.getWidth(null);
int h = image.getHeight(null);
int pix[] = new int[w * h];
PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, pix, 0, w);
try {
if (grabber.grabPixels() != true) {
throw new IOException("Grabber returned false: " +
grabber.status());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return pix;
}
/**
* Returns the color distance between color1 and color2
*/
public static float getPixelDistance(PaletteColor color1, PaletteColor color2){
int c1 = color1.color;
int r1 = (c1 >> 16) & 0xFF;
int g1 = (c1 >> 8) & 0xFF;
int b1 = (c1 >> 0) & 0xFF;
int c2 = color2.color;
int r2 = (c2 >> 16) & 0xFF;
int g2 = (c2 >> 8) & 0xFF;
int b2 = (c2 >> 0) & 0xFF;
return (float) getPixelDistance(r1, g1, b1, r2, g2, b2);
}
public static double getPixelDistance(int r1, int g1, int b1, int r2, int g2, int b2){
return Math.sqrt(Math.pow(r2 - r1, 2) + Math.pow(g2 - g1, 2) + Math.pow(b2 - b1, 2));
}
/**
* Fills the given fillColors palette with the nearest colors from the given colors palette until
* it has the given max_cols size.
*/
public static void fillPalette(List<PaletteColor> fillColors, List<PaletteColor> colors, int max_cols){
while (fillColors.size() < max_cols) {
int index = -1;
float minDistance = -1;
for (int i = 0; i < fillColors.size(); i++) {
PaletteColor color1 = colors.get(i);
for (int j = 0; j < colors.size(); j++) {
PaletteColor color2 = colors.get(j);
if (color1 == color2) {
continue;
}
float distance = getPixelDistance(color1, color2);
if (index == -1 || distance < minDistance) {
index = j;
minDistance = distance;
}
}
}
PaletteColor color = colors.get(index);
fillColors.add(color);
}
}
public static void reducePaletteByAverageDistance(List<PaletteColor> colors, int max_cols, ReductionStrategy reductionStrategy){
while (colors.size() > max_cols) {
int index = -1;
float minDistance = -1;
for (int i = 0; i < colors.size(); i++) {
PaletteColor color1 = colors.get(i);
float averageDistance = 0;
int count = 0;
for (int j = 0; j < colors.size(); j++) {
PaletteColor color2 = colors.get(j);
if (color1 == color2) {
continue;
}
averageDistance += getPixelDistance(color1, color2);
count++;
}
averageDistance/=count;
if (minDistance == -1 || averageDistance < minDistance) {
minDistance = averageDistance;
index = i;
}
}
PaletteColor removed = colors.remove(index);
// find the color with the least distance:
PaletteColor best = null;
minDistance = -1;
for (int i = 0; i < colors.size(); i++) {
PaletteColor c = colors.get(i);
float distance = getPixelDistance(c, removed);
if (best == null || distance < minDistance) {
best = c;
minDistance = distance;
}
}
best.indices.addAll(removed.indices);
}
}
/**
* Reduces the given color palette until it has the given max_cols size.
* The colors that are closest in distance to other colors in the palette
* get removed first.
*/
public static void reducePalette(List<PaletteColor> colors, int max_cols, ReductionStrategy reductionStrategy){
if (reductionStrategy == ReductionStrategy.AVERAGE_DISTANCE) {
reducePaletteByAverageDistance(colors, max_cols, reductionStrategy);
return;
}
while (colors.size() > max_cols) {
int index1 = -1;
int index2 = -1;
float minDistance = -1;
for (int i = 0; i < colors.size(); i++) {
PaletteColor color1 = colors.get(i);
for (int j = i+1; j < colors.size(); j++) {
PaletteColor color2 = colors.get(j);
if (color1 == color2) {
continue;
}
float distance = getPixelDistance(color1, color2);
if (index1 == -1 || distance < minDistance) {
index1 = i;
index2 = j;
minDistance = distance;
}
}
}
PaletteColor color1 = colors.get(index1);
PaletteColor color2 = colors.get(index2);
switch (reductionStrategy) {
case BETTER_CONTRAST:
// remove the color with the lower average distance to the other palette colors
int count = 0;
float distance1 = 0;
float distance2 = 0;
for (PaletteColor c : colors) {
if (c != color1 && c != color2) {
count++;
distance1 += getPixelDistance(color1, c);
distance2 += getPixelDistance(color2, c);
}
}
if (count != 0 && distance1 != distance2) {
distance1 /= (float)count;
distance2 /= (float)count;
if (distance1 < distance2) {
// remove color 1;
colors.remove(index1);
color2.indices.addAll(color1.indices);
} else{
// remove color 2;
colors.remove(index2);
color1.indices.addAll(color2.indices);
}
break;
}
//$FALL-THROUGH$
default:
// remove the color with viewer mappings to the input pixels
if (color1.indices.size() < color2.indices.size()) {
// remove color 1;
colors.remove(index1);
color2.indices.addAll(color1.indices);
} else{
// remove color 2;
colors.remove(index2);
color1.indices.addAll(color2.indices);
}
break;
}
}
}
/**
* Creates an initial color palette from the given pixels and the given palette by
* selecting the colors with the nearest distance to the given pixels.
* This method also stores the indices of the corresponding pixels inside the
* returned PaletteColor instances.
*/
public static List<PaletteColor> createInitialPalette(int pixels[], int[] palette){
Map<Integer, Integer> used = new HashMap<>();
ArrayList<PaletteColor> result = new ArrayList<>();
for (int i = 0, l = pixels.length; i < l; i++) {
double bestDistance = Double.MAX_VALUE;
int bestIndex = -1;
int pixel = pixels[i];
int r1 = (pixel >> 16) & 0xFF;
int g1 = (pixel >> 8) & 0xFF;
int b1 = (pixel >> 0) & 0xFF;
for (int k = 0; k < palette.length; k++) {
int pixel2 = palette[k];
int r2 = (pixel2 >> 16) & 0xFF;
int g2 = (pixel2 >> 8) & 0xFF;
int b2 = (pixel2 >> 0) & 0xFF;
double dist = getPixelDistance(r1, g1, b1, r2, g2, b2);
if (dist < bestDistance) {
bestDistance = dist;
bestIndex = k;
}
}
Integer index = used.get(bestIndex);
PaletteColor c;
if (index == null) {
index = result.size();
c = new PaletteColor(palette[bestIndex]);
result.add(c);
used.put(bestIndex, index);
} else{
c = result.get(index);
}
c.indices.add(i);
}
return result;
}
/**
* Creates a simple random color palette
*/
public static int[] createRandomColorPalette(int num_colors){
Random random = new Random(101);
int count = 0;
int[] result = new int[num_colors];
float add = 360f / (float)num_colors;
for(float i = 0; i < 360f && count < num_colors; i += add) {
float hue = i;
float saturation = 90 +random.nextFloat() * 10;
float brightness = 50 + random.nextFloat() * 10;
result[count++] = Color.HSBtoRGB(hue, saturation, brightness);
}
return result;
}
public static int[] createGrayScalePalette(int count){
float[] grays = new float[count];
float step = 1f/(float)count;
grays[0] = 0;
for (int i = 1; i < count-1; i++) {
grays[i]=i*step;
}
grays[count-1]=1;
return createGrayScalePalette(grays);
}
/**
* Returns a grayscale palette based on the given shades of gray
*/
public static int[] createGrayScalePalette(float[] grays){
int[] result = new int[grays.length];
for (int i = 0; i < result.length; i++) {
float f = grays[i];
result[i] = Color.HSBtoRGB(0, 0, f);
}
return result;
}
private static int[] createResultingImage(int[] pixels,List<PaletteColor> paletteColors, boolean dither, int w, int h) {
int[] palette = new int[paletteColors.size()];
for (int i = 0; i < palette.length; i++) {
palette[i] = paletteColors.get(i).color;
}
if (!dither) {
for (PaletteColor c : paletteColors) {
for (int i : c.indices) {
pixels[i] = c.color;
}
}
} else{
FloydSteinbergDither.generateDither(pixels, palette, w, h);
}
return palette;
}
public static int[] quantize(int[] pixels, int widht, int heigth, int[] colorPalette, int max_cols, boolean dither, ReductionStrategy reductionStrategy) {
// create the initial palette by finding the best match colors from the given color palette
List<PaletteColor> paletteColors = createInitialPalette(pixels, colorPalette);
// reduce the palette size to the given number of maximum colors
reducePalette(paletteColors, max_cols, reductionStrategy);
assert paletteColors.size() <= max_cols;
if (paletteColors.size() < max_cols) {
// fill the palette with the nearest remaining colors
List<PaletteColor> remainingColors = new ArrayList<>();
Set<PaletteColor> used = new HashSet<>(paletteColors);
for (int i = 0; i < colorPalette.length; i++) {
int color = colorPalette[i];
PaletteColor c = new PaletteColor(color);
if (!used.contains(c)) {
remainingColors.add(c);
}
}
fillPalette(paletteColors, remainingColors, max_cols);
}
assert paletteColors.size() == max_cols;
// create the resulting image
return createResultingImage(pixels,paletteColors, dither, widht, heigth);
}
static enum ReductionStrategy{
ORIGINAL_COLORS,
BETTER_CONTRAST,
AVERAGE_DISTANCE,
}
public static void main(String args[]) throws IOException {
// input parameters
String imageFileName = args[0];
File file = new File(imageFileName);
boolean dither = true;
int colorPaletteSize = 80;
int max_cols = 3;
max_cols = Math.min(max_cols, colorPaletteSize);
// create some random color palette
// int[] colorPalette = createRandomColorPalette(colorPaletteSize);
int[] colorPalette = createGrayScalePalette(20);
ReductionStrategy reductionStrategy = ReductionStrategy.AVERAGE_DISTANCE;
// show the original image inside a frame
ImageFrame original = new ImageFrame();
original.setImage(file);
original.setTitle("Original Image");
original.setLocation(0, 0);
Image image = original.getImage();
int width = image.getWidth(null);
int heigth = image.getHeight(null);
int pixels[] = getPixels(image);
int[] palette = quantize(pixels, width, heigth, colorPalette, max_cols, dither, reductionStrategy);
// show the reduced image in another frame
ImageFrame reduced = new ImageFrame();
reduced.setImage(width, heigth, pixels);
reduced.setTitle("Quantized Image (" + palette.length + " colors, dither: " + dither + ")");
reduced.setLocation(100, 100);
}
}
POSSIBLE IMPROVEMENTS
1) The used Floyd-Steinberg algorithm does currently only work for palettes with a maximum size of 256 colors. I guess this could be fixed easily, but since the used FloydSteinbergDither class requires quite a lot of conversions at the moment, it would certainly be better to implement the algorithm from scratch so it fits the color model that is used in the end.
2) I believe using another dithering algorithm like scolorq would perhaps be better. On the "To Do List" at the end of their homepage they write:
[TODO:] The ability to fix some colors to a predetermined set (supported by the algorithm but not the current implementation)
So it seems using a fixed palette should be possible for the algorithm. The Photoshop/Gimp plugin Ximagic seems to implement this functionality using scolorq. From their homepage:
Ximagic Quantizer is a Photoshop plugin for image color quantization (color reduction) & dithering.
Provides: Predefined palette quantization
3) The algorithm to fill the palette could perhaps be improved - e.g. by filling the palette with colors depending on their average distance (like in the reduction algorithm). But this should be tested depending on the finally used dithering algorithm.
EDIT: I think I may have answered a slightly different question. jarnbjo pointed out something that may be wrong with my solution, and I realized I misunderstood the question. I'm leaving my answer here for posterity, though.
I may have a solution to this in Matlab. To find the closest color, I used the weights given by Albert Renshaw in a comment here. I used the HSV colorspace, but all inputs to the code were in standard RGB. Greyscale iamges were converted to 3-channel greyscale images.
To select the best colors to use, I seeded kmeans with the test sample palette and then reset the centroids to be the values they were closest to in the sample pallet.
function imo = recolor(im,new_colors,max_colors)
% Convert to HSV
im2 = rgb2hsv(im);
new_colors = rgb2hsv(new_colors);
% Get number of colors in palette
num_colors = uint8(size(new_colors,1));
% Reshape image so every row is a diferent pixel, and every column a channel
% this is necessary for kmeans in Matlab
im2 = reshape(im2, size(im,1)*size(im,2),size(im,3));
% Seed kmeans with sample pallet, drop empty clusters
[IDX, C] = kmeans(im2,max_colors,'emptyaction','drop');
% For each pixel, IDX tells which cluster in C it corresponds to
% C contains the centroids of each cluster
% Because centroids are adjusted from seeds, we need to select which original color
% in the palette it corresponds to. We cannot be sure that the centroids in C correspond
% to their seed values
% Note that Matlab starts indexing at 1 instead of 0
for i=1:size(C,1)
H = C(i,1);
S = C(i,2);
V = C(i,3);
bdel = 100;
% Find which color in the new_colors palette is closest
for j=1:size(new_colors,1)
H2 = new_colors(j,1);
S2 = new_colors(j,2);
V2 = new_colors(j,3);
dH = (H2-H)^2*0.475;
dS = (S2-S)^2*0.2875;
dV = (V2-V)^2*0.2375;
del = sqrt(dH+dS+dV);
if isnan(del)
continue
end
% update if the new delta is lower than the best
if del<bdel
bdel = del;
C(i,:) = new_colors(j,:);
end
end
end
% Update the colors, this is equal to the following
% for i=1:length(imo)
% imo(i,:) = C(IDX(i),:)
imo = C(IDX,:);
% put it back in its original shape
imo = reshape(imo, size(im));
imo = hsv2rgb(imo);
imshow(imo);
The problem with it right now as I have it written is that it is very slow for color images (Lenna took several minutes).
Is this along the lines of what you are looking for?
Examples.
If you don't understand all the Matlab notation, let me know.
First of all I'd like to insist on the fact that this is no advanced distance color computation.
So far I assumed the first palette is one you either configured or precalculated from an image.
Here, I only configured it and focused on the subpalette extraction problem. I did not use an algorithm, it's highly probable that it may not be the best.
Store an image into a canvas 2d context which will serve as a buffer, I'll refer to it as ctxHidden
Store pixels data of ctxHidden into a variable called img
Loop through entire img with function constraintImageData(img, palette) which accepts as argument img and the palette to transform current img pixels to given colors with the help of the distance function nearestColor(palette, r, g, b, a). Note that this function returns a witness, which basically counts how many times each colors of the palette being used at least once. My example also applies a Floyd-Steinberg dithering, even though you mentionned it was not a problem.
Use the witness to sort descending by colors apparition frequency (from the palette)
Extract these colors from the initial palette to get a subpalette according to maxColors (or max_colors)
Draw the image with the final subpalette, from ctxHidden original data.
You must expect your final image to give you squishy results if maxColors is too low or if your original palette is too distant from the original image colors.
I did a jsfiddle with processing.js, and it is clearly not necessary here but I started using it so I left it as is.
Now here is what the code looks like (the second canvas is the result, applying the final subpalette with a delay of 3 seconds)
var image = document.getElementById('original'),
palettePanel = document.getElementById('palette'),
subPalettePanel = document.getElementById('subpalette'),
canvas = document.getElementById('main'),
maxColors = 12,
palette = [
0x7F8FB1FF,
0x000000FF,
0x404c00FF,
0xe46501FF,
0x722640FF,
0x40337fFF,
0x666666FF,
0x0e5940FF,
0x1bcb01FF,
0xbfcc80FF,
0x333333FF,
0x0033CCFF,
0x66CCFFFF,
0xFF6600FF,
0x000033FF,
0xFFCC00FF,
0xAA0033FF,
0xFF00FFFF,
0x00FFFFFF,
0x123456FF
],
nearestColor = function (palette, r, g, b, a) {
var rr, gg, bb, aa, color, closest,
distr, distg, distb, dista,
dist,
minDist = Infinity;
for (var i = 0; i < l; i++) {
color = palette[i];
rr = palette[i] >> 24 & 0xFF;
gg = palette[i] >> 16 & 0xFF;
bb = palette[i] >> 8 & 0xFF;
aa = palette[i] & 0xFF;
if (closest === undefined) {
closest = color;
}
// compute abs value
distr = Math.abs(rr - r);
distg = Math.abs(gg - g);
distb = Math.abs(bb - b);
dista = Math.abs(aa - a);
dist = (distr + distg + distb + dista * .5) / 3.5;
if (dist < minDist) {
closest = color;
minDist = dist;
}
}
return closest;
},
subpalette = [],
i, l = palette.length,
r, g, b, a,
img,
size = 5,
cols = palettePanel.width / size,
drawPalette = function (p, palette) {
var i, l = palette.length;
p.setup = function () {
p.size(50,50);
p.background(255);
p.noStroke();
for (i = 0; i < l; i++) {
r = palette[i] >> 24 & 0xFF;
g = palette[i] >> 16 & 0xFF;
b = palette[i] >> 8 & 0xFF;
a = palette[i] & 0xFF;
p.fill(r,g,b,a);
p.rect (i%cols*size, ~~(i/cols)*size, size, size);
}
}
},
constraintImageDataToPalette = function (img, palette) {
var i, l, x, y, index,
pixel, x, y,
right, bottom, bottomLeft, bottomRight,
color,
r, g, b, a, i, l,
pr, pg, pb, pa,
rErrorBase,
gErrorBase,
bErrorBase,
aErrorBase,
index,
w = img.width,
w4 = w*4,
h = img.height,
witness = {};
for (i = 0, l = w*h*4; i < l; i += 4) {
x = (i%w);
y = ~~(i/w);
index = x + y*w;
right = index + 4,
bottomLeft = index - 4 + w4,
bottom = index + w4,
bottomRight = index + w4 + 4,
pixel = img.data;
r = pixel[index];
g = pixel[index+1];
b = pixel[index+2];
a = pixel[index+3];
color = nearestColor(palette, r,g,b,a);
witness[color] = (witness[color] || 0) + 1;
// explode channels
pr = color >> 24 & 0xFF;
pg = color >> 16 & 0xFF;
pb = color >> 8 & 0xFF;
pa = color & 0xFF;
// set new color
pixel[index] = pr;
pixel[index+1] = pg;
pixel[index+2] = pb;
pixel[index+3] = pa;
// calculate error
rErrorBase = (r - pr);
gErrorBase = (g - pg);
bErrorBase = (b - pb);
aErrorBase = (a - pa);
///*
// diffuse error right 7/16 = 0.4375
pixel[right] += 0.4375 * rErrorBase;
pixel[right+1] += 0.4375 * gErrorBase;
pixel[right+2] += 0.4375 * bErrorBase;
pixel[right+3] += 0.4375 * aErrorBase;
// diffuse error bottom-left 3/16 = 0.1875
pixel[bottomLeft] += 0.1875 * rErrorBase;
pixel[bottomLeft+1] += 0.1875 * gErrorBase;
pixel[bottomLeft+2] += 0.1875 * bErrorBase;
pixel[bottomLeft+3] += 0.1875 * aErrorBase;
// diffuse error bottom 5/16 = 0.3125
pixel[bottom] += 0.3125 * rErrorBase;
pixel[bottom+1] += 0.3125 * gErrorBase;
pixel[bottom+2] += 0.3125 * bErrorBase;
pixel[bottom+3] += 0.3125 * aErrorBase;
//diffuse error bottom-right 1/16 = 0.0625
pixel[bottomRight] += 0.0625 * rErrorBase;
pixel[bottomRight+1] += 0.0625 * gErrorBase;
pixel[bottomRight+2] += 0.0625 * bErrorBase;
pixel[bottomRight+3] += 0.0625 * aErrorBase;
//*/
}
return witness;
};
new Processing(palettePanel, function (p) { drawPalette(p, palette); });
image.onload = function () {
var l = palette.length;
new Processing(canvas, function (p) {
// argb 24 bits colors
p.setup = function () {
p.size(300, 200);
p.background(0);
p.noStroke();
var ctx = canvas.getContext('2d'),
ctxHidden = document.getElementById('buffer').getContext('2d'),
img, log = [],
witness = {};
ctxHidden.drawImage(image, 0, 0);
img = ctxHidden.getImageData(0, 0, canvas.width, canvas.height);
// constraint colors to largest palette
witness = constraintImageDataToPalette(img, palette);
// show which colors have been picked from the panel
new Processing(subPalettePanel, function (p) { drawPalette(p, Object.keys(witness)); });
ctx.putImageData(img, 0, 0);
var colorsWeights = [];
for (var key in witness) {
colorsWeights.push([+key, witness[key]]);
}
// sort descending colors by most presents ones
colorsWeights.sort(function (a, b) {
return b[1] - a[1];
});
// get the max_colors first of the colors picked to ensure a higher probability of getting a good color
subpalette = colorsWeights
.slice(0, maxColors)
.map(function (colorValueCount) {
// return the actual color code
return colorValueCount[0];
});
// reset image we previously modified
img = ctxHidden.getImageData(0, 0, canvas.width, canvas.height);
// this time constraint with new subpalette
constraintImageDataToPalette(img, subpalette);
// wait 3 seconds to apply new palette and show exactly how it changed
setTimeout(function () {
new Processing(subPalettePanel, function (p) { drawPalette(p, subpalette); });
ctx.putImageData(img, 0, 0);
}, 3000);
};
});
};
NOTE: I have no experience in java image computation, so I used javascript instead. I tried to comment my code, if you have any question about it I'll answer and explain it.
Below is presented an approach implemented in Java using Marvin Framework. It might be a starting point for solving your problem.
Input:
Palette P with M colors.
Number of Colors N.
Image G
Steps:
Apply the Palette P to the image G by replacing the pixels color to the most similar color (less distance in RGB space) in the palette. The output image has the distribution of palette colors by usage.
Compute an histogram containing each color in the palette and how many times it is used in the image (number of pixels).
Sort the palette by pixel usage, most to less used.
Select the N first items in the sorted list and generate a new palette.
Apply this new palette to the image.
Below is presented the output of this approach.
Original image:
(source: sourceforge.net)
Palette, and the image quantitized with 32, 8, 4 colors:
Source code:
public class ColorQuantizationExample {
public ColorQuantizationExample(){
MarvinImage imageOriginal = MarvinImageIO.loadImage("./res/quantization/lena.jpg");
MarvinImage imageOutput = new MarvinImage(imageOriginal.getWidth(), imageOriginal.getHeight());
Set<Color> palette = loadPalette("./res/quantization/palette_7.png");
quantitize(imageOriginal, imageOutput, palette, 32);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_32.jpg");
quantitize(imageOriginal, imageOutput, palette, 8);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_8.jpg");
quantitize(imageOriginal, imageOutput, palette, 4);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_4.jpg");
palette = loadPalette("./res/quantization/palette_8.png");
quantitize(imageOriginal, imageOutput, palette, 32);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_32.jpg");
quantitize(imageOriginal, imageOutput, palette, 8);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_8.jpg");
quantitize(imageOriginal, imageOutput, palette, 4);
MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_4.jpg");
}
/**
* Load a set of colors from a palette image.
*/
private Set<Color> loadPalette(String path){
Set<Color> ret = new HashSet<Color>();
MarvinImage image = MarvinImageIO.loadImage(path);
String key;
for(int y=0; y<image.getHeight(); y++){
for(int x=0; x<image.getWidth(); x++){
Color c = new Color
(
image.getIntComponent0(x, y),
image.getIntComponent1(x, y),
image.getIntComponent2(x, y)
);
ret.add(c);
}
}
return ret;
}
private void quantitize(MarvinImage imageIn, MarvinImage imageOut, Set<Color> palette, int colors){
applyPalette(imageIn, imageOut, palette);
HashMap<Color, Integer> hist = getColorHistogram(imageOut);
List<Map.Entry<Color, Integer>> list = new LinkedList<Map.Entry<Color, Integer>>( hist.entrySet() );
Collections.sort( list, new Comparator<Map.Entry<Color, Integer>>()
{
#Override
public int compare( Map.Entry<Color, Integer> o1, Map.Entry<Color, Integer> o2 )
{
return (o1.getValue() > o2.getValue() ? -1: 1);
}
} );
Set<Color> newPalette = reducedPalette(list, colors);
applyPalette(imageOut.clone(), imageOut, newPalette);
}
/**
* Apply a palette to an image.
*/
private void applyPalette(MarvinImage imageIn, MarvinImage imageOut, Set<Color> palette){
Color color;
for(int y=0; y<imageIn.getHeight(); y++){
for(int x=0; x<imageIn.getWidth(); x++){
int red = imageIn.getIntComponent0(x, y);
int green = imageIn.getIntComponent1(x, y);
int blue = imageIn.getIntComponent2(x, y);
color = getNearestColor(red, green, blue, palette);
imageOut.setIntColor(x, y, 255, color.getRed(), color.getGreen(), color.getBlue());
}
}
}
/**
* Reduce the palette colors to a given number. The list is sorted by usage.
*/
private Set<Color> reducedPalette(List<Map.Entry<Color, Integer>> palette, int colors){
Set<Color> ret = new HashSet<Color>();
for(int i=0; i<colors; i++){
ret.add(palette.get(i).getKey());
}
return ret;
}
/**
* Compute color histogram
*/
private HashMap<Color, Integer> getColorHistogram(MarvinImage image){
HashMap<Color, Integer> ret = new HashMap<Color, Integer>();
for(int y=0; y<image.getHeight(); y++){
for(int x=0; x<image.getWidth(); x++){
Color c = new Color
(
image.getIntComponent0(x, y),
image.getIntComponent1(x, y),
image.getIntComponent2(x, y)
);
if(ret.get(c) == null){
ret.put(c, 0);
}
ret.put(c, ret.get(c)+1);
}
}
return ret;
}
private Color getNearestColor(int red, int green, int blue, Set<Color> palette){
Color nearestColor=null, c;
double nearestDistance=Integer.MAX_VALUE;
double tempDist;
Iterator<Color> it = palette.iterator();
while(it.hasNext()){
c = it.next();
tempDist = distance(red, green, blue, c.getRed(), c.getGreen(), c.getBlue());
if(tempDist < nearestDistance){
nearestDistance = tempDist;
nearestColor = c;
}
}
return nearestColor;
}
private double distance(int r1, int g1, int b1, int r2, int g2, int b2){
double dist= Math.pow(r1-r2,2) + Math.pow(g1-g2,2) + Math.pow(b1-b2,2);
return Math.sqrt(dist);
}
public static void main(String args[]){
new ColorQuantizationExample();
}
}

Categories