Algorithm to trend towards a given number after n iterations - java

I'm trying to find a good algorithm to accomplish the following:
I have two RGB colors. I start off with one color (say, red = 255, 0, 0) and after a number of iterations, I want it to turn blue (0, 0, 255).
My current algorithm simply takes the sum of each component of the color and divides by two, which does the trick but much too quickly. At each iteration, I want the the numbers to change by only 1 tenth of their original value. So iteration 1 might return the color (230, 0, 25) and so on. Keep in mind, the destination color can also change. so suddenly instead of blue, I want green.
Does anyone know of a good way to accomplish this? I can't seem to figure out the math.
Thank you!

There have been two posts about other color spaces and a linear approach already.
But if you are really looking for an algorithm that does exactly what you're asking for, check this out:
static class ColorChanger {
static private final int APPROACH_STEPS = 10;
private final Color mStartColor;
private final Color mTargetColor;
private int mApproachStep = 0;
private Color mCurrentColor;
public ColorChanger(final Color pStartColor, final Color pTargetColor) {
mStartColor = pStartColor;
mTargetColor = pTargetColor;
System.out.println("\nStarting color is: " + mStartColor);
System.out.println("Approaching target 1: " + mTargetColor);
}
public Color approach() {
++mApproachStep;
if (mApproachStep <= APPROACH_STEPS) { // dont overshoot target color. could throw an exception here too
final int newRedCode = nextColorCode(mStartColor.getRed(), mTargetColor.getRed());
final int newGreenCode = nextColorCode(mStartColor.getGreen(), mTargetColor.getGreen());
final int newBlueCode = nextColorCode(mStartColor.getBlue(), mTargetColor.getBlue());
mCurrentColor = new Color(newRedCode, newGreenCode, newBlueCode);
}
System.out.println("\tNew step color is: " + mCurrentColor);
return mCurrentColor;
}
private int nextColorCode(final int pCurrentCode, final int pTargetCode) {
final int diff = pTargetCode - pCurrentCode;
final int newCode = pCurrentCode + diff * mApproachStep / APPROACH_STEPS;
return newCode;
}
public Color getCurrentColor() {
return mCurrentColor;
}
public boolean isTargetColor() {
return mApproachStep == APPROACH_STEPS;
}
}
public static void main(final String[] args) {
final Color startColor = Color.RED;
final Color targetColor1 = Color.GREEN;
final Color targetColor2 = Color.BLUE;
final Color targetColor3 = Color.RED;
// approach in only 5 steps, will by far not reach target color
final ColorChanger cc1 = new ColorChanger(startColor, targetColor1);
for (int i = 0; i < 5; i++) {
cc1.approach();
}
// full approach #1
final ColorChanger cc2 = new ColorChanger(cc1.getCurrentColor(), targetColor2);
while (!cc2.isTargetColor()) {
cc2.approach();
}
// full approach #2
final ColorChanger cc3 = new ColorChanger(cc2.getCurrentColor(), targetColor3);
for (int i = 0; i < ColorChanger.APPROACH_STEPS; i++) {
cc3.approach();
}
System.out.println("Program ends");
}

