Color quantization: Difference between revisions

m
(Rename Perl 6 -> Raku, alphabetize, minor clean-up)
m (→‎{{header|Wren}}: Minor tidy)
 
(8 intermediate revisions by 5 users not shown)
Line 16:
# Node folding priorities are tracked by a binary heap instead of typical linked list.
The output image is better at preserving textures of the original than Gimp, though it obviously depends on the input image. This particular frog image has the color bar added at the top specifically to throw off some early truncation algorithms, which Gimp is suseptible to.
<langsyntaxhighlight lang="c">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 */
Line 126:
node_free();
free(heap.buf);
}</langsyntaxhighlight>
 
=={{header|Common Lisp}}==
{{libheader|opticl}}
Use median cut.
<langsyntaxhighlight lang="lisp">(defpackage #:quantize
(:use #:cl
#:opticl))
Line 196:
(quantized (gethash original color-map)))
(values-list quantized)))
(write-png-file output-file result-image)))</langsyntaxhighlight>
 
=={{header|D}}==
Line 202:
{{trans|OCaml}}
This code retains the style of the original OCaML code, and uses the bitmap module from the Bitmap Task.
<langsyntaxhighlight lang="d">import core.stdc.stdio, std.stdio, std.algorithm, std.typecons,
std.math, std.range, std.conv, std.string, bitmap;
 
Line 346:
const imq = colorQuantize(im, nCols);
imq.savePPM6("quantum_frog_quantized.ppm");
}</langsyntaxhighlight>
 
===Imperative Version===
{{trans|C}}
This code retains part of the style of the original C code.
<langsyntaxhighlight lang="d">import core.stdc.stdlib: malloc, calloc, realloc, free, abort;
import std.stdio: stderr, File;
import std.ascii: isWhite;
Line 797:
im.free;
return 0;
}</langsyntaxhighlight>
Compiled with ldc2, it runs on the quantum_frog image in about 0.20 seconds with dithering and about 0.10 seconds without dithering.
 
=={{header|Go}}==
A very basic median cut algorithm, no dithering.
<langsyntaxhighlight lang="go">package main
 
import (
Line 1,104:
*pq = q[:n]
return c
}</langsyntaxhighlight>
 
=={{header|Haskell}}==
{{libheader|JuicyPixels}}
A variation of the median cut algorithm by splitting color space on the nearest to the mean instead. It provides lower error than the Gimp output sample.
<langsyntaxhighlight Haskelllang="haskell">import qualified Data.ByteString.Lazy as BS
import qualified Data.Foldable as Fold
import qualified Data.List as List
Line 1,236:
case args of
[path, outpath] -> quantizeIO path outpath 16
_ -> putStrLn $ "Usage: " ++ prog ++ " <image-file> <out-file.png>"</langsyntaxhighlight>
 
=={{header|J}}==
Line 1,242:
Here, we use a simplistic averaging technique to build an initial set of colors and then use k-means clustering to refine them.
 
<langsyntaxhighlight lang="j">kmcL=:4 :0
C=. /:~ 256 #.inv ,y NB. colors
G=. x (i.@] <.@* %) #C NB. groups (initial)
Line 1,251:
G=. (i. <./)"1 C +/&.:*: .- |:Q
end.Q
)</langsyntaxhighlight>
 
The left argument is the number of colors desired.
Line 1,259:
The result is the colors represented as pixel triples (blue, green, red). They are shown here as fractional numbers, but they should be either rounded to the nearest integer in the range 0..255 (and possibly converted back to bmp integer form) or scaled so they are floating point triples in the range 0..1.
 
<langsyntaxhighlight lang="j"> 16 kmcL img
7.52532 22.3347 0.650468
8.20129 54.4678 0.0326828
Line 1,275:
164.969 199.742 67.0467
179.849 207.594 109.973
209.229 221.18 204.513</langsyntaxhighlight>
 
=={{header|Java}}==
A simple median cut algorithm.
<syntaxhighlight lang="java">
 
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
 
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];
for ( Item item : aBucket ) {
for ( Channel channel : Channel.values() ) {
means[channel.index] += item.getPrimary(channel);
}
}
for ( Channel channel : Channel.values() ) {
means[channel.index] /= aBucket.size();
}
Color color = new Color(means[Channel.RED.index], means[Channel.GREEN.index], means[Channel.BLUE.index]);
colorsUsed.add(color);
 
for ( Item item : aBucket ) {
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>();
}
</syntaxhighlight>
{{ out }}
[[Media:Quantum_frog16Java.png]]
<pre>
The 16 colors used in Red, Green, Blue format are:
(8, 36, 0)
(7, 54, 0)
(22, 66, 0)
(51, 99, 3)
(53, 129, 1)
(64, 140, 2)
(67, 142, 5)
(71, 144, 11)
(79, 143, 11)
(99, 139, 14)
(88, 156, 18)
(129, 171, 23)
(127, 165, 53)
(166, 205, 65)
(165, 189, 123)
(205, 226, 159)
</pre>
 
