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();
}
}
I implemented different type of sorting (bubble, insertion, selection). Know I want to compare their implementations like the following for each sort (here's an example with the bubble sort) :
For example, here's my bubble sort :
private static int[] bubbleSort(int[] tabToSort) {
int [] tab = tabToSort.clone();
boolean tabSort = false;
while(!tabSort){
tabSort = true;
for(int i = 0; i < tab.length -1; i++){
if(tab[i]> tab[i+1]){
int temp = tab[i+1];
tab[i+1] = tab[i];
tab[i] = temp;
tabSort = false;
}
}
}
return tab;
}
I started the GUI and I placed 1000 random points on it and the line y=x :
#Override
public void paintComponent (Graphics g){
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.BLACK);
Dimension size = getSize();
Insets insets= getInsets();
int w = size.width - insets.left - insets.right;
int h = size.height - insets.top - insets.bottom;
g2d.drawLine(size.width ,0, 0, size.height);
Random r = new Random();
for (int i =0; i < 1000; i++) {
int x = Math.abs(r.nextInt()) % w;
int y = Math.abs(r.nextInt()) % h;
Point p = new Point(x, y);
g2d.drawLine(p.x, p.y, p.x, p.y);
}
}
Here's what I've done :
Now I'm stuck, I have no idea about how to start. Could anyone indicate me the steps/ hints to follow to implement that ?
Thanks :)
You must define what the points mean. Looking at the animation, it looks like the y axis represents a value, whilst the x axis represents the position in the array of that value.
In your paint method, you would then go through the list of items and paint a dot, with the x-point being the position in the array and the y-point being a position on the y-axis. Assuming the values are within a known range.
Also, remember that the y-axis in graphics starts with 0 at the top, so you may have to do some translation of values to coordinates (depending on how you want it to look).
The easiest way would be to convert your paint method into one that uses a predefined List of points as a parameter instead of random points. On each iteration of your sort method pass the sorted array into the paint method and repaint the dots.
You'll need to
Create an int[]Â array with random values as a member variable. Let's call the array data. You probably want to start with a fixed array size and range of 100 each. You can adjust the values to the window size later, when a simple version is working. It may be even better to stick to a fixed size and range and just scale to the space available in paintComponent, making the behavior independent of the window size.
Change paintComponent to loop over data. The loop index is your x value and data[x] determines the y value.
Test that the code still draws the initial random array. Don't care if it is in the uppler left corner only now, you can fix that when the animation is working.
You'll need to add some kind of sleep() call to the innermost loop of your sort method, so you get a chance to observe the steps. Otherwise, even bubblesort will be too fast to observe. I'd recommend to start with one second (parameter value 1000). Make it faster later when everything works.
Start the bubbleSort method in a new thread and make sure your component gets repainted with each step. This may be the most tricky part. Perhaps hand in the component to the bublleSort method (or make bubbleSort a non-static method of the component) and let it request a repaint() at each step (fortunately, this is one of the few thread safe methods in Swing).
Fine-tune your code: Scale the x and y coordinates by multiplying with the space available and then dividing by the array size or value range. Adjust the sleep time as needed. Add support for different sorting algorithms....
If any of the steps is unclear, add a comment.
I've done this for my bachelorthesis, I did it like this (it's not perfect, but it might help you):
(I removed some unimportant methods/functions from the code below. It's mainly to illustrate how I visualized it. You can replace the GRectangle class by a simple java.awt.Point for example.)
The initialization method gives you an example of how you can find the maximum and minimum value of the data so you know how to transform your datavalues => coordinates.
public class DotVisualisation extends Visualisation {
private ArrayList<GRectangle> m_points;
private Comparable[] m_data;
private Comparable m_maxValue;
private Comparable m_minValue;
private int MAX_HEIGHT; // max height in pixels of visualization
/**
* Creates a new DotVisualisation.<br>
* <br>
* This class is a runnable JComponent that will visualize data as a function.
* The visualisation will plot the data with X and Y coordinates on the window.
* The X coordinate of the point is index of the dataelement.
* The Y coordinate of the point is relative to the value of the dataelement.<br>
* <br>
* This visualisation should be used for medium and large arrays.
*
* #author David Nysten
*/
public DotVisualisation()
{
m_points = new ArrayList<GRectangle>();
MAX_HEIGHT = 150;
}
/**
* Returns the maximum supported dimension by this visualisation.
*
* #return The supported dimension.
*/
public static int getSupportedDimension()
{
return 1;
}
#Override
public Dimension getMaximumSize()
{
return getPreferredSize();
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(m_points.size() + 2, MAX_HEIGHT + 6);
}
#Override
public Dimension getMinimumSize()
{
return getPreferredSize();
}
#Override
public void paintComponent(Graphics g)
{
for(int i = 0; i < m_points.size(); ++i)
m_points.get(i).paintComponent(g);
}
private void swap(int index, int index2) { // See below }
private void initialise()
{
findMinimum();
findMaximum();
m_points.clear();
double multiplier;
int x = 0, y = 0, h;
for(int i = 0; i < m_data.length; ++i)
{
if(m_data[i].compareTo(-1) <= 0)
h = 0;
else
{
Integer value = (Integer) m_data[i];
Integer min = (Integer) m_minValue;
Integer diff = (Integer) m_maxValue - min;
multiplier = MAX_HEIGHT / diff.doubleValue();
h = (int) ((value - min) * multiplier);
}
y = (int) (MAX_HEIGHT - h);
GRectangle r = new GRectangle(x, y, 1, 1); // 1, 1 = width and height
r.setColor(Color.BLACK);
m_points.add(r);
++x;
}
}
private void findMaximum()
{
Comparable max = null;
if(m_data.length > 0)
{
max = m_data[0];
for(int i = 1; i < m_data.length; ++i)
if(m_data[i].compareTo(max) > 0)
max = m_data[i];
}
m_maxValue = max;
}
private void findMinimum()
{
Comparable min = null;
if(m_data.length > 0)
{
min = m_data[0];
for(int i = 1; i < m_data.length; ++i)
if(m_data[i].compareTo(min) < 0)
min = m_data[i];
}
m_minValue = min;
}
}
Take this into account:
Visualizing integers between 0 and 150 on a height of 150 pixels is straightforward. Visualizing a set of integers between the values 565 and 3544545 on a height of 150 is a bit less so.
PS: The code uses the index of the element in the inputarray as the X-coordinate.
PS: The class keeps a reference to the inputarray (m_data variable) but that's ofcourse not needed, you only need it to initialize your points.
PS: My "Visualization" class which is extended by all visualizations, is basicly a JPanel.
PS: The code above is written for positive integers, so will probably need some extra coding to handle negative integers aswell ;).
Then to visualize the actions of the algorithm, I used the observer pattern. The algorithm, for example bubblesort, looked like this:
for(int i = 0; i < size(); ++i)
for(int j = 1; j < size(); ++j)
if(greaterThan(j - 1, j))
swap(j - 1, j);
Where the function swap was defined as follows (simplified version again):
protected void swap(int index1, int index2)
{
if(index1 != index2)
{
incrementSwap(); // counting swaps and visualizing counter
m_command.clear();
m_command.setAction(Action.SWAP);
m_command.addParameter(index1);
m_command.addParameter(index2);
setChanged();
notifyObservers(m_command);
E temp = m_data[index1];
m_data[index1] = m_data[index2];
m_data[index2] = temp;
}
}
Where I notified my observers (visualizations) that a swap occured on index1 and index2. The m_command variable is an instance of the Command-class (wrote it myself) which is just a wrapper for the information needed by the visualization. Which is: the action that occured and the relevant information (indices for a swap-action for example).
So in the visualization i swapped the GRectangles on those indices aswell as their X-coordinates;
private void swap(int index, int index2)
{
if(index == index2)
return;
GRectangle r1 = m_points.get(index);
GRectangle r2 = m_points.get(index2);
int tempX = r1.getX();
r1.setLocation(r2.getX(), r1.getY());
r2.setLocation(tempX, r2.getY());
m_points.set(index, r2);
m_points.set(index2, r1);
}
You can add lines like this:
try {
Thread.sleep(100);
} catch(InterruptedException ignore) {}
to let a thread sleep 100ms before continueing. This might come in handy if it's getting visualized too fast.
So for an array with random integers it might look like this:
And after sorting:
(Ofcourse it's not a straight line because the values in the inputarray were generated at random in this case)
So if you have to - like I had to - allow multiple algorithms to work with the same visualization, I can recommend you to separate the visualization class and the algorithm class and work with an observer pattern to let the visualization update whenever an action occurs (set, swap, ...).
And then you can create something like this for comparisons;
http://i445.photobucket.com/albums/qq179/ultddave/DotVisualizationMany_zps63269d2a.png
http://i445.photobucket.com/albums/qq179/ultddave/DotVisualizationMany2_zps65e96fa9.png
Good luck!
I've been working on this program for a couple weeks now, adding components to an applet in order to output a simple tic tac toe game. I've gotten some of it to work, but as of now, when you run, all it does is that you click it once, the CPU projects its mark in the upper left corner, and then you can click anywhere else and it automatically says you win. I can't get the CPU to keep playing. I don't expect anyone to tell me what to do, but I'm just confused as to which method I need to work on in order to get the CPU to respond. My professor has left some very helpful pseudocode, but I still don't quite understand. I've been working with the method "gameEnd," checking for winners horizontally, vertically and diagonally to see if that's the source to getting the game to continue beyond just two marks, but it's not working. Anyone got any suggestions? Thanks.
import java.awt.Color;
import java.awt.Event;
import java.awt.Font;
import java.awt.Graphics;
import java.util.Random;
public class TicTacToe extends java.applet.Applet {
// Ignore this constant
private static final long serialVersionUID = 1942709821640345256L;
// You can change this boolean constant to control debugging log output
private static final boolean DEBUGGING = false;
// Constants
// Size of one side of the board in pixels
private static final int BOARD_SIZE_PIXELS = 600;
// Number of squares on one side of the board
private static final int SQUARES = 3;
// Diameter of the circle drawn in each square
private static final int CIRCLE_WIDTH = 90;
// Colors to be used in the game
private static final Color BACKGROUND_COLOR = Color.WHITE;
private static final Color SQUARE_BORDER_COLOR = Color.BLACK;
private static final Color GAME_OVER_MESSAGE_COLOR = Color.BLACK;
private static final Color HUMAN_COLOR = Color.RED;
private static final Color HUMAN_WINNING_COLOR = Color.MAGENTA;
private static final Color CPU_COLOR = Color.BLUE;
private static final Color CPU_WINNING_COLOR = Color.CYAN;
// Status constant values for the game board
private static final int EMPTY = 0;
private static final int HUMAN = 1;
private static final int HUMAN_WINNING = 2;
private static final int CPU = -1;
private static final int CPU_WINNING = -2;
// String displayed when the game ends
private static final String GAME_WIN_MESSAGE = "You win! Click to play again.";
private static final String GAME_LOSE_MESSAGE = "You lose......Click to play again.";
private static final String GAME_DRAW_MESSAGE = "No one wins? Click to play again...";
// Instance variables that control the game
// Whether or not the user just clicked the mouse
private boolean mouseClicked = false;
// Whether or not to start the game again
private boolean restart = false;
// Whether or not the CPU should start playing a move.
// USED ONLY WHEN THE CPU PLAYS FIRST!
private boolean onFirstMove = false;
// The column and row of the SQUARE the user clicked on
private int xMouseSquare; // column
private int yMouseSquare; // row
// The width (and height) of a single game square
private int squareWidth = BOARD_SIZE_PIXELS / SQUARES;
// An array to hold square status values on the board.
// The status values can be EMPTY, HUMAN, CPU or other values
private int[][] gameBoard;
// The column and row of the SQUARE the CPU player will move on
private int xCPUMove;
private int yCPUMove;
// Add the rest of your instance variables here, if you need any. (You won't
// need to, but you may if you want to.)
// Ignore these instance variables
// CPUinMove represents if the CPU is thinking (generating the CPU move).
// If it is true, it means the CPUMove() method is running and no new move
// should be added
private boolean CPUinMove;
// Methods that you need to write:
/*
* Pre: x and y are x-coordinate and y-coordinate where the user just
* clicks. squareWidth is the width (and height) of a single game square.
*
* Post: xMouseSquare and yMouseSquare are set to be the column and row
* where the user just clicked on (depending on x and y).
*
* Hint: You need only two statements in this method.
*/
// Setting MouseSquare equal to x and y divided by the Square Width to create a location for
// the user after clicking
private void setMouseSquare(int x, int y) {
//
xMouseSquare = x/squareWidth;
yMouseSquare = y/squareWidth;
}
/*
* Pre: SQUARES is an int that holds the number of game squares along one
* side of the game board. xSquare is an int such that 0 <= xSquare <
* SQUARES. CIRCLE_WIDTH is an int that holds the diameter of the circle to
* be drawn in the center of a square. squareWidth is an int that holds the
* width and height in pixels of a single game square.
*
* Post: Return the correct x-coordinate (in pixels) of the left side of the
* circle centered in a square in the column xSquare.
*
* Hint: This method should be very simple. What you need to do is to find
* the right equation.
*/
private int getIconDisplayXLocation(int xSquare) {
// This line is an example of using DEBUGGING variable
if (DEBUGGING) {
System.out.println("The input that getIconDisplayXLocation() receives is: " + xSquare);
}
// equation that returns the correct variable in the column xSquare
return squareWidth * xSquare + (squareWidth - CIRCLE_WIDTH)/2;
}
/*
* Pre: SQUARES is an int that holds the number of game squares along one
* side of the game board. ySquare is an int such that 0 <= ySquare <
* SQUARES. CIRCLE_WIDTH is an int that holds the diameter of the circle to
* be drawn in the center of a square. squareWidth is an int that holds the
* width and height in pixels of a single game square.
*
* Post: Return the correct y-coordinate (in pixels) of the top of the
* circle centered in a square in the row ySquare.
*
* Hint: This method should be very simple. What you need to do is to find
* the right equation.
*/
private int getIconDisplayYLocation(int ySquare) {
// This line is an example of using DEBUGGING variable
if (DEBUGGING) {
System.out.println("The input that getIconDisplayYLocation() receives is: " + ySquare);
}
// equation that returns the correct variable in the column ySquare
return squareWidth * ySquare + (squareWidth - CIRCLE_WIDTH)/2;
}
/*
* The instance variable gameBoard will be created and initialized
*
* Pre: SQUARES is set to an int. gameBoard is a 2-dimensional array type
* variable that holds the status of current game board. Each value in the
* array represents a square on the board
*
* Post: gameBoard must be assigned a new 2-dimensional array. Every square
* of gameBoard should be initialized to EMPTY.
*
* Hint: A loop.
*/
private void buildBoard() {
// Setting the two methods equal to local variables, x and y
int x = xMouseSquare;
int y = yMouseSquare;
// This line creates the gameBoard array. You must write several more
// lines to initialize all its values to EMPTY
// Write game board using the equation of three across and down for each column
// Constructs a 3 by 3 array of integers for the board
gameBoard = new int[3][3];
// Initialize variables i and j, set equal to 0; this establishes a connection for loop
for (x = 0; x < 3; x++) {
for (y = 0; y < 3; y++) {
gameBoard[x][y] = EMPTY;
}
}
}
/*
* Returns whether the most recently clicked square is a legal choice in the
* game.
*
* Pre: gameBoard is a 2-dimensional array type variable that holds the
* status of current game board. xSquare and ySquare represent the column
* and row of the most recently clicked square.
*
* Post: Returns true if and only if the square is a legal choice. If the
* square is empty on current game board, it is legal and the method shall
* return true; if it is taken by either human or CPU or it is not a square
* on current board, it is illegal and the method shall return false.
*
* Hint: Should be simple but think carefully to cover all cases.
*/
private boolean legalSquare(int xSquare, int ySquare) {
if (gameBoard[xSquare][ySquare] == EMPTY)
return true;
else return false;
}
/*
* Pre: gameBoard is an array that holds the current status of the game
* board. xSquare and ySquare represent the column and row of the most
* recently clicked square. player represent the current player (HUMAN or
* CPU). This method is always called after checking legalSquare().
*
* Post: Set the square as taken by current player on the game board if the
* square is empty.
*
* Hint: Very simple.
*/
private void setMovePosition(int xSquare, int ySquare, int player) {
player = HUMAN;
player = CPU;
this.xMouseSquare = xSquare;
this.yMouseSquare = ySquare;
}
/*
* Check if HUMAN or CPU wins the game.
*
* Pre: gameBoard is an array to hold square status values on the board. The
* status values can be EMPTY, HUMAN, CPU or other values.
*
* Post: The method will return true if and only if a player wins the game.
* Winning a game means there is at least one row, one column, or one
* diagonal that is taken by the same player. The method does not need to
* indicate who wins because if it is implemented correctly, the winner must
* be the one who just made a move.
*
* Hint: Complicated method. Use loops to shorten your code. Think about how
* to represent "a player takes 3 squares in a row/column/diagonal".
*/
private boolean gameEnd() {
// Setting local variables
int i = 0;
int x = xMouseSquare;
int y = yMouseSquare;
int sw = squareWidth;
int[][] g = gameBoard;
// Checking for a winner
// Checking columns
for (y = 0; y < 3; y++) {
for (x = 0; x < 3; x++) {
i = g[x][y] + i;
}
}
{
// Checking rows
for (x = 0; x < 3; x++) {
for (y = 0; y < 3; y++) {
i = g[x][y] + i;
}
}
}
// Checking first diagonal
{
for (x = 0; x < 3; x++) {
g[x][x] = 3;
}
}
for (x = 1; x < 3; x++) {
g[x][x] = 3;
}
for (x = 2; x < 3; x++) {
g[x][x] = 3;
}
// Checking second diagonal
for (y = 0; y < 3; y++) {
g[y][2 - y] = 3;
}
for (y = 1; y < 3; y++) {
g[y][2 - y] = 3;
}
for (y = 2; y < 3; y++) {
g[y][2 - y] = 3;
}
return true;
}
/*
* Check if the game ends as a draw.
*
* Pre: gameBoard is an array to hold square status values on the board. The
* status values can be EMPTY, HUMAN, CPU or other values. This method is
* always called after gameEnd().
*
* Post: The method will return true if and only if all squares on the game
* board are taken by HUMAN or CPU players (no EMPTY squares left).
*
* Hint: Should be simple. Use loops.
*/
// Value of...
private boolean gameDraw() {
if (squareWidth == (3^2 - 1)) {
}
return true;
}
/*
* Marks circles on the line on which a player wins.
*
* Pre: g is Graphics object that is ready to draw on. HUMAN_WINNING_COLOR
* and CPU_WINNING_COLOR are both Color objects that represent the color to
* show when HUMAN/CPU wins. gameBoard is gameBoard is an array to hold
* square status values on the board. The status values can be EMPTY, HUMAN,
* CPU or other values.
*
* Post: ALL the row(s)/column(s)/diagonal(s) on which a player wins will be
* marked as the special color.
*
* Hint: You must draw a new circle with the special color (to replace the
* old circle) on the square if the square belongs to a winning
* row/column/diagonal. You can change gameBoard because the board will be
* reset after displaying the winning line. You can use helper methods
* (existing ones or your own) to finish this method. Pay attention that
* many functions in this method is similar to gameEnd(). You should think
* about reusing code.
*
* Hint2: This method is not necessary for the game logic. You don't have to
* start it early. Start when you know everything else is correct.
*/
private void markWinningLine(Graphics g) {
// TODO
}
/*
* Generates the next square where the CPU plays a move.
*
* Pre: gameBoard is an array to hold square status values on
* the board. The status values can be EMPTY, HUMAN, CPU or other values.
*
* Post: Set xCPUMove and yCPUMove to represent the column and the row which
* CPU plans to play a move on. The respective square MUST BE EMPTY! It will
* cause a logical error if this method returns a square that is already
* taken!
*
* Hint: Don't start too early -- currently this method works (though
* naively). Make sure that everything else works before touching this
* method!
*/
private void CPUMove() {
// TODO
// The following block gives a naive solution -- it finds the first
// empty square and play a move on it. You can use this method to test
// other methods in the beginning. However, you must replace this block
// with your own algorithms eventually.
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (gameBoard[i][j] == 0) {
xCPUMove = i;
yCPUMove = j;
return;
}
}
}
}
/* Put any helper methods you wish to add here. */
/* You will not need to change anything below this line. */
/*
* DO NOT change this method.
*
* Set the game board to show a new, blank game.
*/
private void wipeBoard(Graphics g) {
g.setColor(BACKGROUND_COLOR);
g.fillRect(0, 0, BOARD_SIZE_PIXELS, BOARD_SIZE_PIXELS);
}
/*
* DO NOT change this method.
*
* Displays a circle on g, of the given color, in the center of the given
* square.
*/
private void displayHit(Graphics g, Color color, int xSquare, int ySquare) {
g.setColor(color);
g.fillOval(getIconDisplayXLocation(xSquare),
getIconDisplayYLocation(ySquare), CIRCLE_WIDTH, CIRCLE_WIDTH);
}
/*
* DO NOT change this method.
*
* This method handles mouse clicks. You will not need to call it.
*/
#Override
public boolean mouseDown(Event e, int xMouse, int yMouse) {
if (isClickable()) {
mouseClicked = true;
setMouseSquare(xMouse, yMouse);
}
repaint();
return true;
}
/*
* DO NOT change this method.
*
* This method handles drawing the board. You will not need to call it.
*/
#Override
public void update(Graphics g) {
paint(g);
}
/*
* DO NOT change this method.
*
* Draws the border between game squares onto canvas. Also, draws the moves
* that are already made.
*/
private void displayGame(Graphics canvas) {
canvas.setColor(SQUARE_BORDER_COLOR);
for (int i = 0; i < BOARD_SIZE_PIXELS; i += squareWidth) {
for (int j = 0; j < BOARD_SIZE_PIXELS; j += squareWidth) {
canvas.drawRect(i, j, squareWidth, squareWidth);
}
}
for (int i = 0; i < SQUARES; i++) {
for (int j = 0; j < SQUARES; j++) {
switch (gameBoard[i][j]) {
case HUMAN:
case HUMAN_WINNING:
displayHit(canvas, HUMAN_COLOR, i, j);
break;
case CPU:
case CPU_WINNING:
displayHit(canvas, CPU_COLOR, i, j);
break;
default:
break;
}
}
}
}
/*
* DO NOT change this method.
*
* This method relays information about the availability of mouse clicking
* in the game. You will not need to call it.
*/
private boolean isClickable() {
return !CPUinMove;
}
/*
* DO NOT change the contents this method.
*
* If this method is changed to public void paint(Graphics canvas), it will
* execute the program with the CPU-first order.
*
* This method is like the "main" method (but for applets). You will not
* need to call it. It contains most of the game logic.
*/
// #Override
public void paint(Graphics canvas) {
displayGame(canvas);
if (mouseClicked) {
if (onFirstMove) {
CPUMove();
setMovePosition(xCPUMove, yCPUMove, CPU);
displayHit(canvas, CPU_COLOR, xCPUMove, yCPUMove);
onFirstMove = false;
} else {
if (restart) {
wipeBoard(canvas);
setUpGame();
repaint();
} else if (legalSquare(xMouseSquare, yMouseSquare)) {
setMovePosition(xMouseSquare, yMouseSquare, HUMAN);
displayHit(canvas, HUMAN_COLOR, xMouseSquare, yMouseSquare);
if (gameEnd()) {
markWinningLine(canvas);
canvas.setFont(new Font("SansSerif", Font.PLAIN, 30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_WIN_MESSAGE, squareWidth / 2,
squareWidth);
restart = true;
} else {
CPUinMove = true;
CPUMove();
setMovePosition(xCPUMove, yCPUMove, CPU);
displayHit(canvas, CPU_COLOR, xCPUMove, yCPUMove);
CPUinMove = false;
if (gameEnd()) {
markWinningLine(canvas);
canvas
.setFont(new Font("SansSerif", Font.PLAIN,
30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_LOSE_MESSAGE,
squareWidth / 2, squareWidth);
restart = true;
} else if (gameDraw()) {
canvas
.setFont(new Font("SansSerif", Font.PLAIN,
30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_DRAW_MESSAGE,
squareWidth / 2, squareWidth);
restart = true;
}
}
}
}
mouseClicked = false;
}
}
/*
* DO NOT change this method.
*
* This method is like the "main" method (but for applets). You will not
* need to call it. It contains most of the game logic.
*/
public void paint_game(Graphics canvas) {
// display the current game board
displayGame(canvas);
// the following block will run every time the user clicks the mouse
if (mouseClicked) { // when the user clicks the mouse
// if the game is ready to start or to be restarted
if (restart) {
// clear the window and set up the game again
wipeBoard(canvas);
setUpGame();
repaint();
}
// else, if the game is in play, check if the click is on a legal
// square
else if (legalSquare(xMouseSquare, yMouseSquare)) {
// if the square is legal, mark the corresponding position as
// taken by HUMAN
setMovePosition(xMouseSquare, yMouseSquare, HUMAN);
// display the new position with a HUMAN_COLOR circle
displayHit(canvas, HUMAN_COLOR, xMouseSquare, yMouseSquare);
// check if the game ends (if it is, HUMAN wins)
if (gameEnd()) {
// if HUMAN wins, mark the winning line as a special color.
markWinningLine(canvas);
// display the human winning message
canvas.setFont(new Font("SansSerif", Font.PLAIN, 30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_WIN_MESSAGE, squareWidth / 2,
squareWidth);
// mark the game as ready to restart
restart = true;
} else if (gameDraw()) {
// if HUMAN doesn't win but the board is full, it is a draw
// display the draw message
canvas.setFont(new Font("SansSerif", Font.PLAIN, 30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_DRAW_MESSAGE, squareWidth / 2,
squareWidth);
// mark the game as ready to restart
restart = true;
} else {
// if HUMAN doesn't win and the board is not full, the CPU
// is ready to move
CPUinMove = true;
// calculates the next CPU move
CPUMove();
// mark the corresponding position as taken by CPU
setMovePosition(xCPUMove, yCPUMove, CPU);
// display the new position with a CPU_COLOR circle
displayHit(canvas, CPU_COLOR, xCPUMove, yCPUMove);
CPUinMove = false;
if (gameEnd()) {
// if CPU wins, mark the winning line as a special
// color.
markWinningLine(canvas);
// display the human losing message
canvas.setFont(new Font("SansSerif", Font.PLAIN, 30));
canvas.setColor(GAME_OVER_MESSAGE_COLOR);
canvas.drawString(GAME_LOSE_MESSAGE, squareWidth / 2,
squareWidth);
// mark the game as ready to restart
restart = true;
}
// else (if the game is not ended after the CPU move), the
// game is ready to get the next HUMAN move
}
}
mouseClicked = false;
}
}
/*
* DO NOT change this method.
*
* This method initializes the applet. You will not need to call it.
*/
#Override
public void init() {
setSize(BOARD_SIZE_PIXELS, BOARD_SIZE_PIXELS);
setBackground(BACKGROUND_COLOR);
setUpGame();
}
/*
* DO NOT change this method.
*
* Creates a fresh game board and sets up the game state to get ready for a
* new game.
*/
private void setUpGame() {
buildBoard();
CPUinMove = false;
restart = false;
onFirstMove = true;
}
}