Good ol' math does the job (as usual), so let's start with a more mathematical approach:
value-space RGB: [0,255]^3
Let a,b e RGB , step_w, step_no e N
f(a , b , step_w , step_no) = (a0 + (b0 - a0) / step_w * step_no , a1 + (b1 ...
From math to actual code:
Color f(Color a , Color b , int step_w , int step_no){
return new Color(a.getRed() + (b.getRed() - a.getRed()) / step_w * step_no , a.getGreen() + (b.getGreen() - a.getGreen()) / step_w * step_no , ...);
}
step_w is the number of total steps and step_no the number of steps performed so far. f(c1 , c2 , x , y) will return c1 for y = 0 and c2 for y = x and a matching color in between for 0 < y < x.
There are nicer ways for color-transformation though (lab color spaces, HSL, etc.), that look more natural.

Related

Adding Offsets to My Java Game

So, as the title reads I am trying to add offsets to my java game. I was given a tip by a friend that I need to minus the offset from where I render the tiles onto my screen.
So I created a random world generator and did the offset thing, but I ran into a problem.
My Code:
public void generateMap(Graphics g) {
block = seed.nextInt(2);
//Render Dirt
if(block == 0) {
g.drawImage(Assets.dirt, x - GameState.xOffset, y - GameState.yOffset, null);
x += 32;
}
//Render Grass
if(block == 1) {
g.drawImage(Assets.grass, x - GameState.xOffset, y - GameState.yOffset, null);
x += 32;
}
//Check Where the X is
if(x > xFinish) {
if(y < yFinish) {
x = xStart;
y += 32;
}
}
}
looks simple enough right? after I do that I create code to add one to the offset every time I loop around:
public void tick() {
xOffset += 1;
}
So after that is done I run it but it does this:
is there any simple way I can fix this so that it appears that the screen "scrolls" to the left?
Is there any simple way I can fix this...
Probably not. Games are complicated. Don't let that dissuade you.
You are generating your game world and drawing in the same methods - you don't want to do this. Separation of responsibility is very important - you don't want a whole bunch of code in one spot doing the same thing. In this case, the functionality to generate the world and the drawing code need to be split.
For the world generation, generate the game world once, and persist it to storage using whatever format you like. Keep this away from the drawing code - it has no place there.
For representing blocks in your world, consider something like this:
class Block {
public BlockType getType() {
return type;
}
public int getxPosition() {
return xPosition;
}
public int getyPosition() {
return yPosition;
}
// hashCode(), equals(), etc omitted, they should be implemented
public static enum BlockType {
Dirt(Assets.dirt),
Grass(Assets.grass);
private final BufferedImage image;
BlockType(BufferedImage image) {
this.image = image;
}
public BufferedImage getImage() {
return image;
}
}
private final BlockType type;
private final int xPosition;
private final int yPosition;
private Block(BlockType type, int xPosition, int yPosition) {
this.type = type;
this.xPosition = xPosition;
this.yPosition = yPosition;
}
public static Block getInstance(BlockType type, int xPosition, int yPosition) {
return new Block(type, xPosition, yPosition);
}
}
You can then use Block.getInstance() to generate a map once, like this:
class GameState {
private final int WORLD_SIZE = 1024;
private Block[][] _world = new Block[WORLD_SIZE][WORLD_SIZE];
private static Random seed = new Random();
public void generateMap() {
int blockTypeLength = Block.BlockType.values().length;
for (int x = 0; x < WORLD_SIZE; x++) {
for (int y = 0; y < WORLD_SIZE; y++) {
int blockType = seed.nextInt(blockTypeLength);
_world[x][y] = Block.getInstance(Block.BlockType.values()[blockType], x, y);
}
}
}
public Block[][] getMap() {
return _world; // not thread safe, shares internal state, all the usual warnings
}
This obviously isn't the only way to generate a world - you would probably generate a world and save, then load from disk in later games (unless it was a short lived game - I don't know, that's your call).
Once you've got the world sorted out, you'd move on to a different module that would handle drawing. Assume GameState has two fields playerX and playerY that represent the player's coordinates in the game world (note: direct fields like this are bad practice, but used to simplify this example):
public void paintComponent(Graphics g) {
super.paintComponent(g);
Block[][] screen = new Block[16][16]; // declare a screen buffer to draw
// Assumes player is in the center of the screen
int screenRadiusX = GameFrame.Assets.SCREENBOUNDS_X / 2 / blockSize;
int screenRadiusY = GameFrame.Assets.SCREENBOUNDS_Y / 2 / blockSize;
for (int x = state.playerX - 8, xS = 0; x < state.playerX + 8; x++, xS++) {
for (int y = state.playerY - 8, yS = 0; y < state.playerY + 8; y++, yS++) {
screen[xS][yS] = world[x][y];
}
}
for (int x = 0; x < screen.length; x++) {
for (int y = 0; y < screen.length; y++) {
Block current = screen[x][y];
g.drawImage(current.getType().getImage(),
x * blockSize, // blockSize is the pixel dimension of
y * blockSize,
null
);
}
}
}
If this helps, then great! I'm glad I was able to help. If not, or if some ideas are still unclear, then I would consider perhaps running through a tutorial or book that walks you through making a game. Don't forget to learn the platform you're coding on during such a process.

Finding solution to maze in java

I have a project to have a red dot inside the first box of a maze i randomly generated and the dot is supposed to follow its way through the boxes and find the end of the maze. Now if it hits a dead end, its supposed to go back to where its path started and not go back down that path, that leads to a dead end. i made it so each box represents the #1, this way when the red dot travels over the box, it increments by 1, so it can realize where its been. its always supposed to go to the lowest number possible so it can never go back to the dead ends its already been to. i am able to reach the end of the maze but i come into 2 problems.
the method i wrote that does all this work is the solve() function. I cant understand why 2 things happen...
1st thing is that when the red dot comes to a branch of dead ends, sometimes itll just go to one dead end, to a different dead end , back to the same dead end.. traveling to the same 'numbers' when im trying to have it only go towards the boxes that have 1's or just the lower numbers.
2nd thing is that once it inevitably reaches the end of the maze.. the red dot goes into the green area, where i specifically say in the while loop, it can not be in a green box.
if M[y][x] = 0, its a green box and if its = 1 its a black box. anything higher than 1 will also be inside the box.
your help is highly appreciated as ive been stuck on this problem for hours and cant seem to find out the problem.
the problem persists in the solve() method
import java.awt.*;
import java.awt.event.*;
import java.awt.Graphics;
import javax.swing.*;
public class mazedfs extends JFrame implements KeyListener
{
/* default values: */
private static int bh = 16; // height of a graphical block
private static int bw = 16; // width of a graphical block
private int mh = 41; // height and width of maze
private int mw = 51;
private int ah, aw; // height and width of graphical maze
private int yoff = 40; // init y-cord of maze
private Graphics g;
private int dtime = 40; // 40 ms delay time
byte[][] M; // the array for the maze
public static final int SOUTH = 0;
public static final int EAST = 1;
public static final int NORTH = 2;
public static final int WEST = 3;
public static boolean showvalue = true; // affects drawblock
// args determine block size, maze height, and maze width
public mazedfs(int bh0, int mh0, int mw0)
{
bh = bw = bh0; mh = mh0; mw = mw0;
ah = bh*mh;
aw = bw*mw;
M = new byte[mh][mw]; // initialize maze (all 0's - walls).
this.setBounds(0,0,aw+10,10+ah+yoff);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try{Thread.sleep(500);} catch(Exception e) {} // Synch with system
this.addKeyListener(this);
g = getGraphics(); //g.setColor(Color.red);
setup();
}
public void paint(Graphics g) {} // override automatic repaint
public void setup()
{
g.setColor(Color.green);
g.fill3DRect(0,yoff,aw,ah,true); // fill raised rectangle
g.setColor(Color.black);
// showStatus("Generating maze...");
digout(mh-2,mw-2); // start digging!
// digout exit
M[mh-1][mw-2] = M[mh-2][mw-1] = 1;
drawblock(mh-2,mw-1);
solve(); // this is the function you will write for parts 1 and 2
play(); // for part 3
}
public static void main(String[] args)
{
int blocksize = bh, mheight = 41, mwidth = 41; // need to be odd
if (args.length==3)
{
mheight=Integer.parseInt(args[0]);
mwidth=Integer.parseInt(args[1]);
blocksize=Integer.parseInt(args[2]);
}
mazedfs W = new mazedfs(blocksize,mheight,mwidth);
}
public void drawblock(int y, int x)
{
g.setColor(Color.black);
g.fillRect(x*bw,yoff+(y*bh),bw,bh);
g.setColor(Color.yellow);
// following line displays value of M[y][x] in the graphical maze:
if (showvalue)
g.drawString(""+M[y][x],(x*bw)+(bw/2-4),yoff+(y*bh)+(bh/2+6));
}
void drawdot(int y, int x)
{
g.setColor(Color.red);
g.fillOval(x*bw,yoff+(y*bh),bw,bh);
try{Thread.sleep(dtime);} catch(Exception e) {}
}
/////////////////////////////////////////////////////////////////////
/* function to generate random maze */
public void digout(int y, int x)
{
M[y][x] = 1; // digout maze at coordinate y,x
drawblock(y,x); // change graphical display to reflect space dug out
int dir = (int)(Math.random()*4);
for (int i=0;i<4;i++){
int [] DX = {0,0,2,-2};
int [] DY = {-2,2,0,0};
int newx = x + DX[dir];
int newy = y + DY[dir];
if(newx>=0 && newx<mw && newy>=0 && newy<mh && M[newy][newx]==0)
{
M[y+DY[dir]/2][x+DX[dir]/2] = 1;
drawblock(y+DY[dir]/2,x+DX[dir]/2);
digout(newy,newx);
}
dir = (dir + 1)%4;}
} // digout
public void solve() // This is the method i need help with.
{
int x=1, y=1;
drawdot(y,x);
while(y!=mh-1 || x!=mw-1 && M[y][x]!=0){
int min = 0x7fffffff;
int DX = 0;
int DY = 0;
if (y-1>0 && min>M[y-1][x] && M[y-1][x]!=0){
min = M[y-1][x];
DX = 0;
DY = -1;
}//ifNORTH
if (y+1>0 && min>M[y+1][x] && M[y+1][x]!=0){
min = M[y+1][x];
DY = 1;
DX = 0;
}//ifSOUTH
if (x-1>0 && min>M[y][x-1] && M[y][x-1]!=0){
min = M[y][x-1];
DX = -1;
DY = 0;
}//ifWEST
if (x+1>0 && min>M[y][x+1] && M[y][x+1]!=0){
min = M[y][x+1];
DX = 1;
DY = 0;
}//ifEAST
M[y][x]++;
drawblock(y,x);
x = x+DX;
y = y+DY;
drawdot(y,x);
}//while
// modify this function to move the dot to the end of the maze. That
// is, when the dot reaches y==mh-2, x==mw-2
} // solve
///////////////////////////////////////////////////////////////
/// For part three (save a copy of part 2 version first!), you
// need to implement the KeyListener interface.
public void play() // for part 3
{
// code to setup game
}
// for part 3 you may also define some other instance vars outside of
// the play function.
// for KeyListener interface
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}
public void keyPressed(KeyEvent e) // change this one
{
int key = e.getKeyCode(); // code for key pressed
System.out.println("YOU JUST PRESSED KEY "+key);
}
} // mazedfs
////////////
// define additional classes (stack) you may need here.
The issue causing the second problem you are facing (dot moving to green box) lies in the while loop conditiony!=mh-1 || x!=mw-1 && M[y][x]!=0 . The condition evaluates to y!=mh-1 ||(x!=mw-1 && M[y][x]!=0) since && has higher precedence over the || and || just needs one of its operand to be true. In your case, y!=mh-1 is still ture at the end of maze. Hence the loop continues and the dot moves into green area. To fix the issue modify the condition as (y!=mh-1 || x!=mw-1) && M[y][x]!=0. Hope this helps.

Maze, optimal path finding using stacks

i have a program that generates a random maze. In the maze a red dot is displayed and the red dot flashes on by each block in the maze. all the blocks in the maze are == 1 and if the red dot goes through that block, it increments ++. the red dot goes in the direction towards the lowest number, that way it wont stay in an infinite loop by a dead end. once it reaches the end, ive solved the maze.
This is where im stumped, im trying to print the red dot to find the optimal route all the way back to the beginning where it started. I used a stack class that i made to record all the y and x components of where the red dot traveled. im able to traceback every where the red dot has gone but that isnt the optimal solution.
My question is how could i print the red dot tracing back in only the optimal path. My idea of solving this would be to check and see if the coordinates of a stack where visited before, if so..find the last case where it was visited and print the red dot up until that point. that way itll never deal with the dead ends it traveled.
the method solve() is what contains the traceback and solving technique for the red dot to travel through the maze and back.
Im not the greatest programmer and im still learning how to use stacks, ive been stumped for hours and dont know how to approach this. Please be kind and explain how you would do it using the stack i made. Thank you
import java.awt.*;
import java.awt.event.*;
import java.awt.Graphics;
import javax.swing.*;
public class mazedfs extends JFrame implements KeyListener
{
/* default values: */
private static int bh = 16; // height of a graphical block
private static int bw = 16; // width of a graphical block
private int mh = 41; // height and width of maze
private int mw = 51;
private int ah, aw; // height and width of graphical maze
private int yoff = 40; // init y-cord of maze
private Graphics g;
private int dtime = 40; // 40 ms delay time
byte[][] M; // the array for the maze
public static final int SOUTH = 0;
public static final int EAST = 1;
public static final int NORTH = 2;
public static final int WEST = 3;
public static boolean showvalue = true; // affects drawblock
// args determine block size, maze height, and maze width
public mazedfs(int bh0, int mh0, int mw0)
{
bh = bw = bh0; mh = mh0; mw = mw0;
ah = bh*mh;
aw = bw*mw;
M = new byte[mh][mw]; // initialize maze (all 0's - walls).
this.setBounds(0,0,aw+10,10+ah+yoff);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try{Thread.sleep(500);} catch(Exception e) {} // Synch with system
this.addKeyListener(this);
g = getGraphics(); //g.setColor(Color.red);
setup();
}
public void paint(Graphics g) {} // override automatic repaint
public void setup()
{
g.setColor(Color.green);
g.fill3DRect(0,yoff,aw,ah,true); // fill raised rectangle
g.setColor(Color.black);
// showStatus("Generating maze...");
digout(mh-2,mw-2); // start digging!
// digout exit
M[mh-1][mw-2] = M[mh-2][mw-1] = 1;
drawblock(mh-2,mw-1);
solve(); // this is the function you will write for parts 1 and 2
play(); // for part 3
}
public static void main(String[] args)
{
int blocksize = bh, mheight = 41, mwidth = 41; // need to be odd
if (args.length==3)
{
mheight=Integer.parseInt(args[0]);
mwidth=Integer.parseInt(args[1]);
blocksize=Integer.parseInt(args[2]);
}
mazedfs W = new mazedfs(blocksize,mheight,mwidth);
}
public void drawblock(int y, int x)
{
g.setColor(Color.black);
g.fillRect(x*bw,yoff+(y*bh),bw,bh);
g.setColor(Color.yellow);
// following line displays value of M[y][x] in the graphical maze:
if (showvalue)
g.drawString(""+M[y][x],(x*bw)+(bw/2-4),yoff+(y*bh)+(bh/2+6));
}
void drawdot(int y, int x)
{
g.setColor(Color.red);
g.fillOval(x*bw,yoff+(y*bh),bw,bh);
try{Thread.sleep(dtime);} catch(Exception e) {}
}
/////////////////////////////////////////////////////////////////////
/* function to generate random maze */
public void digout(int y, int x)
{
M[y][x] = 1; // digout maze at coordinate y,x
drawblock(y,x); // change graphical display to reflect space dug out
int dir = (int)(Math.random()*4);
for (int i=0;i<4;i++){
int [] DX = {0,0,2,-2};
int [] DY = {-2,2,0,0};
int newx = x + DX[dir];
int newy = y + DY[dir];
if(newx>=0 && newx<mw && newy>=0 && newy<mh && M[newy][newx]==0)
{
M[y+DY[dir]/2][x+DX[dir]/2] = 1;
drawblock(y+DY[dir]/2,x+DX[dir]/2);
digout(newy,newx);
}
dir = (dir + 1)%4;}
} // digout
/* Write a routine to solve the maze.
Start at coordinates x=1, y=1, and stop at coordinates
x=mw-1, y=mh-2. This coordinate was especially dug out
after the program called your digout function (in the "actionPerformed"
method).
*/
public void solve()
{
int x=1, y=1;
Stack yourstack = null;
drawdot(y,x);
while(y!=mh-2 || x!=mw-2 && M[y][x]!=0){
int min = 0x7fffffff;
int DX = 0;
int DY = 0;
if (y-1>0 && min>M[y-1][x] && M[y-1][x]!=0){
min = M[y-1][x];
DX = 0;
DY = -1;
}//ifNORTH
if (y+1>0 && min>M[y+1][x] && M[y+1][x]!=0){
min = M[y+1][x];
DY = 1;
DX = 0;
}//ifSOUTH
if (x-1>0 && min>M[y][x-1] && M[y][x-1]!=0){
min = M[y][x-1];
DX = -1;
DY = 0;
}//ifWEST
if (x+1>0 && min>M[y][x+1] && M[y][x+1]!=0){
min = M[y][x+1];
DX = 1;
DY = 0;
}//ifEAST
M[y][x]++;
drawblock(y,x);
x = x+DX;
y = y+DY;
drawdot(y,x);
yourstack = new Stack(y,x,yourstack); // creates new stack for each coordinate travelled
}//while
while(yourstack != null){
yourstack = yourstack.tail;
drawdot(yourstack.y,yourstack.x); // this will traceback every box ive been through
}//while
} // solve
class Stack{
int x;
int y;
Stack tail;
public Stack(int a, int b, Stack t){
y = a;
x=b;
tail=t;
}
}//stackclass
///////////////////////////////////////////////////////////////
/// For part three (save a copy of part 2 version first!), you
// need to implement the KeyListener interface.
public void play() // for part 3
{
// code to setup game
}
// for part 3 you may also define some other instance vars outside of
// the play function.
// for KeyListener interface
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}
public void keyPressed(KeyEvent e) // change this one
{
int key = e.getKeyCode(); // code for key pressed
System.out.println("YOU JUST PRESSED KEY "+key);
}
} // mazedfs
////////////
// define additional classes (stack) you may need here.
when you trace your path back you currently just go back to your stack - but thats not the shortest path...
...whenever you can go back check the values of M around you:
byte valueOfFieldNorthOfXY = M[x][y-1]; //x/y is your current position
byte valueOfFieldWesthOfXY = M[x-1][y];
byte ...;
byte ...; //and so on...
while the first while-loop in your solve-methode simplay solves the maze by flooding it the second while-method is for going back...
and when i say flooding i mean: each time a field has been passed by the 'walker' the value of M[x][y] has been increased by 1 (when the 'walker' has walked 3x over field 5/6 then the value from M[5][6] = 3)
so when you go back from the end (#40/50) to the start (#1/1), you do this algorith:
1) i stand on x/y
2) i check the values north/east/south/west of me
2a) if i come from north, then i ignore the north field
2 ) ... and so on...
2d) if i come from west , then i ignore the west field
3) i pick that direction, where the value is the least
4) put the current field int your packPathStack and walk to
the 'best' direction
5) repeat (go back to Nr.1) until you are #1/1
example
? 4 ? //i'm standing at X (x/y)
2 x f //? are values from walls or not yet considerd
? ? ? //f is where i come from
--> i pick direction WEST(2) because thats less than NORTH(4)
implement this algorithm and you a NEW stack i call it yourPathBackStack
Stack yourPathBackStack = new Stack();
while(x != 1 && y != 1 ){ //x/y = 1/1 = start - do it until you are there (Nr. 5)
// 1) i stand on x/y
int x = yourPathBackStack.x;
int y = yourPathBackStack.y;
// 2) i check the values north/east/south/west of me
byte valueOfFieldNorthOfXY = ... ; //as seen above
// 2a) if i come from north, then i ignore the north field
if (yourstack.tail.x == x && yourstack.tail.y == y-1){
//check - i DO come from north
//make then valueOfFieldNorthOfXY very high
valueOfFieldNorthOfXY = 100; //it's the same as ignoring
}
// 2 ) ... and so on...
// 2d) if i come from west , then i ignore the west field
if (yourstack.tail.x == x-1 && ...// as seen above
// 3) i pick that direction, where the value is the least
int direction = NORTH; //lets simply start with north;
byte maxValue = 100;
if ( valueOfFieldNorthOfXY < maxValue ){ //First north
maxValue = valueOfFieldNorthOfXY;
direction = NORTH;
}
if ( valueOfFieldWestOfXY < maxValue ){ //Then east
maxValue = valueOfFieldWestOfXY ;
direction = WEST;
}
//then the also other directions
if ( value ... //as seen above
// 4) put the current field int your yourPathBackStack and walk to
// the 'best' direction
int newx = x;
int newy = y;
if (direction == NORTH){ //direction has been caclulated above
newy = newy - 1;
}
if (direc ... //with all other direction)
yourPathBackStack = new Stack(newx, newy, yourPathBackStack );
drawdot(yourPathBackStack.y,yourPathBackStack.x);
}

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();
}
}

Compare color shades?

I have two colors, how do I check if they are the same color but just a different shade? I've been trying but I cant seem to figure it out, I really don't know what I'm doing lol... This is what I have so far:
import java.awt.Color;
public class Sandbox {
public Sandbox() {
Color c = new Color(5349322);
int r, g, b;
r = c.getBlue();
g = c.getGreen();
b = c.getRed();
System.out.println("Red: " + r);
System.out.println("Green: " + g);
System.out.println("Blue: " + b);
}
private boolean FindColorTol(int intTargetColor, int Tolerance) {
Color targetColor = new Color(intTargetColor);
Color imgColor = new Color(5349322);
int targetRED = targetColor.getBlue(),
targetGREEN = targetColor.getGreen(),
targetBLUE = targetColor.getRed(),
imgRED = imgColor.getBlue(),
imgGREEN = imgColor.getGreen(),
imgBLUE = imgColor.getRed();
return false;
}
private int getLargest(int...values) {
int largest = 0;
for(int i = 0; i < values.length; i++) {
if(values.length > i + 1) {
if(values[i] > values[i + 1])
largest = values[i];
else
largest = values[i + 1];
}
}
return largest;
}
public static void main(String[] args) {
new Sandbox();
}
}
And also, why does Color.getRed(), return the value for blue and, Color.getBlue() returns the value for returns the value for red? I am using this to find RGB values: http://www.colorschemer.com/online.html
I am trying to use this to find a specified color within an image.
In colour theory, a shade is what you get by mixing a colour with different amounts of black. So you can easily check if two RGB triplets correspond to different shades of the same colour by normalizing their values
max1 = max(r1,g1,b1);
max2 = max(r2,g2,b2);
if ( approxEQ(r1/max1,r2/max2,DELTA) &&
approxEQ(g1/max1,g2/max2,DELTA) &&
approxEQ(b1/max1,b2/max2,DELTA) ) {
/* Same colour, different shades */
}
(where, obviously, max(a,b,c) returns the largest of three parameters, and approxEQ(a,b,d) returns true if |a-b|≤d, or false otherwise.)
If you want to check for tints as well, you would be better off converting your RGB values to HSV or HSL.
Maybe HSL Color will help. Strictly speaking I think the Hue and Saturation would need to be the same values.

Categories