=={{header|Julia}}==
The Images package for Julia uses the ImageMagick libraries by default, but this Julia module does not currently implement ImageMagick's support for color quantization. However, once ImageMagick is installed for the Images Julia module, a direct call to ImageMagick's convert command is possible.
<langsyntaxhighlight lang="julia">
const execstring =`convert Quantum_frog.png -dither None -colors 16 Quantum_frog_new.png`
run(execstring)
</syntaxhighlight>
</lang>
 
=={{header|Kotlin}}==
{{works with|Ubuntu 16.04}}
Rather than coding this from scratch, we invoke programatically ImageMagick's 'convert' tool which has all this stuff built in.
<langsyntaxhighlight lang="scala">// Version 1.2.41
 
import java.io.BufferedReader
Line 1,327 ⟶ 1,481:
}
br.close()
}</langsyntaxhighlight>
 
{{output}}
Line 1,351 ⟶ 1,505:
 
=={{header|Mathematica}} / {{header|Wolfram Language}}==
<langsyntaxhighlight Mathematicalang="mathematica">ColorQuantize[Import["http://rosettacode.org/mw/images/3/3f/Quantum_frog.png"],16,Dithering->False]</langsyntaxhighlight>
 
=={{header|Nim}}==
{{libheader|nimPNG}}
We use a simple version of the median cut algorithm, with no special optimizations.
 
<syntaxhighlight lang="nim">import algorithm
import nimPNG
 
type
 
Channel {.pure.} = enum R, G, B
 
QItem = tuple
color: array[Channel, byte] # Color of the pixel.
index: int # Position of pixel in the sequential sequence.
 
#---------------------------------------------------------------------------------------------------
 
proc quantize(bucket: openArray[QItem]; output: var seq[byte]) =
## Apply the quantization to the pixels in the bucket.
 
# Compute the mean value on each channel.
var means: array[Channel, int]
for qItem in bucket:
for channel in R..B:
means[channel] += qItem.color[channel].int
for channel in R..B:
means[channel] = (means[channel] / bucket.len).toInt
 
# Store the new colors into the pixels.
for qItem in bucket:
for channel in R..B:
output[3 * qItem.index + ord(channel)] = means[channel].byte
 
#---------------------------------------------------------------------------------------------------
 
proc medianCut(bucket: openArray[QItem]; depth: Natural; output: var seq[byte]) =
## Apply the algorithm on the bucket.
 
if depth == 0:
# Terminated for this bucket. Apply the quantization.
quantize(bucket, output)
return
 
# Compute the range of values for each channel.
var minVal: array[Channel, int] = [1000, 1000, 1000]
var maxVal: array[Channel, int] = [-1, -1, -1]
for qItem in bucket:
for channel in R..B:
let val = qItem.color[channel].int
if val < minVal[channel]: minVal[channel] = val
if val > maxVal[channel]: maxVal[channel] = val
let valRange: array[Channel, int] = [maxVal[R] - minVal[R],
maxVal[G] - minVal[G],
maxVal[B] - minVal[B]]
 
# Find the channel with the greatest range.
var selchannel: Channel
if valRange[R] >= valRange[G]:
if valRange[R] >= valRange[B]:
selchannel = R
else:
selchannel = B
elif valrange[G] >= valrange[B]:
selchannel = G
else:
selchannel = B
 
# Sort the quantization items according to the selected channel.
let sortedBucket = case selchannel
of R: sortedByIt(bucket, it.color[R])
of G: sortedByIt(bucket, it.color[G])
of B: sortedByIt(bucket, it.color[B])
 
# Split the bucket into two buckets.
let medianIndex = bucket.high div 2
medianCut(sortedBucket.toOpenArray(0, medianIndex), depth - 1, output)
medianCut(sortedBucket.toOpenArray(medianIndex, bucket.high), depth - 1, output)
 
#———————————————————————————————————————————————————————————————————————————————————————————————————
 
const Input = "Quantum_frog.png"
const Output = "Quantum_frog_16.png"
 
let pngImage = loadPNG24(seq[byte], Input).get()
 
# Build the first bucket.
var bucket = newSeq[QItem](pngImage.data.len div 3)
var idx: Natural = 0
for item in bucket.mitems:
item = (color: [pngImage.data[idx], pngImage.data[idx + 1], pngImage.data[idx + 2]],
index: idx div 3)
inc idx, 3
 
# Create the storage for the quantized image.
var data = newSeq[byte](pngImage.data.len)
 
# Launch the quantization.
medianCut(bucket, 4, data)
 
