Data update
This commit is contained in:
parent
4bb20c9b71
commit
cbaf4c4b64
12390 changed files with 318560 additions and 27248 deletions
|
|
@ -1,79 +1,79 @@
|
|||
typedef struct oct_node_t oct_node_t, *oct_node;
|
||||
struct oct_node_t{
|
||||
/* sum of all colors represented by this node. 64 bit in case of HUGE image */
|
||||
uint64_t r, g, b;
|
||||
int count, heap_idx;
|
||||
oct_node kids[8], parent;
|
||||
unsigned char n_kids, kid_idx, flags, depth;
|
||||
/* sum of all colors represented by this node. 64 bit in case of HUGE image */
|
||||
uint64_t r, g, b;
|
||||
int count, heap_idx;
|
||||
oct_node kids[8], parent;
|
||||
unsigned char n_kids, kid_idx, flags, depth;
|
||||
};
|
||||
|
||||
/* cmp function that decides the ordering in the heap. This is how we determine
|
||||
which octree node to fold next, the heart of the algorithm. */
|
||||
inline int cmp_node(oct_node a, oct_node b)
|
||||
{
|
||||
if (a->n_kids < b->n_kids) return -1;
|
||||
if (a->n_kids > b->n_kids) return 1;
|
||||
if (a->n_kids < b->n_kids) return -1;
|
||||
if (a->n_kids > b->n_kids) return 1;
|
||||
|
||||
int ac = a->count * (1 + a->kid_idx) >> a->depth;
|
||||
int bc = b->count * (1 + b->kid_idx) >> b->depth;
|
||||
return ac < bc ? -1 : ac > bc;
|
||||
int ac = a->count * (1 + a->kid_idx) >> a->depth;
|
||||
int bc = b->count * (1 + b->kid_idx) >> b->depth;
|
||||
return ac < bc ? -1 : ac > bc;
|
||||
}
|
||||
|
||||
/* adding a color triple to octree */
|
||||
oct_node node_insert(oct_node root, unsigned char *pix)
|
||||
{
|
||||
# define OCT_DEPTH 8
|
||||
/* 8: number of significant bits used for tree. It's probably good enough
|
||||
for most images to use a value of 5. This affects how many nodes eventually
|
||||
end up in the tree and heap, thus smaller values helps with both speed
|
||||
and memory. */
|
||||
# define OCT_DEPTH 8
|
||||
/* 8: number of significant bits used for tree. It's probably good enough
|
||||
for most images to use a value of 5. This affects how many nodes eventually
|
||||
end up in the tree and heap, thus smaller values helps with both speed
|
||||
and memory. */
|
||||
|
||||
unsigned char i, bit, depth = 0;
|
||||
for (bit = 1 << 7; ++depth < OCT_DEPTH; bit >>= 1) {
|
||||
i = !!(pix[1] & bit) * 4 + !!(pix[0] & bit) * 2 + !!(pix[2] & bit);
|
||||
if (!root->kids[i])
|
||||
root->kids[i] = node_new(i, depth, root);
|
||||
unsigned char i, bit, depth = 0;
|
||||
for (bit = 1 << 7; ++depth < OCT_DEPTH; bit >>= 1) {
|
||||
i = !!(pix[1] & bit) * 4 + !!(pix[0] & bit) * 2 + !!(pix[2] & bit);
|
||||
if (!root->kids[i])
|
||||
root->kids[i] = node_new(i, depth, root);
|
||||
|
||||
root = root->kids[i];
|
||||
}
|
||||
root = root->kids[i];
|
||||
}
|
||||
|
||||
root->r += pix[0];
|
||||
root->g += pix[1];
|
||||
root->b += pix[2];
|
||||
root->count++;
|
||||
return root;
|
||||
root->r += pix[0];
|
||||
root->g += pix[1];
|
||||
root->b += pix[2];
|
||||
root->count++;
|
||||
return root;
|
||||
}
|
||||
|
||||
/* remove a node in octree and add its count and colors to parent node. */
|
||||
oct_node node_fold(oct_node p)
|
||||
{
|
||||
if (p->n_kids) abort();
|
||||
oct_node q = p->parent;
|
||||
q->count += p->count;
|
||||
if (p->n_kids) abort();
|
||||
oct_node q = p->parent;
|
||||
q->count += p->count;
|
||||
|
||||
q->r += p->r;
|
||||
q->g += p->g;
|
||||
q->b += p->b;
|
||||
q->n_kids --;
|
||||
q->kids[p->kid_idx] = 0;
|
||||
return q;
|
||||
q->r += p->r;
|
||||
q->g += p->g;
|
||||
q->b += p->b;
|
||||
q->n_kids --;
|
||||
q->kids[p->kid_idx] = 0;
|
||||
return q;
|
||||
}
|
||||
|
||||
/* traverse the octree just like construction, but this time we replace the pixel
|
||||
color with color stored in the tree node */
|
||||
void color_replace(oct_node root, unsigned char *pix)
|
||||
{
|
||||
unsigned char i, bit;
|
||||
unsigned char i, bit;
|
||||
|
||||
for (bit = 1 << 7; bit; bit >>= 1) {
|
||||
i = !!(pix[1] & bit) * 4 + !!(pix[0] & bit) * 2 + !!(pix[2] & bit);
|
||||
if (!root->kids[i]) break;
|
||||
root = root->kids[i];
|
||||
}
|
||||
for (bit = 1 << 7; bit; bit >>= 1) {
|
||||
i = !!(pix[1] & bit) * 4 + !!(pix[0] & bit) * 2 + !!(pix[2] & bit);
|
||||
if (!root->kids[i]) break;
|
||||
root = root->kids[i];
|
||||
}
|
||||
|
||||
pix[0] = root->r;
|
||||
pix[1] = root->g;
|
||||
pix[2] = root->b;
|
||||
pix[0] = root->r;
|
||||
pix[1] = root->g;
|
||||
pix[2] = root->b;
|
||||
}
|
||||
|
||||
/* Building an octree and keep leaf nodes in a bin heap. Afterwards remove first node
|
||||
|
|
@ -81,31 +81,31 @@ void color_replace(oct_node root, unsigned char *pix)
|
|||
contains required number of colors. */
|
||||
void color_quant(image im, int n_colors)
|
||||
{
|
||||
int i;
|
||||
unsigned char *pix = im->pix;
|
||||
node_heap heap = { 0, 0, 0 };
|
||||
int i;
|
||||
unsigned char *pix = im->pix;
|
||||
node_heap heap = { 0, 0, 0 };
|
||||
|
||||
oct_node root = node_new(0, 0, 0), got;
|
||||
for (i = 0; i < im->w * im->h; i++, pix += 3)
|
||||
heap_add(&heap, node_insert(root, pix));
|
||||
oct_node root = node_new(0, 0, 0), got;
|
||||
for (i = 0; i < im->w * im->h; i++, pix += 3)
|
||||
heap_add(&heap, node_insert(root, pix));
|
||||
|
||||
while (heap.n > n_colors + 1)
|
||||
heap_add(&heap, node_fold(pop_heap(&heap)));
|
||||
while (heap.n > n_colors + 1)
|
||||
heap_add(&heap, node_fold(pop_heap(&heap)));
|
||||
|
||||
double c;
|
||||
for (i = 1; i < heap.n; i++) {
|
||||
got = heap.buf[i];
|
||||
c = got->count;
|
||||
got->r = got->r / c + .5;
|
||||
got->g = got->g / c + .5;
|
||||
got->b = got->b / c + .5;
|
||||
printf("%2d | %3llu %3llu %3llu (%d pixels)\n",
|
||||
i, got->r, got->g, got->b, got->count);
|
||||
}
|
||||
double c;
|
||||
for (i = 1; i < heap.n; i++) {
|
||||
got = heap.buf[i];
|
||||
c = got->count;
|
||||
got->r = got->r / c + .5;
|
||||
got->g = got->g / c + .5;
|
||||
got->b = got->b / c + .5;
|
||||
printf("%2d | %3llu %3llu %3llu (%d pixels)\n",
|
||||
i, got->r, got->g, got->b, got->count);
|
||||
}
|
||||
|
||||
for (i = 0, pix = im->pix; i < im->w * im->h; i++, pix += 3)
|
||||
color_replace(root, pix);
|
||||
for (i = 0, pix = im->pix; i < im->w * im->h; i++, pix += 3)
|
||||
color_replace(root, pix);
|
||||
|
||||
node_free();
|
||||
free(heap.buf);
|
||||
node_free();
|
||||
free(heap.buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,74 +11,74 @@ import javax.imageio.ImageIO;
|
|||
|
||||
public final class ColorQuantization {
|
||||
|
||||
public static void main(String[] aArgs) throws IOException {
|
||||
BufferedImage original = ImageIO.read( new File("quantum_frog.png") );
|
||||
final int width = original.getWidth();
|
||||
final int height = original.getHeight();
|
||||
int[] originalPixels = original.getRGB(0, 0, width, height, null, 0, width);
|
||||
|
||||
List<Item> bucket = new ArrayList<Item>();
|
||||
for ( int i = 0; i < originalPixels.length; i++ ) {
|
||||
bucket.add( new Item(new Color(originalPixels[i]), i) );
|
||||
}
|
||||
|
||||
int[] resultPixels = new int[originalPixels.length];
|
||||
medianCut(bucket, 4, resultPixels);
|
||||
|
||||
BufferedImage result = new BufferedImage(width, height, original.getType());
|
||||
result.setRGB(0, 0, width, height, resultPixels, 0, width);
|
||||
ImageIO.write(result, "png", new File("Quantum_frog16Java.png"));
|
||||
|
||||
System.out.println("The 16 colors used in Red, Green, Blue format are:");
|
||||
for ( Color color : colorsUsed ) {
|
||||
System.out.println("(" + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static void medianCut(List<Item> aBucket, int aDepth, int[] aResultPixels) {
|
||||
if ( aDepth == 0 ) {
|
||||
quantize(aBucket, aResultPixels);
|
||||
return;
|
||||
}
|
||||
|
||||
int[] minimumValue = new int[] { 256, 256, 256 };
|
||||
int[] maximumValue = new int[] { 0, 0, 0 };
|
||||
for ( Item item : aBucket ) {
|
||||
for ( Channel channel : Channel.values() ) {
|
||||
int value = item.getPrimary(channel);
|
||||
if ( value < minimumValue[channel.index] ) {
|
||||
minimumValue[channel.index] = value;
|
||||
}
|
||||
if ( value > maximumValue[channel.index] ) {
|
||||
maximumValue[channel.index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[] valueRange = new int[] { maximumValue[Channel.RED.index] - minimumValue[Channel.RED.index],
|
||||
maximumValue[Channel.GREEN.index] - minimumValue[Channel.GREEN.index],
|
||||
maximumValue[Channel.BLUE.index] - minimumValue[Channel.BLUE.index] };
|
||||
|
||||
Channel selectedChannel = ( valueRange[Channel.RED.index] >= valueRange[Channel.GREEN.index] )
|
||||
? ( valueRange[Channel.RED.index] >= valueRange[Channel.BLUE.index] ) ? Channel.RED : Channel.BLUE
|
||||
: ( valueRange[Channel.GREEN.index] >= valueRange[Channel.BLUE.index] ) ? Channel.GREEN : Channel.BLUE;
|
||||
|
||||
Collections.sort(aBucket, switch(selectedChannel) {
|
||||
case RED -> redComparator;
|
||||
case GREEN -> greenComparator;
|
||||
case BLUE -> blueComparator; });
|
||||
|
||||
final int medianIndex = aBucket.size() / 2;
|
||||
medianCut(new ArrayList<Item>(aBucket.subList(0, medianIndex)), aDepth - 1, aResultPixels);
|
||||
medianCut(new ArrayList<Item>(aBucket.subList(medianIndex, aBucket.size())), aDepth - 1, aResultPixels);
|
||||
}
|
||||
|
||||
private static void quantize(List<Item> aBucket, int[] aResultPixels) {
|
||||
int[] means = new int[Channel.values().length];
|
||||
public static void main(String[] aArgs) throws IOException {
|
||||
BufferedImage original = ImageIO.read( new File("quantum_frog.png") );
|
||||
final int width = original.getWidth();
|
||||
final int height = original.getHeight();
|
||||
int[] originalPixels = original.getRGB(0, 0, width, height, null, 0, width);
|
||||
|
||||
List<Item> bucket = new ArrayList<Item>();
|
||||
for ( int i = 0; i < originalPixels.length; i++ ) {
|
||||
bucket.add( new Item(new Color(originalPixels[i]), i) );
|
||||
}
|
||||
|
||||
int[] resultPixels = new int[originalPixels.length];
|
||||
medianCut(bucket, 4, resultPixels);
|
||||
|
||||
BufferedImage result = new BufferedImage(width, height, original.getType());
|
||||
result.setRGB(0, 0, width, height, resultPixels, 0, width);
|
||||
ImageIO.write(result, "png", new File("Quantum_frog16Java.png"));
|
||||
|
||||
System.out.println("The 16 colors used in Red, Green, Blue format are:");
|
||||
for ( Color color : colorsUsed ) {
|
||||
System.out.println("(" + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static void medianCut(List<Item> aBucket, int aDepth, int[] aResultPixels) {
|
||||
if ( aDepth == 0 ) {
|
||||
quantize(aBucket, aResultPixels);
|
||||
return;
|
||||
}
|
||||
|
||||
int[] minimumValue = new int[] { 256, 256, 256 };
|
||||
int[] maximumValue = new int[] { 0, 0, 0 };
|
||||
for ( Item item : aBucket ) {
|
||||
for ( Channel channel : Channel.values() ) {
|
||||
int value = item.getPrimary(channel);
|
||||
if ( value < minimumValue[channel.index] ) {
|
||||
minimumValue[channel.index] = value;
|
||||
}
|
||||
if ( value > maximumValue[channel.index] ) {
|
||||
maximumValue[channel.index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[] valueRange = new int[] { maximumValue[Channel.RED.index] - minimumValue[Channel.RED.index],
|
||||
maximumValue[Channel.GREEN.index] - minimumValue[Channel.GREEN.index],
|
||||
maximumValue[Channel.BLUE.index] - minimumValue[Channel.BLUE.index] };
|
||||
|
||||
Channel selectedChannel = ( valueRange[Channel.RED.index] >= valueRange[Channel.GREEN.index] )
|
||||
? ( valueRange[Channel.RED.index] >= valueRange[Channel.BLUE.index] ) ? Channel.RED : Channel.BLUE
|
||||
: ( valueRange[Channel.GREEN.index] >= valueRange[Channel.BLUE.index] ) ? Channel.GREEN : Channel.BLUE;
|
||||
|
||||
Collections.sort(aBucket, switch(selectedChannel) {
|
||||
case RED -> redComparator;
|
||||
case GREEN -> greenComparator;
|
||||
case BLUE -> blueComparator; });
|
||||
|
||||
final int medianIndex = aBucket.size() / 2;
|
||||
medianCut(new ArrayList<Item>(aBucket.subList(0, medianIndex)), aDepth - 1, aResultPixels);
|
||||
medianCut(new ArrayList<Item>(aBucket.subList(medianIndex, aBucket.size())), aDepth - 1, aResultPixels);
|
||||
}
|
||||
|
||||
private static void quantize(List<Item> aBucket, int[] aResultPixels) {
|
||||
int[] means = new int[Channel.values().length];
|
||||
for ( Item item : aBucket ) {
|
||||
for ( Channel channel : Channel.values() ) {
|
||||
means[channel.index] += item.getPrimary(channel);
|
||||
}
|
||||
for ( Channel channel : Channel.values() ) {
|
||||
means[channel.index] += item.getPrimary(channel);
|
||||
}
|
||||
}
|
||||
|
||||
for ( Channel channel : Channel.values() ) {
|
||||
|
|
@ -89,39 +89,39 @@ public final class ColorQuantization {
|
|||
colorsUsed.add(color);
|
||||
|
||||
for ( Item item : aBucket ) {
|
||||
aResultPixels[item.aIndex] = color.getRGB();
|
||||
aResultPixels[item.aIndex] = color.getRGB();
|
||||
}
|
||||
}
|
||||
|
||||
private enum Channel {
|
||||
RED(0), GREEN(1), BLUE(2);
|
||||
|
||||
private Channel(int aIndex) {
|
||||
index = aIndex;
|
||||
}
|
||||
|
||||
private final int index;
|
||||
}
|
||||
|
||||
private record Item(Color aColor, Integer aIndex) {
|
||||
|
||||
public int getPrimary(Channel aChannel) {
|
||||
return switch(aChannel) {
|
||||
case RED -> aColor.getRed();
|
||||
case GREEN -> aColor.getGreen();
|
||||
case BLUE -> aColor.getBlue();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Comparator<Item> redComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getRed(), two.aColor.getRed());
|
||||
private static Comparator<Item> greenComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getGreen(), two.aColor.getGreen());
|
||||
private static Comparator<Item> blueComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getBlue(), two.aColor.getBlue());
|
||||
|
||||
private static List<Color> colorsUsed = new ArrayList<Color>();
|
||||
|
||||
}
|
||||
|
||||
private enum Channel {
|
||||
RED(0), GREEN(1), BLUE(2);
|
||||
|
||||
private Channel(int aIndex) {
|
||||
index = aIndex;
|
||||
}
|
||||
|
||||
private final int index;
|
||||
}
|
||||
|
||||
private record Item(Color aColor, Integer aIndex) {
|
||||
|
||||
public int getPrimary(Channel aChannel) {
|
||||
return switch(aChannel) {
|
||||
case RED -> aColor.getRed();
|
||||
case GREEN -> aColor.getGreen();
|
||||
case BLUE -> aColor.getBlue();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Comparator<Item> redComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getRed(), two.aColor.getRed());
|
||||
private static Comparator<Item> greenComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getGreen(), two.aColor.getGreen());
|
||||
private static Comparator<Item> blueComparator =
|
||||
(one, two) -> Integer.compare(one.aColor.getBlue(), two.aColor.getBlue());
|
||||
|
||||
private static List<Color> colorsUsed = new ArrayList<Color>();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from PIL import Image
|
||||
|
||||
if __name__=="__main__":
|
||||
im = Image.open("frog.png")
|
||||
im2 = im.quantize(16)
|
||||
im2.show()
|
||||
im = Image.open("frog.png")
|
||||
im2 = im.quantize(16)
|
||||
im2.show()
|
||||
|
|
|
|||
76
Task/Color-quantization/Rebol/color-quantization-1.rebol
Normal file
76
Task/Color-quantization/Rebol/color-quantization-1.rebol
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
Rebol [
|
||||
title: "Rosetta code: Color quantization (Median cut)"
|
||||
file: %Color_quantization-median_cut.r3
|
||||
url: https://rosettacode.org/wiki/Color_quantization
|
||||
]
|
||||
|
||||
median-cut: function [
|
||||
"Reduces image to N colors using the median cut algorithm"
|
||||
img [image! file! url!] "Source image to quantize (modified)"
|
||||
n-colors [integer!] "Number of requested colors"
|
||||
][
|
||||
unless image? img [img: load img]
|
||||
colors: make block! n-colors
|
||||
|
||||
;; Build a flat bucket block [clr1 idx1 clr2 idx2 ...]
|
||||
;; where each clr is a tuple! and idx is the pixel's position in the image
|
||||
bucket: make block! img/size/x * img/size/y
|
||||
idx: 1 foreach clr img [ repend bucket [clr ++ idx] ]
|
||||
;; Pre-sort once — all buckets remain sorted after every split
|
||||
sort/skip bucket 2
|
||||
;; Start with one bucket containing all pixels
|
||||
buckets: reduce [bucket]
|
||||
;; Keep splitting until we have enough buckets for the requested color count
|
||||
while [n-colors > length? buckets] [
|
||||
;; Take the first bucket (always the largest due to append order)
|
||||
bucket: take buckets
|
||||
;; Split at the upper median to ensure the most even division of pairs
|
||||
med: 2 * round/ceiling (length? bucket) / 2 / 2
|
||||
;; Append the upper median and the remainder back as two new buckets
|
||||
append/only buckets take/part bucket med
|
||||
append/only buckets bucket
|
||||
]
|
||||
;; Quantize each bucket
|
||||
foreach bucket buckets [
|
||||
;; Averages all colors in a bucket
|
||||
r-sum: 0 g-sum: 0 b-sum: 0
|
||||
foreach [clr idx] bucket [
|
||||
r-sum: r-sum + clr/1
|
||||
g-sum: g-sum + clr/2
|
||||
b-sum: b-sum + clr/3
|
||||
]
|
||||
n: 0.5 * length? bucket ;; number of colors in the bucket
|
||||
mean-color: to tuple! reduce [
|
||||
r-sum / n
|
||||
g-sum / n
|
||||
b-sum / n
|
||||
]
|
||||
;; Write the mean color back to every pixel in this bucket
|
||||
foreach [clr idx] bucket [ img/:idx: mean-color ]
|
||||
;; Keep the mean color
|
||||
append colors mean-color
|
||||
]
|
||||
;; Return image and colors
|
||||
reduce [img colors]
|
||||
]
|
||||
|
||||
;; Download the original image if does not exists.
|
||||
unless exists? %Quantum_frog.png [
|
||||
write %quantum_frog.png
|
||||
read https://static.wikitide.net/rosettacodewiki/3/3f/Quantum_frog.png
|
||||
]
|
||||
foreach [colors output-name] [
|
||||
16 %Quantum_frog-16-colors.png
|
||||
3 %Quantum_frog-3-colors.png
|
||||
][
|
||||
;; Quantize to N colors
|
||||
time: delta-time [
|
||||
set [img colors] median-cut %Quantum_frog.png colors
|
||||
]
|
||||
print ["Time to compute:" time]
|
||||
;; Display all used colors
|
||||
print ["The" length? colors "colors:"]
|
||||
probe new-line/all colors true
|
||||
save output-name img ;; Save the result
|
||||
browse output-name ;; Display the image in a browser
|
||||
]
|
||||
272
Task/Color-quantization/Rebol/color-quantization-2.rebol
Normal file
272
Task/Color-quantization/Rebol/color-quantization-2.rebol
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
Rebol [
|
||||
title: "Rosetta code: Color quantization (Octree)"
|
||||
file: %Color_quantization-octree.r3
|
||||
url: https://rosettacode.org/wiki/Color_quantization
|
||||
]
|
||||
|
||||
octree-quantize: function/with [
|
||||
img [image! file! url!]
|
||||
n-colors [integer!]
|
||||
][
|
||||
unless image? img [img: load img]
|
||||
quantizer: make-octree-quantizer
|
||||
|
||||
;; Downsample to 25% of pixels for tree building - dramatically faster with
|
||||
;; minimal palette quality loss since distribution is well represented by a sample
|
||||
sm-img: resize/filter img 25% 'box
|
||||
|
||||
;; First pass - count color frequencies
|
||||
freq: make map! 32768
|
||||
foreach color sm-img [
|
||||
freq/:color: either p: freq/:color [p + 1][1]
|
||||
]
|
||||
;; Second pass - only add frequent colors to the octree
|
||||
foreach color sm-img [
|
||||
if freq/:color > 1 [
|
||||
add-color quantizer color
|
||||
]
|
||||
]
|
||||
|
||||
palette: make-palette quantizer n-colors
|
||||
|
||||
;; Map each pixel to its nearest palette color.
|
||||
forall img [
|
||||
idx: get-palette-index quantizer img/1
|
||||
img/1: palette/:idx
|
||||
]
|
||||
|
||||
reduce [img palette]
|
||||
][
|
||||
;; Maximum octree depth - at depth 8 each node represents a unique 24-bit color
|
||||
;; (8 bits per channel, one bit inspected per level)
|
||||
make-octree-node: func [level parent /local node] [
|
||||
node: object [
|
||||
;; Accumulated channel sums and pixel count for average color calculation
|
||||
red: green: blue: pixel-count: 0
|
||||
palette-index: 0 ;; assigned during make-palette
|
||||
children: array 8 ;; up to 8 children, one per RGB bit at this level
|
||||
]
|
||||
;; Leaf-level nodes (level 7) are not registered since they won't be merged
|
||||
if level < 7 [
|
||||
add-level-node parent level node
|
||||
]
|
||||
node
|
||||
]
|
||||
|
||||
; Iteratively collect all leaf nodes (nodes with pixel-count > 0) under `node`
|
||||
get-leaf-nodes: function [node] [
|
||||
leaf-nodes: copy []
|
||||
stack: clear []
|
||||
foreach child node/children [
|
||||
if child [append stack child]
|
||||
]
|
||||
while [not empty? stack] [
|
||||
current: take stack
|
||||
either current/pixel-count > 0 [
|
||||
append leaf-nodes current
|
||||
][
|
||||
foreach child current/children [
|
||||
if child [append stack child]
|
||||
]
|
||||
]
|
||||
]
|
||||
leaf-nodes
|
||||
]
|
||||
|
||||
;; Walk the tree to find the palette index assigned to `color`.
|
||||
;; At a leaf, return its index. Otherwise, compute which child to descend into
|
||||
;; by extracting one bit per channel at the current level to form a 3-bit index.
|
||||
;; If the exact child was pruned during palette reduction, fall back to the first
|
||||
;; available sibling.
|
||||
get-palette-index-for-color: function [node color level] [
|
||||
; Walk the tree iteratively until we reach a leaf (pixel-count > 0)
|
||||
while [node/pixel-count = 0] [
|
||||
index: 1
|
||||
mask: 128 >> level
|
||||
unless zero? color/1 & mask [index: index + 4] ; bit 2
|
||||
unless zero? color/2 & mask [index: index + 2] ; bit 1
|
||||
unless zero? color/3 & mask [index: index + 1] ; bit 0
|
||||
++ level
|
||||
;; If exact child was pruned, fall back to first available child
|
||||
either node/children/:index [
|
||||
node: node/children/:index
|
||||
][
|
||||
foreach child node/children [
|
||||
if child [node: child break]
|
||||
]
|
||||
]
|
||||
]
|
||||
node/palette-index
|
||||
]
|
||||
|
||||
;; Merge all children of `node` into the node itself, making it a leaf.
|
||||
;; Accumulates children's color sums and pixel counts into the parent.
|
||||
;; Returns the net reduction in leaf count: (number of children merged) - 1,
|
||||
;; because the parent itself becomes a new leaf.
|
||||
remove-leaves: function [node] [
|
||||
result: 0
|
||||
foreach child node/children [
|
||||
if child [
|
||||
node/red: node/red + child/red
|
||||
node/green: node/green + child/green
|
||||
node/blue: node/blue + child/blue
|
||||
node/pixel-count: node/pixel-count + child/pixel-count
|
||||
++ result
|
||||
]
|
||||
]
|
||||
result - 1
|
||||
]
|
||||
|
||||
;; Compute the average color for a leaf node by dividing accumulated
|
||||
;; channel sums by the total pixel count
|
||||
get-color: func [node] [
|
||||
make tuple! reduce [
|
||||
node/red / node/pixel-count
|
||||
node/green / node/pixel-count
|
||||
node/blue / node/pixel-count
|
||||
]
|
||||
]
|
||||
|
||||
get-pixel-count: func [node] [
|
||||
result: 0
|
||||
stack: clear []
|
||||
foreach child node/children [if child [append stack child]]
|
||||
while [not empty? stack] [
|
||||
if current: take stack [
|
||||
either current/pixel-count > 0 [
|
||||
result: result + current/pixel-count
|
||||
][
|
||||
append stack current/children
|
||||
]
|
||||
]
|
||||
]
|
||||
result
|
||||
]
|
||||
|
||||
;; Create and initialise a new octree quantizer.
|
||||
;; `levels` holds one block of nodes per depth level, used during palette reduction.
|
||||
;; `leaf-count` tracks the current number of leaves without requiring a tree walk.
|
||||
make-octree-quantizer: func [/local quantizer] [
|
||||
quantizer: context [
|
||||
levels: array/initial 8 []
|
||||
root: none
|
||||
leaf-count: 0
|
||||
;; caches:
|
||||
color-node: make map! 16384
|
||||
color-index: make map! 65536
|
||||
]
|
||||
quantizer/root: make-octree-node 0 quantizer
|
||||
quantizer
|
||||
]
|
||||
|
||||
;; Register `node` at `level` in the quantizer so it can be found during reduction
|
||||
add-level-node: func [quantizer level node] [
|
||||
append (pickz quantizer/levels level) node
|
||||
]
|
||||
|
||||
get-leaves: func [quantizer] [
|
||||
get-leaf-nodes quantizer/root
|
||||
]
|
||||
|
||||
;; Add `color` to the octree. Uses color-node cache to skip tree
|
||||
;; traversal for already-seen colors, only walking the tree on first
|
||||
;; encounter of each unique color. At each level, a 3-bit index derived
|
||||
;; from one bit per RGB channel selects the child. A new leaf is counted
|
||||
;; the first time a node receives a pixel at max depth.
|
||||
add-color: function [quantizer color [tuple!]] [
|
||||
unless node: quantizer/color-node/:color [
|
||||
;; First time seeing this color - traverse/build the tree path
|
||||
node: quantizer/root
|
||||
level: 0
|
||||
while [level < 8] [
|
||||
index: 1
|
||||
mask: 128 >> level
|
||||
unless zero? color/1 & mask [index: index + 4]
|
||||
unless zero? color/2 & mask [index: index + 2]
|
||||
unless zero? color/3 & mask [index: index + 1]
|
||||
unless node/children/:index [
|
||||
node/children/:index: make-octree-node level quantizer
|
||||
]
|
||||
node: node/children/:index
|
||||
++ level
|
||||
]
|
||||
;; Count new leaf (pixel-count = 0 means this node hasn't been seen before)
|
||||
if all [
|
||||
node/pixel-count = 0
|
||||
level >= 8
|
||||
][ quantizer/leaf-count: quantizer/leaf-count + 1 ]
|
||||
;; Cache the node so future occurrences of this color skip the traversal
|
||||
quantizer/color-node/:color: node
|
||||
]
|
||||
;; Accumulate color data regardless of cache hit or miss
|
||||
node/red: node/red + color/1
|
||||
node/green: node/green + color/2
|
||||
node/blue: node/blue + color/3
|
||||
node/pixel-count: node/pixel-count + 1
|
||||
]
|
||||
|
||||
;; Build a palette of at most `color-count` colors by reducing the octree bottom-up.
|
||||
;; Iterates levels from deepest to shallowest, merging sibling leaves into their
|
||||
;; parent until the leaf count fits within the target. Then assigns palette
|
||||
;; indices to remaining leaves and records their average colors.
|
||||
make-palette: function [quantizer color-count] [
|
||||
palette: copy []
|
||||
palette-index: 0
|
||||
leaf-count: quantizer/leaf-count
|
||||
for level-index 8 1 -1 [
|
||||
level-nodes: quantizer/levels/:level-index
|
||||
unless empty? level-nodes [
|
||||
foreach node level-nodes [
|
||||
if leaf-count <= color-count [break]
|
||||
;; count how many children this node has (= reduction + 1)
|
||||
children-count: 0
|
||||
foreach child node/children [if child [++ children-count]]
|
||||
if (leaf-count - children-count + 1) >= color-count [
|
||||
leaf-count: leaf-count - (remove-leaves node)
|
||||
]
|
||||
]
|
||||
if leaf-count <= color-count [break]
|
||||
quantizer/levels/:level-index: copy []
|
||||
]
|
||||
]
|
||||
;; Assign palette indices to remaining leaves and record their average colors
|
||||
foreach node get-leaves quantizer [
|
||||
if palette-index >= color-count [break]
|
||||
if node/pixel-count > 0 [append palette get-color node]
|
||||
node/palette-index: palette-index
|
||||
++ palette-index
|
||||
]
|
||||
palette
|
||||
]
|
||||
|
||||
;; Look up the palette index for `color` by traversing the (possibly reduced) tree
|
||||
get-palette-index: function [quantizer color] [
|
||||
unless index: quantizer/color-index/:color [
|
||||
quantizer/color-index/:color: index:
|
||||
get-palette-index-for-color quantizer/root color 0
|
||||
]
|
||||
index + 1
|
||||
]
|
||||
]
|
||||
|
||||
;; Download the original image if does not exists.
|
||||
unless exists? %Quantum_frog.png [
|
||||
write %quantum_frog.png
|
||||
read https://static.wikitide.net/rosettacodewiki/3/3f/Quantum_frog.png
|
||||
]
|
||||
|
||||
foreach [colors output-name] [
|
||||
16 %Quantum_frog-octree-16-colors.png
|
||||
32 %Quantum_frog-octree-32-colors.png
|
||||
][
|
||||
;; Quantize to N colors
|
||||
time: delta-time [
|
||||
set [img colors] octree-quantize %Quantum_frog.png colors
|
||||
]
|
||||
print ["Time to compute:" time]
|
||||
;; Display all used colors
|
||||
print ["The" length? colors "colors used:"]
|
||||
probe new-line/all colors true
|
||||
save output-name img ;; Save the result
|
||||
browse output-name ;; Display the image in a browser
|
||||
]
|
||||
|
|
@ -7,19 +7,19 @@ proc makeCluster {pixels} {
|
|||
set bmin [set bmax [lindex $pixels 0 2]]
|
||||
set rsum [set gsum [set bsum 0]]
|
||||
foreach p $pixels {
|
||||
lassign $p r g b
|
||||
if {$r<$rmin} {set rmin $r} elseif {$r>$rmax} {set rmax $r}
|
||||
if {$g<$gmin} {set gmin $g} elseif {$g>$gmax} {set gmax $g}
|
||||
if {$b<$bmin} {set bmin $b} elseif {$b>$bmax} {set bmax $b}
|
||||
incr rsum $r
|
||||
incr gsum $g
|
||||
incr bsum $b
|
||||
lassign $p r g b
|
||||
if {$r<$rmin} {set rmin $r} elseif {$r>$rmax} {set rmax $r}
|
||||
if {$g<$gmin} {set gmin $g} elseif {$g>$gmax} {set gmax $g}
|
||||
if {$b<$bmin} {set bmin $b} elseif {$b>$bmax} {set bmax $b}
|
||||
incr rsum $r
|
||||
incr gsum $g
|
||||
incr bsum $b
|
||||
}
|
||||
set n [llength $pixels]
|
||||
list [expr {double($n)*($rmax-$rmin)*($gmax-$gmin)*($bmax-$bmin)}] \
|
||||
[list [expr {$rsum/$n}] [expr {$gsum/$n}] [expr {$bsum/$n}]] \
|
||||
[list [expr {$rmax-$rmin}] [expr {$gmax-$gmin}] [expr {$bmax-$bmin}]] \
|
||||
$pixels
|
||||
[list [expr {$rsum/$n}] [expr {$gsum/$n}] [expr {$bsum/$n}]] \
|
||||
[list [expr {$rmax-$rmin}] [expr {$gmax-$gmin}] [expr {$bmax-$bmin}]] \
|
||||
$pixels
|
||||
}
|
||||
|
||||
proc colorQuant {img n} {
|
||||
|
|
@ -27,52 +27,52 @@ proc colorQuant {img n} {
|
|||
set height [image height $img]
|
||||
# Extract the pixels from the image
|
||||
for {set x 0} {$x < $width} {incr x} {
|
||||
for {set y 0} {$y < $height} {incr y} {
|
||||
lappend pixels [$img get $x $y]
|
||||
}
|
||||
for {set y 0} {$y < $height} {incr y} {
|
||||
lappend pixels [$img get $x $y]
|
||||
}
|
||||
}
|
||||
# Divide pixels into clusters
|
||||
for {set cs [list [makeCluster $pixels]]} {[llength $cs] < $n} {} {
|
||||
set cs [lsort -real -index 0 $cs]
|
||||
lassign [lindex $cs end] score centroid volume pixels
|
||||
lassign $centroid cr cg cb
|
||||
lassign $volume vr vg vb
|
||||
while 1 {
|
||||
set p1 [set p2 {}]
|
||||
if {$vr>$vg && $vr>$vb} {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 0]<$cr} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
} elseif {$vg>$vb} {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 1]<$cg} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
} else {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 2]<$cb} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
}
|
||||
if {[llength $p1] && [llength $p2]} break
|
||||
# Partition failed! Perturb partition point away from the centroid and try again
|
||||
set cr [expr {$cr + 20*rand() - 10}]
|
||||
set cg [expr {$cg + 20*rand() - 10}]
|
||||
set cb [expr {$cb + 20*rand() - 10}]
|
||||
}
|
||||
set cs [lreplace $cs end end [makeCluster $p1] [makeCluster $p2]]
|
||||
set cs [lsort -real -index 0 $cs]
|
||||
lassign [lindex $cs end] score centroid volume pixels
|
||||
lassign $centroid cr cg cb
|
||||
lassign $volume vr vg vb
|
||||
while 1 {
|
||||
set p1 [set p2 {}]
|
||||
if {$vr>$vg && $vr>$vb} {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 0]<$cr} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
} elseif {$vg>$vb} {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 1]<$cg} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
} else {
|
||||
foreach p $pixels {
|
||||
if {[lindex $p 2]<$cb} {lappend p1 $p} {lappend p2 $p}
|
||||
}
|
||||
}
|
||||
if {[llength $p1] && [llength $p2]} break
|
||||
# Partition failed! Perturb partition point away from the centroid and try again
|
||||
set cr [expr {$cr + 20*rand() - 10}]
|
||||
set cg [expr {$cg + 20*rand() - 10}]
|
||||
set cb [expr {$cb + 20*rand() - 10}]
|
||||
}
|
||||
set cs [lreplace $cs end end [makeCluster $p1] [makeCluster $p2]]
|
||||
}
|
||||
# Produce map from pixel values to quantized values
|
||||
foreach c $cs {
|
||||
set centroid [format "#%02x%02x%02x" {*}[lindex $c 1]]
|
||||
foreach p [lindex $c end] {
|
||||
set map($p) $centroid
|
||||
}
|
||||
set centroid [format "#%02x%02x%02x" {*}[lindex $c 1]]
|
||||
foreach p [lindex $c end] {
|
||||
set map($p) $centroid
|
||||
}
|
||||
}
|
||||
# Remap the source image
|
||||
set newimg [image create photo -width $width -height $height]
|
||||
for {set x 0} {$x < $width} {incr x} {
|
||||
for {set y 0} {$y < $height} {incr y} {
|
||||
$newimg put $map([$img get $x $y]) -to $x $y
|
||||
}
|
||||
for {set y 0} {$y < $height} {incr y} {
|
||||
$newimg put $map([$img get $x $y]) -to $x $y
|
||||
}
|
||||
}
|
||||
return $newimg
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue