|
from __future__ import annotations |
|
|
|
from typing import Any, Tuple, List, Union |
|
from numpy.typing import NDArray |
|
|
|
import cv2 |
|
from PIL import Image |
|
import numpy as np |
|
import scipy as sp |
|
|
|
from hakuimg.dither import dithering |
|
|
|
|
|
INFLATE_FILTER = [ |
|
None, |
|
np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], np.uint8), |
|
np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], np.uint8), |
|
np.array( |
|
[ |
|
[0, 0, 1, 0, 0], |
|
[0, 1, 1, 1, 0], |
|
[1, 1, 1, 1, 1], |
|
[0, 1, 1, 1, 0], |
|
[0, 0, 1, 0, 0], |
|
], |
|
np.uint8, |
|
), |
|
np.array( |
|
[ |
|
[1, 1, 1, 1, 1], |
|
[1, 1, 1, 1, 1], |
|
[1, 1, 1, 1, 1], |
|
[1, 1, 1, 1, 1], |
|
[1, 1, 1, 1, 1], |
|
], |
|
np.uint8, |
|
), |
|
np.ones((7, 7), np.uint8), |
|
np.ones((9, 9), np.uint8), |
|
np.ones((11, 11), np.uint8), |
|
np.ones((13, 13), np.uint8), |
|
np.ones((15, 15), np.uint8), |
|
np.ones((17, 17), np.uint8), |
|
] |
|
|
|
|
|
def pil_imgread_img_as_array(img) -> NDArray[Any]: |
|
"""Convert image to RGBA and read to ndarray""" |
|
img = Image.fromarray(img) |
|
img = img.convert("RGBA") |
|
img_arr = np.asarray(img) |
|
return img_arr |
|
|
|
|
|
def preprocess( |
|
img: NDArray[Any], |
|
blur: int = 0, |
|
erode: int = 0, |
|
) -> NDArray[Any]: |
|
""" |
|
Process for |
|
* outline inflation (erode) |
|
* smoothing (blur) |
|
* saturation |
|
* contrast |
|
""" |
|
|
|
if erode: |
|
img = cv2.erode( |
|
img, |
|
INFLATE_FILTER[erode], |
|
iterations=1, |
|
) |
|
|
|
|
|
if blur: |
|
img = cv2.bilateralFilter(img, 15, blur * 20, 20) |
|
img = img.astype(np.float32) |
|
return img |
|
|
|
|
|
def pixelize( |
|
img: Image, |
|
k: int, |
|
c: int, |
|
d_w: int, |
|
d_h: int, |
|
o_w: int, |
|
o_h: int, |
|
precise: int, |
|
mode: str = "dithering", |
|
resize: bool = True, |
|
) -> Tuple[NDArray[Any], NDArray[Any]]: |
|
""" |
|
Use down scale and up scale to make pixel image. |
|
|
|
And use k-means to confine the num of colors. |
|
""" |
|
img = cv2.resize(img, (d_w, d_h), interpolation=cv2.INTER_NEAREST) |
|
|
|
|
|
|
|
if "kmeans" in mode: |
|
criteria = ( |
|
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, |
|
precise * 5, |
|
0.01, |
|
) |
|
img_cp = img.reshape(-1, c) |
|
_, label, center = cv2.kmeans( |
|
img_cp, k, None, criteria, 1, cv2.KMEANS_PP_CENTERS |
|
) |
|
|
|
if "dithering" in mode: |
|
center /= 255 |
|
kdt = sp.spatial.KDTree(center) |
|
|
|
def find_center(px): |
|
return center[kdt.query(px)[1]] |
|
|
|
result = dithering(img, find_center) |
|
else: |
|
result = center[label.flatten()].reshape(*img.shape) |
|
|
|
elif mode == "dithering": |
|
result = dithering(img, lambda px: np.round(px * (k - 1)) / (k - 1)) |
|
else: |
|
raise NotImplementedError("Unknown Method") |
|
|
|
if resize: |
|
result = cv2.resize(result, (o_w, o_h), interpolation=cv2.INTER_NEAREST) |
|
return result.astype(np.uint8) |
|
|
|
|
|
def run( |
|
src: Image.Image, |
|
k: int = 3, |
|
scale: int = 2, |
|
blur: int = 0, |
|
erode: int = 0, |
|
mode: str = "kmeans", |
|
precise: int = 10, |
|
resize: bool = True, |
|
) -> Tuple[Image.Image, List[List[Union[str, float]]]]: |
|
|
|
|
|
img = np.asarray(src.convert('RGBA')) |
|
|
|
|
|
alpha_channel = img[:, :, 3] |
|
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) |
|
h, w, c = img.shape |
|
d_h = h // scale |
|
d_w = w // scale |
|
o_h = h |
|
o_w = w |
|
|
|
|
|
|
|
|
|
img = preprocess(img, blur, erode) |
|
|
|
|
|
|
|
|
|
result = pixelize(img, k, c, d_w, d_h, o_w, o_h, precise, mode, resize) |
|
|
|
|
|
|
|
|
|
a = cv2.resize(alpha_channel, (d_w, d_h), interpolation=cv2.INTER_NEAREST) |
|
if resize: |
|
a = cv2.resize(a, (o_w, o_h), interpolation=cv2.INTER_NEAREST) |
|
a[a != 0] = 255 |
|
if 0 not in a: |
|
a[0, 0] = 0 |
|
r, g, b = cv2.split(result) |
|
result = cv2.merge((r, g, b, a)) |
|
|
|
|
|
result = cv2.cvtColor(result, cv2.COLOR_RGBA2BGRA) |
|
|
|
|
|
return Image.fromarray(result) |
|
|