# Save the result into a PNG file.
let status = savePNG24(Output, data, pngImage.width, pngImage.height)
if status.isOk:
echo "File ", Input, " processed. Result is available in file ", Output
else:
echo "Error: ", status.error</syntaxhighlight>
 
=={{header|OCaml}}==
Line 1,357 ⟶ 1,617:
Here we use a simplified method inspired from this paper: [http://www.leptonica.com/papers/mediancut.pdf www.leptonica.com/papers/mediancut.pdf]
 
<langsyntaxhighlight lang="ocaml">let rem_from rem from =
List.filter ((<>) rem) from
 
Line 1,457 ⟶ 1,717:
done;
done;
(res)</langsyntaxhighlight>
 
=={{header|Perl}}==
<langsyntaxhighlight lang="perl">use strict;
use warnings;
 
Line 1,468 ⟶ 1,728:
$img->read(file => 'frog.png');
my $img16 = $img->to_paletted({ max_colors => 16});
$img16->write(file => "frog-16.png")</langsyntaxhighlight>
Compare offsite images: [https://github.com/SqrtNegInf/Rosettacode-Perl5-Smoke/blob/master/ref/frog.png frog.png] vs.
[https://github.com/SqrtNegInf/Rosettacode-Perl5-Smoke/blob/master/ref/frog-16.png frog-16.png]
 
=={{header|Phix}}==
{{libheader|Phix/pGUI}}
{{trans|Tcl}}
Gui app, shows original and modified side-by-side.
<langsyntaxhighlight Phixlang="phix">-- demo\rosetta\Color_quantization.exw
include pGUI.e
 
Line 1,555 ⟶ 1,815:
imImage new_image = imImageCreateBased(im1, -1, -1, IM_RGB, -1)
imConvertColorSpace(im1, new_image)
im1 = imImageDestroy(im1)
im1 = new_image
end if
Line 1,568 ⟶ 1,828:
Ihandle dlg = IupDialog(IupHbox({label1, label2}))
IupSetAttribute(dlg, "TITLE", "Color quantization")
IupCloseOnEscape(dlg)
IupShow(dlg)
 
IupMainLoop()
IupClose()</langsyntaxhighlight>
 
=={{header|PureBasic}}==
[[file:Compare_16_Quantum_frog_PureBasic.png|comparison|thumb|200px]]
[[file:Compare_16_Quantum_frog_histograms_PureBasic.png|histogram (external application)|thumb|200px]]
<syntaxhighlight lang="purebasic">
<lang PureBasic>
; ColorQuantization.pb
 
Line 1,800 ⟶ 2,059:
 
 
</syntaxhighlight>
</lang>
 
=={{header|Python}}==
<langsyntaxhighlight lang="python">from PIL import Image
 
if __name__=="__main__":
im = Image.open("frog.png")
im2 = im.quantize(16)
im2.show()</langsyntaxhighlight>
 
=={{header|Racket}}==
<syntaxhighlight lang="racket">
<lang Racket>
#lang racket/base
(require racket/class
Line 2,138 ⟶ 2,397:
r g b))
(loop child (add1 level))))))
</syntaxhighlight>
</lang>
 
=={{header|Raku}}==
Line 2,144 ⟶ 2,403:
{{works with|Rakudo|2018.10}}
 
<syntaxhighlight lang="raku" perl6line>use MagickWand;
use MagickWand::Enums;
 
Line 2,150 ⟶ 2,409:
$frog.read("./Quantum_frog.png");
$frog.quantize(16, RGBColorspace, 0, True, False);
$frog.write('./Quantum-frog-16-perl6.png');</langsyntaxhighlight>
See: [https://github.com/thundergnat/rc/blob/master/img/Quantum-frog-16-perl6.png Quantum-frog-16-perl6.png] (offsite .png image)
 
=={{header|Sidef}}==
<langsyntaxhighlight lang="ruby">require('Image::Magick')
 
func quantize_image(n = 16, input, output='output.png') {
Line 2,163 ⟶ 2,422:
}
 
quantize_image(input: 'Quantum_frog.png')</langsyntaxhighlight>
 
=={{header|Tcl}}==
{{trans|OCaml}}
{{libheader|Tk}}
<langsyntaxhighlight lang="tcl">package require Tcl 8.6
package require Tk
 
Line 2,245 ⟶ 2,504:
}
return $newimg
}</langsyntaxhighlight>
Demonstration code:
<langsyntaxhighlight lang="tcl">set src [image create photo -file quantum_frog.png]
set dst [colorQuant $src 16]
# Save as GIF now that quantization is done, then exit explicitly (no GUI desired)
$dst write quantum_frog_compressed.gif
exit</langsyntaxhighlight>
 
