Skip to content

Commit 6ca86d5

Browse files
committed
Median cut creation of palettes added.
1 parent 2448a3a commit 6ca86d5

File tree

4 files changed

+50
-10
lines changed

4 files changed

+50
-10
lines changed

hitherdither/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
# Version information.
20-
__version__ = '0.1.1'
20+
__version__ = '0.1.2.dev1'
2121
version = __version__ # backwards compatibility name
2222
try:
2323
version_info = [int(x) if x.isdigit() else x for x in

hitherdither/palette.py

+44-5
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,50 @@ def pixel_closest_colour(self, pixel, order=2):
9494

9595
@classmethod
9696
def create_by_kmeans(cls, image):
97-
pass
97+
raise NotImplementedError()
9898

9999
@classmethod
100-
def create_by_median_cut(cls, image):
101-
pass
100+
def create_by_median_cut(cls, image, n=16, dim=None):
101+
img = np.array(image)
102+
# Create pixel buckets to simplify sorting and splitting.
103+
if img.ndim == 3:
104+
pixels = img.reshape((img.shape[0] * img.shape[1], img.shape[2]))
105+
elif img.ndim == 2:
106+
pixels = img.reshape((img.shape[0] * img.shape[1], 1))
107+
108+
def median_cut(p, dim=None):
109+
"""Median cut method.
110+
111+
Reference:
112+
https://en.wikipedia.org/wiki/Median_cut
113+
114+
:param p: The pixel array to split in two.
115+
:return: Two numpy arrays, split by median cut method.
116+
"""
117+
if dim is not None:
118+
sort_dim = dim
119+
else:
120+
mins = p.min(axis=0)
121+
maxs = p.max(axis=0)
122+
sort_dim = np.argmax(maxs - mins)
123+
124+
argument = np.argsort(p[:, sort_dim])
125+
p = p[argument, :]
126+
m = np.median(p[:, sort_dim])
127+
split_mask = p[:, sort_dim] >= m
128+
return [p[-split_mask, :].copy(), p[split_mask, :].copy()]
129+
130+
# Do actual splitting loop.
131+
bins = [pixels, ]
132+
while len(bins) < n:
133+
new_bins = []
134+
for bin in bins:
135+
new_bins += median_cut(bin, dim)
136+
bins = new_bins
137+
138+
# Average over pixels in each bin to create
139+
colours = np.array([np.array(bin.mean(axis=0).round(), 'uint8') for bin in bins], 'uint8')
140+
return cls(colours)
102141

103142
def create_PIL_png_from_closest_colour(self, cc):
104143
"""Create a ``P`` PIL image with this palette.
@@ -108,15 +147,15 @@ def create_PIL_png_from_closest_colour(self, cc):
108147
Reference: http://stackoverflow.com/a/29438149
109148
110149
:param :class:`numpy.ndarray` cc: A ``[M x N]`` array with integer
111-
values representing palette colour indices to build image from.
150+
values representing palette colour indices to build image from.
112151
:return: A :class:`PIL.Image.Image` image of mode ``P``.
113152
114153
"""
115154
pa_image = Image.new("P", cc.shape[::-1])
116155
pa_image.putpalette(self.colours.flatten().tolist())
117156
im = Image.fromarray(np.array(cc, 'uint8')).im.convert("P", 0, pa_image.im)
118157
return pa_image._makeself(im)
119-
158+
120159
def create_PIL_png_from_rgb_array(self, img_array):
121160
"""Create a ``P`` PIL image from a RGB image with this palette.
122161

run.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@
2626
s = data.scene()
2727
p = Palette(hitherdither.data.palette())
2828

29+
p2 = Palette.create_by_median_cut(s)
30+
2931
# Map raw image to the palette
3032
closest_colour = p.image_closest_colour(s, order=2)
3133
# Render the undithered image with only colours in
3234
# the palette as a RGB numpy array.
3335
undithered_image = p.render(closest_colour)
3436
# Create a PIL Image of mode "P" from the palette colour index matrix.
35-
#s_png = p.create_PIL_png_from_closest_colour(closest_colour)
36-
#s_png.show()
37+
s_png = p.create_PIL_png_from_closest_colour(closest_colour)
38+
s_png.show()
3739

3840
#print(np.linalg.norm(undithered_image - np.array(s_png.convert("RGB"))))
3941

setup.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
basedir = os.path.dirname(os.path.abspath(__file__))
2323

2424
if sys.argv[-1] == 'publish':
25-
os.system('python setup.py register')
2625
os.system('python setup.py sdist upload')
2726
os.system('python setup.py bdist_wheel upload')
2827
sys.exit()
@@ -42,7 +41,7 @@ def read(f):
4241
author='Henrik Blidh',
4342
author_email='[email protected]',
4443
url='https://github.com/hbldh/hitherdither',
45-
description='PIL extension with dithering algorithms that is applicable for arbitrary palettes',
44+
description='Dithering algorithms for arbitrary palettes in PIL',
4645
long_description=read('README.rst'),
4746
license='MIT',
4847
keywords=['PIL', 'Pillow', 'dithering', 'dither'],

0 commit comments

Comments
 (0)