Push it
1
css/picnic.min.css
vendored
Normal file
186
dither.py
Executable file
@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/local/bin/python3.8
|
||||||
|
|
||||||
|
import hitherdither
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
from PIL import Image
|
||||||
|
import logging
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
"""
|
||||||
|
This script recursively traverses folders and creates dithered versions of the images it finds.
|
||||||
|
These are stored in the same folder as the images in a folder called "dithers".
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--directory', help="Set the directory to traverse", default="."
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-rm', '--remove', help="Removes all the folders with dithers and their contents", action="store_true"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--colorize', help="Colorizes the dithered images", action="store_true"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--verbose', help="Print out more detailed information about what this script is doing", action="store_true"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
image_ext = [".jpg", ".JPG", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"]
|
||||||
|
|
||||||
|
|
||||||
|
content_dir = args.directory
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
exclude_dirs = set(["dithers"])
|
||||||
|
|
||||||
|
|
||||||
|
logging.info("Dithering all images in {} and subfolders".format(content_dir))
|
||||||
|
logging.debug("excluding directories: {}".format("".join(exclude_dirs)))
|
||||||
|
|
||||||
|
def colorize(source_image, category):
|
||||||
|
"""
|
||||||
|
Picks a colored dithering palette based on the post category.
|
||||||
|
"""
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
'low-tech': hitherdither.palette.Palette([(30,32,40), (11,21,71),(57,77,174),(158,168,218),(187,196,230),(243,244,250)]),
|
||||||
|
'obsolete': hitherdither.palette.Palette([(9,74,58), (58,136,118),(101,163,148),(144,189,179),(169,204,195),(242,247,246)]),
|
||||||
|
'high-tech': hitherdither.palette.Palette([(86,9,6), (197,49,45),(228,130,124),(233,155,151),(242,193,190),(252,241,240)]),
|
||||||
|
'grayscale': hitherdither.palette.Palette([(25,25,25), (75,75,75),(125,125,125),(175,175,175),(225,225,225),(250,250,250)])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if category:
|
||||||
|
|
||||||
|
for i in colors.keys():
|
||||||
|
if i in category.lower():
|
||||||
|
color = colors[i]
|
||||||
|
logging.info("Applying color palette '{}' for {}".format(i, category))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logging.info("No category for {}, {}".format(source_image, category))
|
||||||
|
print("No category for {}, {}".format(source_image, category))
|
||||||
|
color = colors['grayscale']
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.info("No category for {}, {}".format(source_image, category))
|
||||||
|
print("No category for {}, {}".format(source_image, category))
|
||||||
|
color = colors['grayscale']
|
||||||
|
|
||||||
|
return color
|
||||||
|
|
||||||
|
|
||||||
|
def dither_image(source_image, output_image, category ='high-tech'):
|
||||||
|
#see hitherdither docs for different dithering algos and settings
|
||||||
|
|
||||||
|
# if args.colorize:
|
||||||
|
# palette = colorize(source_image, category)
|
||||||
|
# else:
|
||||||
|
# palette = hitherdither.palette.Palette([(25,25,25), (75,75,75),(125,125,125),(175,175,175),(225,225,225),(250,250,250)])
|
||||||
|
try:
|
||||||
|
img= Image.open(source_image).convert('RGB')
|
||||||
|
img.thumbnail((800,800), Image.LANCZOS)
|
||||||
|
#palette = palettes[category]
|
||||||
|
#palette = hitherdither.palette.Palette.create_by_median_cut(img)
|
||||||
|
palette = hitherdither.palette.Palette(
|
||||||
|
[0x080000, 0x201A0B, 0x432817, 0x492910,
|
||||||
|
0x234309, 0x5D4F1E, 0x9C6B20, 0xA9220F,
|
||||||
|
0x2B347C, 0x2B7409, 0xD0CA40, 0xE8A077,
|
||||||
|
0x6A94AB, 0xD5C4B3, 0xFCE76E, 0xFCFAE2]
|
||||||
|
)
|
||||||
|
threshold = [96, 96, 96]
|
||||||
|
img_dithered = hitherdither.ordered.bayer.bayer_dithering(img, palette, threshold, order=8)
|
||||||
|
#if args.colorize:
|
||||||
|
# img_dithered = colorize(img_dithered, category)
|
||||||
|
# logging.debug("Created {} in category {}".format(img_dithered, category))
|
||||||
|
|
||||||
|
img_dithered.save(output_image, optimize=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(" failed to convert {}".format(source_image))
|
||||||
|
logging.debug(e)
|
||||||
|
|
||||||
|
def delete_dithers(content_dir):
|
||||||
|
logging.info("Deleting 'dither' folders in {} and below".format(content_dir))
|
||||||
|
for root, dirs, files in os.walk(content_dir, topdown=True):
|
||||||
|
if root.endswith('dithers'):
|
||||||
|
shutil.rmtree(root)
|
||||||
|
logging.info("Removed {}".format(root))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_front_matter(md):
|
||||||
|
with open(md) as f:
|
||||||
|
contents = f.readlines()
|
||||||
|
cat = None
|
||||||
|
for l in contents:
|
||||||
|
if l.startswith("categories: "):
|
||||||
|
cat = l.split("categories: ")[1]
|
||||||
|
cat = cat.strip("[")
|
||||||
|
cat = cat.strip()
|
||||||
|
cat = cat.strip("]")
|
||||||
|
|
||||||
|
logging.debug("Categories: {} from {}".format(cat, l.strip()))
|
||||||
|
return cat
|
||||||
|
|
||||||
|
prev_root = None
|
||||||
|
|
||||||
|
if args.remove:
|
||||||
|
delete_dithers(
|
||||||
|
os.path.abspath(content_dir)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for root, dirs, files in os.walk(os.path.abspath(content_dir), topdown=True):
|
||||||
|
logging.debug("Checking next folder {}".format(root))
|
||||||
|
|
||||||
|
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
||||||
|
|
||||||
|
category = None
|
||||||
|
if prev_root is None:
|
||||||
|
prev_root = root
|
||||||
|
|
||||||
|
if prev_root is not root:
|
||||||
|
if files:
|
||||||
|
if any(x.endswith(tuple(image_ext)) for x in files):
|
||||||
|
if not os.path.exists(os.path.join(root,'dithers')):
|
||||||
|
os.mkdir(os.path.join(root,'dithers'))
|
||||||
|
logging.info(" created in {}".format(root))
|
||||||
|
|
||||||
|
if args.colorize:
|
||||||
|
#iterate over md files to find one with a category
|
||||||
|
if not category:
|
||||||
|
for i in os.listdir(root):
|
||||||
|
if i.startswith('index'):
|
||||||
|
category2 = parse_front_matter(os.path.join(root,i))
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
for fname in files:
|
||||||
|
if fname.endswith(tuple(image_ext)):
|
||||||
|
file_, ext = os.path.splitext(fname)
|
||||||
|
source_image= os.path.join(root,fname)
|
||||||
|
output_image = os.path.join(os.path.join(root, 'dithers'), file_+'_dithered.png')
|
||||||
|
if not os.path.exists(output_image):
|
||||||
|
if not args.colorize:
|
||||||
|
category2 = "high-tech"
|
||||||
|
dither_image(source_image,output_image, category2)
|
||||||
|
logging.info(" converted {}".format(fname))
|
||||||
|
logging.debug(output_image)
|
||||||
|
else:
|
||||||
|
logging.debug("Dithered version of {} found, skipping".format(fname))
|
||||||
|
|
||||||
|
prev_root = root
|
||||||
|
|
||||||
|
|
||||||
|
logging.info("Done dithering")
|
BIN
dithers/favicon.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
dithers/fonk.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
dithers/git.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
dithers/jitsi.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
dithers/nitter.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
dithers/piped.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
dithers/rd.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
dithers/rss.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
dithers/teddit.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
dithers/thedroth_logo.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
dithers/wikiless.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
favicon.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
fonts/comfortaa.ttf
Normal file
BIN
fonts/exo2.ttf
Normal file
BIN
fonts/exo2l.otf
Normal file
15
hitherdither/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from . import data
|
||||||
|
from . import math
|
||||||
|
from . import ordered
|
||||||
|
from . import diffusion
|
||||||
|
from . import palette
|
||||||
|
from . import utils
|
||||||
|
from .__version__ import __version__, version
|
BIN
hitherdither/__init__.pyc
Normal file
BIN
hitherdither/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
hitherdither/__pycache__/__version__.cpython-38.pyc
Normal file
BIN
hitherdither/__pycache__/diffusion.cpython-38.pyc
Normal file
BIN
hitherdither/__pycache__/exceptions.cpython-38.pyc
Normal file
BIN
hitherdither/__pycache__/palette.cpython-38.pyc
Normal file
BIN
hitherdither/__pycache__/utils.cpython-38.pyc
Normal file
17
hitherdither/__version__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
__version__.py
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2017-05-10 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
# Version information.
|
||||||
|
__version__ = "0.1.7"
|
||||||
|
version = __version__ # backwards compatibility name
|
89
hitherdither/data/__init__.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pathlib2 as pathlib
|
||||||
|
except ImportError:
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import urlopen
|
||||||
|
except ImportError:
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def scene():
|
||||||
|
"""Chrono Cross PNG image used in Yliluoma's web page.
|
||||||
|
|
||||||
|
:return: The PIL image of the Chrono Cross scene.
|
||||||
|
|
||||||
|
"""
|
||||||
|
image_path = pathlib.Path(__file__).resolve().parent.joinpath("scene.png")
|
||||||
|
image_url = "http://bisqwit.iki.fi/jutut/kuvat/ordered_dither/scene.png"
|
||||||
|
return _image(image_path, image_url)
|
||||||
|
|
||||||
|
|
||||||
|
def scene_undithered():
|
||||||
|
"""Chrono Cross PNG image rendered directly with specified palette.
|
||||||
|
|
||||||
|
:return: The PIL image of the undithered Chrono Cross scene.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _image(
|
||||||
|
pathlib.Path(__file__).resolve().parent.joinpath("scenenodither.png"),
|
||||||
|
"http://bisqwit.iki.fi/jutut/kuvat/ordered_dither/scenenodither.png",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def scene_bayer0():
|
||||||
|
"""Chrono Cross PNG image dithered using ordered Bayer matrix method.
|
||||||
|
|
||||||
|
:return: The PIL image of the ordered Bayer matrix dithered
|
||||||
|
Chrono Cross scene.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _image(
|
||||||
|
pathlib.Path(__file__).resolve().parent.joinpath("scenebayer0.png"),
|
||||||
|
"http://bisqwit.iki.fi/jutut/kuvat/ordered_dither/scenebayer0.png",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _image(pth, url):
|
||||||
|
"""Load image specified in ``path``. If not present,
|
||||||
|
fetch it from ``url`` and store locally.
|
||||||
|
|
||||||
|
:param str or :class:`~pathlib.Path` pth:
|
||||||
|
:param str url: URL from where to fetch the image.
|
||||||
|
:return: The :class:`~PIL.Image` requested.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if pth.exists():
|
||||||
|
return Image.open(str(pth))
|
||||||
|
else:
|
||||||
|
r = urlopen(url)
|
||||||
|
with open(str(pth), "wb") as f:
|
||||||
|
f.write(r.read())
|
||||||
|
return _image(pth, url)
|
||||||
|
|
||||||
|
|
||||||
|
def palette():
|
||||||
|
return [
|
||||||
|
0x080000,
|
||||||
|
0x201A0B,
|
||||||
|
0x432817,
|
||||||
|
0x492910,
|
||||||
|
0x234309,
|
||||||
|
0x5D4F1E,
|
||||||
|
0x9C6B20,
|
||||||
|
0xA9220F,
|
||||||
|
0x2B347C,
|
||||||
|
0x2B7409,
|
||||||
|
0xD0CA40,
|
||||||
|
0xE8A077,
|
||||||
|
0x6A94AB,
|
||||||
|
0xD5C4B3,
|
||||||
|
0xFCE76E,
|
||||||
|
0xFCFAE2,
|
||||||
|
]
|
BIN
hitherdither/data/__init__.pyc
Normal file
BIN
hitherdither/data/__pycache__/__init__.cpython-38.pyc
Normal file
193
hitherdither/diffusion.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
:mod:`diffusion`
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. moduleauthor:: hbldh <henrik.blidh@swedwise.com>
|
||||||
|
Created on 2016-09-12, 11:34
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_DIFFUSION_MAPS = {
|
||||||
|
"floyd-steinberg": (
|
||||||
|
(1, 0, 7 / 16),
|
||||||
|
(-1, 1, 3 / 16),
|
||||||
|
(0, 1, 5 / 16),
|
||||||
|
(1, 1, 1 / 16),
|
||||||
|
),
|
||||||
|
"atkinson": (
|
||||||
|
(1, 0, 1 / 8),
|
||||||
|
(2, 0, 1 / 8),
|
||||||
|
(-1, 1, 1 / 8),
|
||||||
|
(0, 1, 1 / 8),
|
||||||
|
(1, 1, 1 / 8),
|
||||||
|
(0, 2, 1 / 8),
|
||||||
|
),
|
||||||
|
"jarvis-judice-ninke": (
|
||||||
|
(1, 0, 7 / 48),
|
||||||
|
(2, 0, 5 / 48),
|
||||||
|
(-2, 1, 3 / 48),
|
||||||
|
(-1, 1, 5 / 48),
|
||||||
|
(0, 1, 7 / 48),
|
||||||
|
(1, 1, 5 / 48),
|
||||||
|
(2, 1, 3 / 48),
|
||||||
|
(-2, 2, 1 / 48),
|
||||||
|
(-1, 2, 3 / 48),
|
||||||
|
(0, 2, 5 / 48),
|
||||||
|
(1, 2, 3 / 48),
|
||||||
|
(2, 2, 1 / 48),
|
||||||
|
),
|
||||||
|
"stucki": (
|
||||||
|
(1, 0, 8 / 42),
|
||||||
|
(2, 0, 4 / 42),
|
||||||
|
(-2, 1, 2 / 42),
|
||||||
|
(-1, 1, 4 / 42),
|
||||||
|
(0, 1, 8 / 42),
|
||||||
|
(1, 1, 4 / 42),
|
||||||
|
(2, 1, 2 / 42),
|
||||||
|
(-2, 2, 1 / 42),
|
||||||
|
(-1, 2, 2 / 42),
|
||||||
|
(0, 2, 4 / 42),
|
||||||
|
(1, 2, 2 / 42),
|
||||||
|
(2, 2, 1 / 42),
|
||||||
|
),
|
||||||
|
"burkes": (
|
||||||
|
(1, 0, 8 / 32),
|
||||||
|
(2, 0, 4 / 32),
|
||||||
|
(-2, 1, 2 / 32),
|
||||||
|
(-1, 1, 4 / 32),
|
||||||
|
(0, 1, 8 / 32),
|
||||||
|
(1, 1, 4 / 32),
|
||||||
|
(2, 1, 2 / 32),
|
||||||
|
),
|
||||||
|
"sierra3": (
|
||||||
|
(1, 0, 5 / 32),
|
||||||
|
(2, 0, 3 / 32),
|
||||||
|
(-2, 1, 2 / 32),
|
||||||
|
(-1, 1, 4 / 32),
|
||||||
|
(0, 1, 5 / 32),
|
||||||
|
(1, 1, 4 / 32),
|
||||||
|
(2, 1, 2 / 32),
|
||||||
|
(-1, 2, 2 / 32),
|
||||||
|
(0, 2, 3 / 32),
|
||||||
|
(1, 2, 2 / 32),
|
||||||
|
),
|
||||||
|
"sierra2": (
|
||||||
|
(1, 0, 4 / 16),
|
||||||
|
(2, 0, 3 / 16),
|
||||||
|
(-2, 1, 1 / 16),
|
||||||
|
(-1, 1, 2 / 16),
|
||||||
|
(0, 1, 3 / 16),
|
||||||
|
(1, 1, 2 / 16),
|
||||||
|
(2, 1, 1 / 16),
|
||||||
|
),
|
||||||
|
"sierra-2-4a": (
|
||||||
|
(1, 0, 2 / 4),
|
||||||
|
(-1, 1, 1 / 4),
|
||||||
|
(0, 1, 1 / 4),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def error_diffusion_dithering(image, palette, method="floyd-steinberg", order=2):
|
||||||
|
"""Perform image dithering by error diffusion method.
|
||||||
|
|
||||||
|
.. note:: Error diffusion is totally unoptimized and therefore very slow.
|
||||||
|
It is included more as a reference implementation than as a useful
|
||||||
|
method.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
http://bisqwit.iki.fi/jutut/kuvat/ordered_dither/error_diffusion.txt
|
||||||
|
|
||||||
|
Quantization error of *current* pixel is added to the pixels
|
||||||
|
on the right and below according to the formulas below.
|
||||||
|
This works nicely for most static pictures, but causes
|
||||||
|
an avalanche of jittering artifacts if used in animation.
|
||||||
|
|
||||||
|
Floyd-Steinberg:
|
||||||
|
|
||||||
|
* 7
|
||||||
|
3 5 1 / 16
|
||||||
|
|
||||||
|
Jarvis-Judice-Ninke:
|
||||||
|
|
||||||
|
* 7 5
|
||||||
|
3 5 7 5 3
|
||||||
|
1 3 5 3 1 / 48
|
||||||
|
|
||||||
|
Stucki:
|
||||||
|
|
||||||
|
* 8 4
|
||||||
|
2 4 8 4 2
|
||||||
|
1 2 4 2 1 / 42
|
||||||
|
|
||||||
|
Burkes:
|
||||||
|
|
||||||
|
* 8 4
|
||||||
|
2 4 8 4 2 / 32
|
||||||
|
|
||||||
|
|
||||||
|
Sierra3:
|
||||||
|
|
||||||
|
* 5 3
|
||||||
|
2 4 5 4 2
|
||||||
|
2 3 2 / 32
|
||||||
|
|
||||||
|
Sierra2:
|
||||||
|
|
||||||
|
* 4 3
|
||||||
|
1 2 3 2 1 / 16
|
||||||
|
|
||||||
|
Sierra-2-4A:
|
||||||
|
|
||||||
|
* 2
|
||||||
|
1 1 / 4
|
||||||
|
|
||||||
|
Stevenson-Arce:
|
||||||
|
|
||||||
|
* . 32
|
||||||
|
12 . 26 . 30 . 16
|
||||||
|
. 12 . 26 . 12 .
|
||||||
|
5 . 12 . 12 . 5 / 200
|
||||||
|
|
||||||
|
Atkinson:
|
||||||
|
|
||||||
|
* 1 1 / 8
|
||||||
|
1 1 1
|
||||||
|
1
|
||||||
|
|
||||||
|
:param :class:`PIL.Image` image: The image to apply error
|
||||||
|
diffusion dithering to.
|
||||||
|
:param :class:`~hitherdither.colour.Palette` palette: The palette to use.
|
||||||
|
:param str method: The error diffusion map to use.
|
||||||
|
:param int order: Metric parameter ``ord`` to send to
|
||||||
|
:method:`numpy.linalg.norm`.
|
||||||
|
:return: The error diffusion dithered PIL image of type
|
||||||
|
"P" using the input palette.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ni = np.array(image, "float")
|
||||||
|
|
||||||
|
diff_map = _DIFFUSION_MAPS.get(method.lower())
|
||||||
|
|
||||||
|
for y in range(ni.shape[0]):
|
||||||
|
for x in range(ni.shape[1]):
|
||||||
|
old_pixel = ni[y, x]
|
||||||
|
old_pixel[old_pixel < 0.0] = 0.0
|
||||||
|
old_pixel[old_pixel > 255.0] = 255.0
|
||||||
|
new_pixel = palette.pixel_closest_colour(old_pixel, order)
|
||||||
|
quantization_error = old_pixel - new_pixel
|
||||||
|
ni[y, x] = new_pixel
|
||||||
|
for dx, dy, diffusion_coefficient in diff_map:
|
||||||
|
xn, yn = x + dx, y + dy
|
||||||
|
if (0 <= xn < ni.shape[1]) and (0 <= yn < ni.shape[0]):
|
||||||
|
ni[yn, xn] += quantization_error * diffusion_coefficient
|
||||||
|
return palette.create_PIL_png_from_rgb_array(np.array(ni, "uint8"))
|
21
hitherdither/exceptions.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
exceptions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2017-05-10 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
class HitherDitherError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PaletteCouldNotBeCreatedError(Exception):
|
||||||
|
pass
|
0
hitherdither/math/__init__.py
Normal file
BIN
hitherdither/math/__pycache__/__init__.cpython-38.pyc
Normal file
3
hitherdither/ordered/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import bayer
|
||||||
|
from . import yliluoma
|
||||||
|
from . import cluster
|
BIN
hitherdither/ordered/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
hitherdither/ordered/__pycache__/bayer.cpython-38.pyc
Normal file
BIN
hitherdither/ordered/__pycache__/cluster.cpython-38.pyc
Normal file
88
hitherdither/ordered/bayer.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
bayer_dithering
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2016-09-09 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def B(n, transposed=False):
|
||||||
|
"""Get the Bayer matrix with side of length ``n``.
|
||||||
|
|
||||||
|
Will only work if ``n`` is a power of 2.
|
||||||
|
|
||||||
|
Reference: http://caca.zoy.org/study/part2.html
|
||||||
|
|
||||||
|
:param int n: Power of 2 side length of matrix.
|
||||||
|
:return: The Bayer matrix.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return (1 + I(n, transposed)) / (1 + (n * n))
|
||||||
|
|
||||||
|
|
||||||
|
def I(n, transposed=False):
|
||||||
|
"""Get the index matrix with side of length ``n``.
|
||||||
|
|
||||||
|
Will only work if ``n`` is a power of 2.
|
||||||
|
|
||||||
|
Reference: http://caca.zoy.org/study/part2.html
|
||||||
|
|
||||||
|
:param int n: Power of 2 side length of matrix.
|
||||||
|
:param bool transposed:
|
||||||
|
:return: The index matrix.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if n == 2:
|
||||||
|
if transposed:
|
||||||
|
return np.array([[0, 3], [2, 1]], "int")
|
||||||
|
else:
|
||||||
|
return np.array([[0, 2], [3, 1]], "int")
|
||||||
|
else:
|
||||||
|
smaller_I = I(n >> 1, transposed)
|
||||||
|
if transposed:
|
||||||
|
return np.bmat(
|
||||||
|
[
|
||||||
|
[4 * smaller_I, 4 * smaller_I + 3],
|
||||||
|
[4 * smaller_I + 2, 4 * smaller_I + 1],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return np.bmat(
|
||||||
|
[
|
||||||
|
[4 * smaller_I, 4 * smaller_I + 2],
|
||||||
|
[4 * smaller_I + 3, 4 * smaller_I + 1],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bayer_dithering(image, palette, thresholds, order=8):
|
||||||
|
"""Render the image using the ordered Bayer matrix dithering pattern.
|
||||||
|
|
||||||
|
:param :class:`PIL.Image` image: The image to apply
|
||||||
|
Bayer ordered dithering to.
|
||||||
|
:param :class:`~hitherdither.colour.Palette` palette: The palette to use.
|
||||||
|
:param thresholds: Thresholds to apply dithering at.
|
||||||
|
:param int order: The size of the Bayer matrix.
|
||||||
|
:return: The Bayer matrix dithered PIL image of type "P"
|
||||||
|
using the input palette.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bayer_matrix = B(order)
|
||||||
|
ni = np.array(image, "uint8")
|
||||||
|
thresholds = np.array(thresholds, "uint8")
|
||||||
|
xx, yy = np.meshgrid(range(ni.shape[1]), range(ni.shape[0]))
|
||||||
|
xx %= order
|
||||||
|
yy %= order
|
||||||
|
factor_threshold_matrix = np.expand_dims(bayer_matrix[yy, xx], axis=2) * thresholds
|
||||||
|
new_image = ni + factor_threshold_matrix
|
||||||
|
return palette.create_PIL_png_from_rgb_array(new_image)
|
67
hitherdither/ordered/cluster.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
bayer_dithering
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2016-09-09 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_CLUSTER_DOT_MATRICES = {
|
||||||
|
4: np.array([[12, 5, 6, 13], [4, 0, 1, 7], [11, 3, 2, 8], [15, 10, 9, 14]], "float")
|
||||||
|
/ 16.0,
|
||||||
|
8: np.array(
|
||||||
|
[
|
||||||
|
[24, 10, 12, 26, 35, 47, 49, 37],
|
||||||
|
[8, 0, 2, 14, 45, 59, 61, 51],
|
||||||
|
[22, 6, 4, 16, 43, 57, 63, 53],
|
||||||
|
[30, 20, 18, 28, 33, 41, 55, 39],
|
||||||
|
[34, 46, 48, 36, 25, 11, 13, 27],
|
||||||
|
[44, 57, 60, 50, 9, 1, 3, 15],
|
||||||
|
[42, 56, 62, 52, 23, 7, 5, 17],
|
||||||
|
[32, 40, 54, 38, 31, 21, 19, 29],
|
||||||
|
],
|
||||||
|
"float",
|
||||||
|
)
|
||||||
|
/ 64.0,
|
||||||
|
(5, 3): np.array([[9, 3, 0, 6, 12], [10, 4, 1, 7, 13], [11, 5, 2, 8, 14]], "float")
|
||||||
|
/ 15.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cluster_dot_dithering(image, palette, thresholds, order=4):
|
||||||
|
"""Render the image using the ordered Bayer matrix dithering pattern.
|
||||||
|
|
||||||
|
Reference: http://caca.zoy.org/study/part2.html
|
||||||
|
|
||||||
|
:param :class:`PIL.Image` image: The image to apply the
|
||||||
|
ordered dithering to.
|
||||||
|
:param :class:`~hitherdither.colour.Palette` palette: The palette to use.
|
||||||
|
:param thresholds: Thresholds to apply dithering at.
|
||||||
|
:param int order: The size of the Bayer matrix.
|
||||||
|
:return: The Bayer matrix dithered PIL image of type "P"
|
||||||
|
using the input palette.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
cluster_dot_matrix = _CLUSTER_DOT_MATRICES.get(order)
|
||||||
|
if cluster_dot_matrix is None:
|
||||||
|
raise NotImplementedError("Only order 4 and 8 is implemented as of yet.")
|
||||||
|
ni = np.array(image, "uint8")
|
||||||
|
thresholds = np.array(thresholds, "uint8")
|
||||||
|
xx, yy = np.meshgrid(range(ni.shape[1]), range(ni.shape[0]))
|
||||||
|
xx %= order
|
||||||
|
yy %= order
|
||||||
|
factor_threshold_matrix = (
|
||||||
|
np.expand_dims(cluster_dot_matrix[yy, xx], axis=2) * thresholds
|
||||||
|
)
|
||||||
|
new_image = ni + factor_threshold_matrix
|
||||||
|
return palette.create_PIL_png_from_rgb_array(new_image)
|
1
hitherdither/ordered/yliluoma/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from ._algorithm_one import yliluomas_1_ordered_dithering
|
BIN
hitherdither/ordered/yliluoma/__pycache__/_utils.cpython-38.pyc
Normal file
180
hitherdither/ordered/yliluoma/_algorithm_one.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
algorithm_one
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2016-09-12 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ._utils import color_compare, CCIR_LUMINOSITY
|
||||||
|
from ..bayer import I
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mixing_plan_matrix(palette, order=8):
|
||||||
|
mixing_matrix = []
|
||||||
|
colours = {}
|
||||||
|
colour_component_distances = []
|
||||||
|
|
||||||
|
nn = order * order
|
||||||
|
for i in range(len(palette)):
|
||||||
|
for j in range(i, len(palette)):
|
||||||
|
for ratio in range(0, nn):
|
||||||
|
if i == j and ratio != 0:
|
||||||
|
break
|
||||||
|
# Determine the two component colors.
|
||||||
|
c_mix = _colour_combine(palette, i, j, ratio / nn)
|
||||||
|
hex_colour = palette.rgb2hex(*c_mix.tolist())
|
||||||
|
colours[hex_colour] = (i, j, ratio / nn)
|
||||||
|
mixing_matrix.append(c_mix)
|
||||||
|
|
||||||
|
c1 = np.array(palette[i], "int")
|
||||||
|
c2 = np.array(palette[j], "int")
|
||||||
|
cmpval = (
|
||||||
|
color_compare(c1, c2)
|
||||||
|
* 0.1
|
||||||
|
* (np.abs((ratio / float(nn)) - 0.5) + 0.5)
|
||||||
|
)
|
||||||
|
colour_component_distances.append(cmpval)
|
||||||
|
|
||||||
|
mixing_matrix = np.array(mixing_matrix)
|
||||||
|
colour_component_distances = np.array(colour_component_distances)
|
||||||
|
|
||||||
|
for c in mixing_matrix:
|
||||||
|
assert palette.rgb2hex(*c.tolist()) in colours
|
||||||
|
|
||||||
|
return mixing_matrix, colours, colour_component_distances
|
||||||
|
|
||||||
|
|
||||||
|
def _colour_combine(palette, i, j, ratio):
|
||||||
|
c1, c2 = np.array(palette[i], "int"), np.array(palette[j], "int")
|
||||||
|
return np.array(c1 + ratio * (c2 - c1), "uint8")
|
||||||
|
|
||||||
|
|
||||||
|
def _improved_mixing_error_fcn(
|
||||||
|
colour, mixing_matrix, colour_component_distances, luma_mat=None
|
||||||
|
):
|
||||||
|
"""Compares two colours using the Psychovisual model.
|
||||||
|
|
||||||
|
The simplest way to adjust the psychovisual model is to
|
||||||
|
add some code that considers the difference between the
|
||||||
|
two pixel values that are being mixed in the dithering
|
||||||
|
process, and penalizes combinations that differ too much.
|
||||||
|
|
||||||
|
Wikipedia has an entire article about the topic of comparing
|
||||||
|
two color values. Most of the improved color comparison
|
||||||
|
functions are based on the CIE colorspace, but simple
|
||||||
|
improvements can be done in the RGB space too. Such a simple
|
||||||
|
improvement is shown below. We might call this RGBL, for
|
||||||
|
luminance-weighted RGB.
|
||||||
|
|
||||||
|
:param :class:`numpy.ndarray` colour: The colour to estimate error to.
|
||||||
|
:param :class:`numpy.ndarray` mixing_matrix: The rgb
|
||||||
|
values of mixed colours.
|
||||||
|
:param :class:`numpy.ndarray` colour_component_distances: The colour
|
||||||
|
distance of the mixed colours.
|
||||||
|
:return: :class:`numpy.ndarray`
|
||||||
|
|
||||||
|
"""
|
||||||
|
colour = np.array(colour, "int")
|
||||||
|
if luma_mat is None:
|
||||||
|
luma_mat = mixing_matrix.dot(CCIR_LUMINOSITY / 1000.0 / 255.0)
|
||||||
|
luma_colour = colour.dot(CCIR_LUMINOSITY) / (255.0 * 1000.0)
|
||||||
|
luma_diff_squared = (luma_mat - luma_colour) ** 2
|
||||||
|
diff_colour_squared = ((colour - mixing_matrix) / 255.0) ** 2
|
||||||
|
cmpvals = diff_colour_squared.dot(CCIR_LUMINOSITY) / 1000.0
|
||||||
|
cmpvals *= 0.75
|
||||||
|
cmpvals += luma_diff_squared
|
||||||
|
cmpvals += colour_component_distances
|
||||||
|
return cmpvals
|
||||||
|
|
||||||
|
|
||||||
|
def yliluomas_1_ordered_dithering(image, palette, order=8):
|
||||||
|
"""A dithering method that weighs in color combinations of palette.
|
||||||
|
|
||||||
|
N.B. tri-tone dithering is not implemented.
|
||||||
|
|
||||||
|
:param :class:`PIL.Image` image: The image to apply
|
||||||
|
Bayer ordered dithering to.
|
||||||
|
:param :class:`~hitherdither.colour.Palette` palette: The palette to use.
|
||||||
|
:param int order: The Bayer matrix size to use.
|
||||||
|
:return: The dithered PIL image of type "P" using the input palette.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bayer_matrix = I(order, transposed=True) / 64.0
|
||||||
|
ni = np.array(image, "uint8")
|
||||||
|
xx, yy = np.meshgrid(range(ni.shape[1]), range(ni.shape[0]))
|
||||||
|
factor_matrix = bayer_matrix[yy % order, xx % order]
|
||||||
|
|
||||||
|
# Prepare all precalculated mixed colours and their respective
|
||||||
|
mixing_matrix, colour_map, colour_component_distances = _get_mixing_plan_matrix(
|
||||||
|
palette
|
||||||
|
)
|
||||||
|
mixing_matrix = np.array(mixing_matrix, "int")
|
||||||
|
luma_mat = mixing_matrix.dot(CCIR_LUMINOSITY / 1000.0 / 255.0)
|
||||||
|
|
||||||
|
color_matrix = np.zeros(ni.shape[:2], dtype="uint8")
|
||||||
|
for x, y in zip(np.nditer(xx), np.nditer(yy)):
|
||||||
|
|
||||||
|
min_index = np.argmin(
|
||||||
|
_improved_mixing_error_fcn(
|
||||||
|
ni[y, x, :], mixing_matrix, colour_component_distances, luma_mat
|
||||||
|
)
|
||||||
|
)
|
||||||
|
closest_mix_colour = mixing_matrix[min_index, :].tolist()
|
||||||
|
closest_mix_hexcolour = palette.rgb2hex(*closest_mix_colour)
|
||||||
|
plan = colour_map.get(closest_mix_hexcolour)
|
||||||
|
color_matrix[y, x] = plan[1] if (factor_matrix[y, x] < plan[-1]) else plan[0]
|
||||||
|
|
||||||
|
return palette.create_PIL_png_from_closest_colour(color_matrix)
|
||||||
|
|
||||||
|
|
||||||
|
def _evaluate_mixing_error(
|
||||||
|
desired_colour,
|
||||||
|
mixed_colour,
|
||||||
|
component_colour_1,
|
||||||
|
component_colour_2,
|
||||||
|
ratio,
|
||||||
|
component_colour_compare_value=None,
|
||||||
|
):
|
||||||
|
"""Compare colours and weigh in component difference.
|
||||||
|
|
||||||
|
double EvaluateMixingError(int r,int g,int b,
|
||||||
|
int r0,int g0,int b0,
|
||||||
|
int r1,int g1,int b1,
|
||||||
|
int r2,int g2,int b2,
|
||||||
|
double ratio)
|
||||||
|
{
|
||||||
|
return ColorCompare(r,g,b, r0,g0,b0)
|
||||||
|
+ ColorCompare(r1,g1,b1, r2,g2,b2) * 0.1
|
||||||
|
* (fabs(ratio-0.5)+0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:param desired_colour:
|
||||||
|
:param mixed_colour:
|
||||||
|
:param component_colour_1:
|
||||||
|
:param component_colour_2:
|
||||||
|
:param ratio:
|
||||||
|
:param component_colour_compare_value:
|
||||||
|
:return:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if component_colour_compare_value is None:
|
||||||
|
return color_compare(desired_colour, mixed_colour) + (
|
||||||
|
color_compare(component_colour_1, component_colour_2)
|
||||||
|
* 0.1
|
||||||
|
* (np.abs(ratio - 0.5) + 0.5)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
color_compare(desired_colour, mixed_colour) + component_colour_compare_value
|
||||||
|
)
|
42
hitherdither/ordered/yliluoma/_utils.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
_utils
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2016-09-23 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# CCIR 601 luminosity
|
||||||
|
CCIR_LUMINOSITY = np.array([299.0, 587.0, 114.0])
|
||||||
|
|
||||||
|
|
||||||
|
def color_compare(c1, c2):
|
||||||
|
"""Compare the difference of two RGB values, weigh by CCIR 601 luminosity
|
||||||
|
|
||||||
|
double ColorCompare(int r1,int g1,int b1, int r2,int g2,int b2)
|
||||||
|
{
|
||||||
|
double luma1 = (r1*299 + g1*587 + b1*114) / (255.0*1000);
|
||||||
|
double luma2 = (r2*299 + g2*587 + b2*114) / (255.0*1000);
|
||||||
|
double lumadiff = luma1-luma2;
|
||||||
|
double diffR = (r1-r2)/255.0, diffG = (g1-g2)/255.0, diffB = (b1-b2)/255.0;
|
||||||
|
return (diffR*diffR*0.299 + diffG*diffG*0.587 + diffB*diffB*0.114)*0.75
|
||||||
|
+ lumadiff*lumadiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:return: float
|
||||||
|
|
||||||
|
"""
|
||||||
|
luma_diff = c1.dot(CCIR_LUMINOSITY) / (255.0 * 1000.0) - c2.dot(CCIR_LUMINOSITY) / (
|
||||||
|
255.0 * 1000.0
|
||||||
|
)
|
||||||
|
diff_col = (c1 - c2) / 255.0
|
||||||
|
return ((diff_col ** 2).dot(CCIR_LUMINOSITY / 1000.0) * 0.75) + (luma_diff ** 2)
|
246
hitherdither/palette.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
palette
|
||||||
|
-----------
|
||||||
|
|
||||||
|
:copyright: 2016-09-09 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.ImagePalette import ImagePalette
|
||||||
|
|
||||||
|
from hitherdither.exceptions import PaletteCouldNotBeCreatedError
|
||||||
|
|
||||||
|
try:
|
||||||
|
string_type = basestring
|
||||||
|
except NameError:
|
||||||
|
string_type = str
|
||||||
|
|
||||||
|
|
||||||
|
def hex2rgb(h):
|
||||||
|
if isinstance(h, string_type):
|
||||||
|
return hex2rgb(int(h[1:] if h.startswith("#") else h, 16))
|
||||||
|
return (h >> 16) & 0xFF, (h >> 8) & 0xFF, h & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def rgb2hex(r, g, b):
|
||||||
|
return (r << 16) + (g << 8) + b
|
||||||
|
|
||||||
|
|
||||||
|
def _get_all_present_colours(im):
|
||||||
|
"""Returns a dict of RGB colours present.
|
||||||
|
|
||||||
|
N.B. Do not use this except for testing purposes.
|
||||||
|
|
||||||
|
Reference: http://stackoverflow.com/a/4643911
|
||||||
|
|
||||||
|
:param im: The image to get number of colours in.
|
||||||
|
:type im: :class:`~PIL.Image.Image`
|
||||||
|
:return: A dict of contained RGB colours as keys.
|
||||||
|
:rtype: dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
by_color = defaultdict(int)
|
||||||
|
for pixel in im.getdata():
|
||||||
|
by_color[pixel] += 1
|
||||||
|
return by_color
|
||||||
|
|
||||||
|
|
||||||
|
class Palette(object):
|
||||||
|
"""The :mod:`~hitherdither` implementation of a colour palette.
|
||||||
|
|
||||||
|
Can be instantiated in from colour specifications in the following forms:
|
||||||
|
|
||||||
|
- ``uint8`` numpy array of size ``[N x 3]``
|
||||||
|
- ``uint8`` numpy array of size ``[3N]``
|
||||||
|
- :class:`~PIL.ImagePalette.ImagePalette`
|
||||||
|
- :class:`~PIL.Image.Image`
|
||||||
|
- list of hex values
|
||||||
|
- list of RGB tuples
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
if isinstance(data, np.ndarray):
|
||||||
|
if data.ndim == 1:
|
||||||
|
self.colours = data.reshape((3, len(data) // 3))
|
||||||
|
else:
|
||||||
|
self.colours = data
|
||||||
|
self.hex = [rgb2hex(*colour) for colour in data]
|
||||||
|
elif isinstance(data, ImagePalette):
|
||||||
|
_tmp = np.frombuffer(data.palette, "uint8")
|
||||||
|
self.colours = _tmp.reshape((3, len(_tmp) // 3))
|
||||||
|
self.hex = [rgb2hex(*colour) for colour in data]
|
||||||
|
elif isinstance(data, Image.Image):
|
||||||
|
if data.palette is None:
|
||||||
|
raise PaletteCouldNotBeCreatedError(
|
||||||
|
"Image of mode {0} has no PIL palette. "
|
||||||
|
"Make sure it is of mode P.".format(data.mode)
|
||||||
|
)
|
||||||
|
_colours = data.getcolors()
|
||||||
|
_n_colours = len(_colours)
|
||||||
|
_tmp = np.array(data.getpalette())[: 3 * _n_colours]
|
||||||
|
self.colours = _tmp.reshape((3, len(_tmp) // 3)).T
|
||||||
|
self.hex = [rgb2hex(*colour) for colour in self]
|
||||||
|
elif isinstance(data, (list, tuple)):
|
||||||
|
if isinstance(data[0], string_type):
|
||||||
|
# Assume hex strings
|
||||||
|
self.hex = data
|
||||||
|
self.colours = np.array([hex2rgb(c) for c in data])
|
||||||
|
elif isinstance(data[0], int):
|
||||||
|
# Assume hex values
|
||||||
|
self.hex = data # TODO: Convert to hex string.
|
||||||
|
self.colours = np.array([hex2rgb(c) for c in data])
|
||||||
|
else:
|
||||||
|
# Assume RGB tuples
|
||||||
|
self.colours = np.array(data)
|
||||||
|
self.hex = [rgb2hex(*colour) for colour in data]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for colour in self.colours:
|
||||||
|
yield colour
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.colours.shape[0]
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if isinstance(item, int):
|
||||||
|
return self.colours[item, :]
|
||||||
|
else:
|
||||||
|
raise IndexError("Can only reference colours by integer values.")
|
||||||
|
|
||||||
|
def render(self, colours):
|
||||||
|
return np.array(np.take(self.colours, colours, axis=0), "uint8")
|
||||||
|
|
||||||
|
def image_distance(self, image, order=2):
|
||||||
|
ni = np.array(image, "float")
|
||||||
|
distances = np.zeros((ni.shape[0], ni.shape[1], len(self)), "float")
|
||||||
|
for i, colour in enumerate(self):
|
||||||
|
distances[:, :, i] = np.linalg.norm(ni - colour, ord=order, axis=2)
|
||||||
|
return distances
|
||||||
|
|
||||||
|
def image_closest_colour(self, image, order=2):
|
||||||
|
return np.argmin(self.image_distance(image, order=order), axis=2)
|
||||||
|
|
||||||
|
def pixel_distance(self, pixel, order=2):
|
||||||
|
return np.array([np.linalg.norm(pixel - colour, ord=order) for colour in self])
|
||||||
|
|
||||||
|
def pixel_closest_colour(self, pixel, order=2):
|
||||||
|
return self.colours[
|
||||||
|
np.argmin(self.pixel_distance(pixel, order=order)), :
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_by_kmeans(cls, image):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_by_median_cut(cls, image, n=16, dim=None):
|
||||||
|
img = np.array(image)
|
||||||
|
# Create pixel buckets to simplify sorting and splitting.
|
||||||
|
if img.ndim == 3:
|
||||||
|
pixels = img.reshape((img.shape[0] * img.shape[1], img.shape[2]))
|
||||||
|
elif img.ndim == 2:
|
||||||
|
pixels = img.reshape((img.shape[0] * img.shape[1], 1))
|
||||||
|
|
||||||
|
def median_cut(p, dim=None):
|
||||||
|
"""Median cut method.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
https://en.wikipedia.org/wiki/Median_cut
|
||||||
|
|
||||||
|
:param p: The pixel array to split in two.
|
||||||
|
:return: Two numpy arrays, split by median cut method.
|
||||||
|
"""
|
||||||
|
if dim is not None:
|
||||||
|
sort_dim = dim
|
||||||
|
else:
|
||||||
|
mins = p.min(axis=0)
|
||||||
|
maxs = p.max(axis=0)
|
||||||
|
sort_dim = np.argmax(maxs - mins)
|
||||||
|
|
||||||
|
argument = np.argsort(p[:, sort_dim])
|
||||||
|
p = p[argument, :]
|
||||||
|
m = np.median(p[:, sort_dim])
|
||||||
|
split_mask = p[:, sort_dim] >= m
|
||||||
|
return [p[~split_mask, :].copy(), p[split_mask, :].copy()]
|
||||||
|
|
||||||
|
# Do actual splitting loop.
|
||||||
|
bins = [
|
||||||
|
pixels,
|
||||||
|
]
|
||||||
|
while len(bins) < n:
|
||||||
|
new_bins = []
|
||||||
|
for bin in bins:
|
||||||
|
new_bins += median_cut(bin, dim)
|
||||||
|
bins = new_bins
|
||||||
|
|
||||||
|
# Average over pixels in each bin to create
|
||||||
|
colours = np.array(
|
||||||
|
[np.array(bin.mean(axis=0).round(), "uint8") for bin in bins], "uint8"
|
||||||
|
)
|
||||||
|
return cls(colours)
|
||||||
|
|
||||||
|
def create_PIL_png_from_closest_colour(self, cc):
|
||||||
|
"""Create a ``P`` PIL image with this palette.
|
||||||
|
|
||||||
|
Avoids the PIL dithering in favour of our own.
|
||||||
|
|
||||||
|
Reference: http://stackoverflow.com/a/29438149
|
||||||
|
|
||||||
|
:param :class:`numpy.ndarray` cc: A ``[M x N]`` array with integer
|
||||||
|
values representing palette colour indices to build image from.
|
||||||
|
:return: A :class:`PIL.Image.Image` image of mode ``P``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pa_image = Image.new("P", cc.shape[::-1])
|
||||||
|
pa_image.putpalette(self.colours.flatten().tolist())
|
||||||
|
im = Image.fromarray(np.array(cc, "uint8")).im.convert("P", 0, pa_image.im)
|
||||||
|
try:
|
||||||
|
# Pillow >= 4
|
||||||
|
return pa_image._new(im)
|
||||||
|
except AttributeError:
|
||||||
|
# Pillow < 4
|
||||||
|
return pa_image._makeself(im)
|
||||||
|
|
||||||
|
def create_PIL_png_from_rgb_array(self, img_array):
|
||||||
|
"""Create a ``P`` PIL image from a RGB image with this palette.
|
||||||
|
|
||||||
|
Avoids the PIL dithering in favour of our own.
|
||||||
|
|
||||||
|
Reference: http://stackoverflow.com/a/29438149
|
||||||
|
|
||||||
|
:param :class:`numpy.ndarray` img_array: A ``[M x N x 3]`` uint8
|
||||||
|
array representing RGB colours.
|
||||||
|
:return: A :class:`PIL.Image.Image` image of mode ``P`` with colours
|
||||||
|
available in this palette.
|
||||||
|
|
||||||
|
"""
|
||||||
|
cc = self.image_closest_colour(img_array, order=2)
|
||||||
|
pa_image = Image.new("P", cc.shape[::-1])
|
||||||
|
pa_image.putpalette(self.colours.flatten().tolist())
|
||||||
|
im = Image.fromarray(np.array(cc, "uint8")).im.convert("P", 0, pa_image.im)
|
||||||
|
try:
|
||||||
|
# Pillow >= 4
|
||||||
|
return pa_image._new(im)
|
||||||
|
except AttributeError:
|
||||||
|
# Pillow < 4
|
||||||
|
return pa_image._makeself(im)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hex2rgb(x):
|
||||||
|
return hex2rgb(x)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rgb2hex(r, g, b):
|
||||||
|
return rgb2hex(r, g, b)
|
27
hitherdither/utils.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
:mod:`utils`
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. moduleauthor:: hbldh <henrik.blidh@swedwise.com>
|
||||||
|
Created on 2016-09-12, 09:50
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def np2pil(img):
|
||||||
|
return Image.fromarray(np.array(img, "uint8"))
|
||||||
|
|
||||||
|
|
||||||
|
def pil2np(img):
|
||||||
|
return np.array(img, "uint8")
|
BIN
images/favicon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
images/fonk.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
images/git.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
images/nitter.png
Normal file
After Width: | Height: | Size: 209 KiB |
BIN
images/piped.png
Normal file
After Width: | Height: | Size: 267 KiB |
BIN
images/rd.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
images/rss.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
images/teddit.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
images/the_droth_logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
images/thedroth_logo.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
images/tmp/fonk.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
images/tmp/nitter.png
Normal file
After Width: | Height: | Size: 551 KiB |
BIN
images/tmp/piped.png
Normal file
After Width: | Height: | Size: 749 KiB |
BIN
images/tmp/rd.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
images/tmp/teddit.png
Normal file
After Width: | Height: | Size: 570 KiB |
BIN
images/tmp/thedroth_logo.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
images/tmp/wikiless.png
Normal file
After Width: | Height: | Size: 400 KiB |
BIN
images/wikiless.png
Normal file
After Width: | Height: | Size: 209 KiB |
196
index.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head profile="https://thedroth.rocks">
|
||||||
|
<link rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="favicon.png" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>TheDroth Rocks! </title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="css/picnic.min.css">
|
||||||
|
<style>
|
||||||
|
aside a.top {
|
||||||
|
font-size: 0;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 180px;
|
||||||
|
padding: .6em 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: .3em .3em 0 0;
|
||||||
|
transition: all .3s ease;
|
||||||
|
}
|
||||||
|
aside a.top.visible {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
aside .links a.button {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 1000px) {
|
||||||
|
aside a.pseudo.top {
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.documentation article > h2 {
|
||||||
|
margin: -2em 0 .6em;
|
||||||
|
padding: 3em 0 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.documentation article > h3 {
|
||||||
|
margin-bottom: .6em;
|
||||||
|
}
|
||||||
|
.documentation aside h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 1.25em 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.documentation aside a.pseudo {
|
||||||
|
color: #0074D9;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.documentation > section {
|
||||||
|
background: #fff;
|
||||||
|
text-align: left;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 80px 0 0;
|
||||||
|
}
|
||||||
|
.documentation article > h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.6em 0;
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.documentation aside a.button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.documentation pre[class*="language-"] {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.documentation .index pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="fedi">
|
||||||
|
<!-- <font size="1.5em"> -->
|
||||||
|
<a href="https://thedroth.rocks" class="brand">
|
||||||
|
<img class="logo" src="dithers/thedroth_logo.png" />
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- responsive-->
|
||||||
|
<input id="bmenub" type="checkbox" class="show">
|
||||||
|
<label for="bmenub" class="burger pseudo button">меню</label>
|
||||||
|
|
||||||
|
<div class="menu">
|
||||||
|
<a href="index.html#proxy" class="pseudo button">Проксирующие сервисы</a>
|
||||||
|
<a href="index.html#selfhosted" class="pseudo button">Собственные сервисы</a>
|
||||||
|
<a href="index.html#blog" class="pseudo button">TheДротский бложик</a>
|
||||||
|
</div>
|
||||||
|
<!-- </font> -->
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main id="home" class="documentation">
|
||||||
|
<section class="flex">
|
||||||
|
<article class="card four-fifth-1000">
|
||||||
|
<header>TheDrothские сервисы</header>
|
||||||
|
<section class="flex">
|
||||||
|
<article class="card">
|
||||||
|
<center>
|
||||||
|
<header id="proxy">Проксирующие</header>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>Piped</h4>
|
||||||
|
<p>Лёгкий приватный интерфейс для YouTube, умеюший автоматически проматывать рекламные вставки. Зачем кормить гугл, если можно не кормить...</p>
|
||||||
|
<a href="https://piped.thedroth.rocks"><img src="dithers/piped.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>Teddit</h4>
|
||||||
|
<p>Быстрый и лёгкий фронтэнд для Reddit</p>
|
||||||
|
<a href="https://teddit.thedroth.rocks"><img src="dithers/teddit.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
<!-- <hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>Nitter</h4>
|
||||||
|
<p>Интерфейс для Twitter, избавленный от всего лишнего</p>
|
||||||
|
<a href="https://nitter.thedroth.rocks"><img src="dithers/nitter.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>Rural Dictionary</h4>
|
||||||
|
<p>Фронтэнд для Urban Dictionary. Знакомься с современным фольклором и сленгом без корпоративной слежки!</p>
|
||||||
|
<a href="https://rd.thedroth.rocks"><img src="dithers/rd.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>WikiLess</h4>
|
||||||
|
<p>Wikipedia без ненужных элементов и трекеров</p>
|
||||||
|
<a href="https://wiki.thedroth.rocks"><img src="dithers/wikiless.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<section class="flex">
|
||||||
|
<article class="card">
|
||||||
|
<center>
|
||||||
|
<header id="selfhosted">Собственные</header>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>Git</h4>
|
||||||
|
<p>Хранилище исходных кодов</p>
|
||||||
|
<a href="https://git.thedroth.rocks"><img src="dithers/git.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>TheДротский Фонк</h4>
|
||||||
|
<p>FunkWhale — хостинг музыки и подкастов</p>
|
||||||
|
<a href="https://fonk.thedroth.rocks"><img src="dithers/fonk.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="flex">
|
||||||
|
<h4>RSS Bridge</h4>
|
||||||
|
<p>Конвертация новостных лент в RSS</p>
|
||||||
|
<a href="https://rss.thedroth.rocks"><img src="dithers/rss.png" width="60%"></a>
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
<article class="card four-fifth-1000">
|
||||||
|
<section class="flex">
|
||||||
|
<article class="card">
|
||||||
|
<div class="flex" id="blog"> <center>
|
||||||
|
<span>
|
||||||
|
<iframe style="border:none;" width="90%" height="700px" seamless src="https://blog.thedroth.rocks"></iframe> </span>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<section>
|
||||||
|
<hr>
|
||||||
|
<a href="https://gitflic.ru/project/the_sn4il/thedroth-rocks">Исходный код сайта</a>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
js/jquery.min.js
vendored
Normal file
8
js/menu.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
function dropme() {
|
||||||
|
var x = document.getElementById("rtopnav");
|
||||||
|
if (x.className === "topnav") {
|
||||||
|
x.className += " responsive";
|
||||||
|
} else {
|
||||||
|
x.className = "topnav";
|
||||||
|
}
|
||||||
|
}
|
36
js/popup.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
function openModal() {
|
||||||
|
document.getElementById("myModal").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById("myModal").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
var slideIndex = 1;
|
||||||
|
showSlides(slideIndex);
|
||||||
|
|
||||||
|
function plusSlides(n) {
|
||||||
|
showSlides(slideIndex += n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentSlide(n) {
|
||||||
|
showSlides(slideIndex = n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSlides(n) {
|
||||||
|
var i;
|
||||||
|
var slides = document.getElementsByClassName("mySlides");
|
||||||
|
var dots = document.getElementsByClassName("demo");
|
||||||
|
var captionText = document.getElementById("caption");
|
||||||
|
if (n > slides.length) {slideIndex = 1}
|
||||||
|
if (n < 1) {slideIndex = slides.length}
|
||||||
|
for (i = 0; i < slides.length; i++) {
|
||||||
|
slides[i].style.display = "none";
|
||||||
|
}
|
||||||
|
for (i = 0; i < dots.length; i++) {
|
||||||
|
dots[i].className = dots[i].className.replace(" active", "");
|
||||||
|
}
|
||||||
|
slides[slideIndex-1].style.display = "block";
|
||||||
|
dots[slideIndex-1].className += " active";
|
||||||
|
captionText.innerHTML = dots[slideIndex-1].alt;
|
||||||
|
}
|