=={{header|Wren}}==
{{trans|Nim}}
{{libheader|DOME}}
{{libheader|Wren-dynamic}}
{{libheader|Wren-sort}}
<syntaxhighlight lang="wren">import "dome" for Window
import "graphics" for Canvas, Color, ImageData
import "./dynamic" for Struct
import "./sort" for Sort
 
var QItem = Struct.create("QItem", ["color", "index"])
 
var ColorsUsed = []
 
class ColorQuantization {
construct new(filename, filename2) {
Window.title = "Color quantization"
_image = ImageData.load(filename)
_w = _image.width
_h = _image.height
Window.resize(_w * 2 + 20, _h + 30)
Canvas.resize(_w * 2 + 20, _h + 30)
 
// draw original image on left half of canvas
_image.draw(0, 0)
Canvas.print(filename, _w/4, _h + 10, Color.white)
 
// create ImageData object for the quantized image
_qImage = ImageData.create(filename2, _w, _h)
_qFilename = filename2
}
 
init() {
// build the first bucket
var bucket = List.filled(_w * _h, null)
for (x in 0..._w) {
for (y in 0..._h) {
var idx = x * _w + y
bucket[idx] = QItem.new(_image.pget(x, y), idx)
}
}
var output = List.filled(_w * _h, Color.black)
 
// launch the quantization
medianCut(bucket, 4, output)
 
// load the result into the quantized ImageData object
for (x in 0..._w) {
for (y in 0..._h) {
_qImage.pset(x, y, output[x *_w + y])
}
}
 
// draw the quantized image on right half of canvas
_qImage.draw(_w + 20, 0)
Canvas.print(_qFilename, _w * 5/4 + 20, _h + 10, Color.white)
 
// save it to a file
_qImage.saveToFile(_qFilename)
 
// print colors used to terminal
System.print("The 16 colors used in R, G, B format are:")
for (c in ColorsUsed) {
System.print("(%(c.r), %(c.g), %(c.b))")
}
}
 
// apply the quantization to the colors in the bucket
quantize(bucket, output) {
// compute the mean value on each RGB component
var means = List.filled(3, 0)
for (q in bucket) {
var i = 0
for (val in [q.color.r, q.color.g, q.color.b]) {
means[i] = means[i] + val
i = i + 1
}
}
for (i in 0..2) {
means[i] = (means[i]/bucket.count).floor
}
var c = Color.rgb(means[0], means[1], means[2])
ColorsUsed.add(c)
 
// store the new color in the output list
for (q in bucket) output[q.index] = c
}
 
// apply the algorithm to the bucket of colors
medianCut(bucket, depth, output) {
if (depth == 0) { // terminated for this bucket, apply the quantization
quantize(bucket, output)
return
}
 
// compute the range of values for each RGB component
var minVal = [1000, 1000, 1000]
var maxVal = [-1, -1, -1]
for (q in bucket) {
var i = 0
for (val in [q.color.r, q.color.g, q.color.b]) {
if (val < minVal[i]) minVal[i] = val
if (val > maxVal[i]) maxVal[i] = val
i = i + 1
}
}
var valRange = [maxVal[0] - minVal[0], maxVal[1] - minVal[1], maxVal[2] - minVal[2]]
 
// find the RGB component with the greatest range
var greatest = 0
if (valRange[1] > valRange[0]) greatest = 1
if (valRange[2] > greatest) greatest = 2
// sort the quantization items according to the greatest
var cmp
if (greatest == 0) {
cmp = Fn.new { |i, j|
var t = (i.color.r - j.color.r).sign
if (t != 0) return t
return (i.index - j.index).sign
}
} else if (greatest == 1) {
cmp = Fn.new { |i, j|
var t = (i.color.g - j.color.g).sign
if (t != 0) return t
return (i.index - j.index).sign
}
} else {
cmp = Fn.new { |i, j|
var t = (i.color.b - j.color.b).sign
if (t != 0) return t
return (i.index - j.index).sign
}
}
Sort.quick(bucket, 0, bucket.count-1, cmp)
var medianIndex = ((bucket.count-1)/2).floor
medianCut(bucket[0...medianIndex], depth - 1, output)
medianCut(bucket[medianIndex..-1], depth - 1, output)
}
 
update() {}
 
draw(alpha) {}
}
 
var Game = ColorQuantization.new("Quantum_frog.png", "Quantum_frog_16.png")</syntaxhighlight>
 
{{out}}
<pre>
The 16 colors used in R, G, B format are:
(5, 48, 0)
(20, 62, 0)
(24, 64, 0)
(44, 96, 0)
(61, 126, 2)
(64, 135, 3)
(71, 138, 5)
(74, 142, 7)
(80, 146, 9)
(88, 150, 13)
(99, 155, 20)
(114, 163, 30)
(135, 177, 48)
(159, 194, 70)
(173, 202, 102)
(199, 214, 186)
</pre>
 
{{omit from|AWK}}
9,476

edits