J'ai implémenté le traitement des demi-teintes de couleur des images en utilisant la bibliothèque d'images "oreiller" de Python. Utilisez cette option lorsque vous souhaitez convertir une photo au style américain ou lorsque vous souhaitez reproduire des ombres telles que les tons d'écran.
Les demi-teintes sont un effet souvent utilisé pour les styles de peinture tels que les bandes dessinées américaines et les dessins pop.
<iclass="fafa-arrow-right"aria-hidden="true">
La demi-teinte de couleur est un processus qui sépare les couleurs en CMJN et superpose des demi-teintes avec différents angles. L'effet est similaire à celui des imprimés en couleur.
Au début, j'espérais que ce serait facile à mettre en œuvre, mais c'était un processus étonnamment gênant. Il y avait peu de documents techniques en japonais, je vais donc faire une note pour partager et améliorer la précision.
Le demi-teinte est une pseudo-représentation de la gradation avec des points monochromatiques disposés selon un angle dans l'image. À un angle de 45 °, où le pas est l'espacement des points et r est le rayon maximal des points, le résultat est comme indiqué dans la figure ci-dessous.
Le problème ici est la disposition des points en fonction de l'angle: plus précisément, l'image originale est tournée et numérisée. La figure ci-dessous montre le scan lorsque l'angle est de 45 °.
Dans le cas de la demi-teinte de couleur, elle est synthétisée en décalant l'angle par rapport à chaque bande de CMJN. Il semble que le cyan est souvent réglé sur 15 °, le jaune sur 30 °, le noir sur 45 ° et le magenta sur 75 °.
Le demi-teinte est implémenté par "oreiller" qui est une bibliothèque d'images standard de Python.
Tout d'abord, c'est un balayage angulaire, ce qui est facile avec la matrice de transformation affine de coordonnées. La matrice de rotation bidimensionnelle est la suivante.
\begin{bmatrix}
cosθ & -sinθ \\
sinθ & cosθ
\end{bmatrix}
\begin{bmatrix}
x\\
y
\end{bmatrix}
Étant donné que l'opération de matrice n'est pas fournie en standard en Python, une fonction d'opération de matrice est générée par la fermeture. A ce moment, un pas d'espacement des points est reçu, et lorsque la valeur de coordonnées augmente de 1, une matrice est créée qui la convertit en un système de coordonnées qui pointe vers le pas adjacent. À propos, il renvoie également la matrice inverse qui convertit le système de coordonnées de pas en système de coordonnées normal.
def create_mat(_rot, _pitch):
"""Générer une matrice pour le système de coordonnées de pas et une matrice inverse pour le système de coordonnées normales"""
_pi = math.pi * (_rot / 180.)
_scale = 1.0 / _pitch
def _mul(x, y):
return (
(x * math.cos(_pi) - y * math.sin(_pi)) * _scale,
(x * math.sin(_pi) + y * math.cos(_pi)) * _scale,
)
def _imul(x, y):
return (
(x * math.cos(_pi) + y * math.sin(_pi)) * _pitch,
(x * -math.sin(_pi) + y * math.cos(_pi)) * _pitch,
)
return _mul, _imul
De plus, comme la source devient difficile à voir en raison de la boucle multiple de x et y pendant le balayage, nous allons également créer un générateur qui scanne x et y. En faisant cela, plusieurs boucles seront éliminées et vous serez en mesure d'écrire clairement.
def x_y_iter(w, h, sx, ex, sy, ey):
fw, fh = float(w), float(h)
for y in range(h + 1):
ty = y / fh
yy = (1. - ty) * sy + ty * ey
for x in range(w + 1):
tx = x / fw
xx = (1. - tx) * sx + tx * ex
yield xx, yy
La fonction de numérisation de l'image est la suivante.
def halftone(img, rot, pitch):
mat, imat = create_mat(rot, pitch)
w_half = img.size[0] // 2
h_half = img.size[1] // 2
pitch_2 = pitch / 2.
#Calculer la boîte englobante
bounding_rect = [
(-w_half - pitch_2, -h_half - pitch_2),
(-w_half - pitch_2, h_half + pitch_2),
]
x, y = zip(*[mat(x, y) for x, y in bounding_rect])
w, h = max(abs(t) for t in x), max(abs(t) for t in y)
#Moyenne avec filtre gaussien
gmono = img.filter(ImageFilter.GaussianBlur(pitch / 2))
#Lancer une analyse,(x, y, color)Générer un tableau de
dots = []
for x, y in x_y_iter(int(w * 2) + 1, int(h * 2) - 1, -w, w, -h + 1., h - 1.):
x, y = imat(x, y)
x += w_half
y += h_half
if -pitch_2 < x < img.size[0] + pitch_2 and -pitch_2 < y < img.size[1] + pitch_2:
color = gmono.getpixel((
min(max(x, 0), img.size[0]-1),
min(max(y, 0), img.size[1]-1)
))
t = pitch_2 * (1.0 - (color / 255))
dots.append((x, y, color))
return dots
Générez une image en demi-teinte à partir de la matrice numérisée.
def dot_image(size, dots, dot_radius, base_color=0, dot_color=0xFF, scale=1.0):
img = Image.new("L", tuple(int(x * scale) for x in size), base_color)
draw = ImageDraw.Draw(img)
for x, y, color in dots:
t = dot_radius * (color / 255) * scale
x *= scale
y *= scale
draw.ellipse((x - t, y - t, x + t, y + t), dot_color)
return img
Générons maintenant une image en demi-teinte monochrome.
img = Image.open("sample.png ").convert("L")
Dans la conversion monochrome, nous voulons exprimer la partie noire avec des points au lieu d'exprimer la partie blanche avec des points, de sorte que la couleur est inversée.
img = ImageOps.invert(img)
Générez une image en demi-teinte avec rot = 45 °, pitch = 3, dot_radius = 2.5.
# rot=45°, pitch=3, dot_radius=2.Convertir avec 5
dots = halftone(img, 45, 3)
#Le dos est 0xFF,Génération d'images en demi-teintes avec des points 0x00
dot_image(img.size, dots, 2.5, 0xFF, 0x00)
sale···
La raison pour laquelle il est si sale est que ImageDraw de l'oreiller ne prend pas en charge l'anti-aliasing, donc le dessin point par point provoque un aliasing. Si vous souhaitez anticréneler ImageDraw avec oreiller, effectuez un super échantillonnage environ 8 fois. (Échantillonnage d'une image plus grande que l'image de sortie et réduction de celle-ci pour réduire le crénelage)
#Suréchantillonner 8 fois et réduire
dot_image(img.size, dots, 2.5, 0xFF, 0x00, scale=8.).resize(img.size, Image.LANCZOS)
On voit un peu de moiré, mais je pense qu'il est de qualité suffisante.
Cairo est une bibliothèque de dessins vectoriels, qui dessine avec des vecteurs, ce qui lui permet de produire très bien.
Vous pouvez sortir en utilisant Cairo simplement en remplaçant la fonction dot_image
par la fonction suivante.
def dot_image_by_cairo(size, dots, dot_radius, base_color=(0, 0, 0), dot_color=(1., 1., 1.), scale=1.0):
import cairo
w, h = tuple(int(x * scale) for x in img.size)
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
ctx = cairo.Context(surface)
ctx.set_source_rgb(*base_color)
ctx.rectangle(0, 0, w, h)
ctx.fill()
for x, y, color in dots:
fcolor = color / 255.
t = dot_radius * fcolor * scale
ctx.set_source_rgb(*dot_color)
ctx.arc(x, y, t, 0, 2 * math.pi)
ctx.fill()
return Image.frombuffer("RGBA", img.size, surface.get_data(), "raw", "RGBA", 0, 1)
#Sortie utilisant le Caire
dot_image_by_cairo(img.size, dots, 2.5, [1]*3, [0]*3, scale=1.)
C'est de meilleure qualité que le suréchantillonnage, mais pycairo est difficile à installer, donc je pense personnellement que le suréchantillonnage est suffisant.
C'est fondamentalement la même chose qu'en monochrome, il suffit de décomposer l'image en CMJN, de générer des demi-teintes pour chaque bande à des angles différents, et enfin de fusionner les bandes en CMJN.
Dans l'exemple ci-dessous, le cyan est émis à 15 °, le jaune à 30 °, le noir à 45 ° et le magenta à 75 °.
cmyk = img.convert("CMYK")
c, m, y, k = cmyk.split()
cdots = halftone(c, 15, 3)
mdots = halftone(m, 75, 3)
ydots = halftone(y, 35, 3)
kdots = halftone(k, 45, 3)
nc = dot_image(img.size, cdots, 2.5, scale=3.)
nm = dot_image(img.size, mdots, 2.5, scale=3.)
ny = dot_image(img.size, ydots, 2.5, scale=3.)
nk = dot_image(img.size, kdots, 2.5, scale=3.)
Image.merge("CMYK", [nc, nm, ny, nk]).convert("RGB")
Vous n'avez pas besoin de savoir lorsque vous utilisez un oreiller, mais lors du portage sur un smartphone, etc., vous devez calculer vous-même la conversion RVB-> CMJN, CMJN-> RVB. Il n'y a pas de formule de conversion parfaite entre RVB et CMJN, c'est donc juste une pseudo-formule. Voici une formule de conversion générale pour RVB-> CMJN et CMJN->.
RGB->CMYK
C=(1-R-K)/(1-K)\\
M=(1-G-K)/(1-K)\\
Y=(1-B-K)/(1-K)\\
K=min(1-R,1-G,1-B)
CMYK->RGB
R=1-min(1,C*(1-K)+K)\\
G=1-min(1,M*(1-K)+K)\\
B=1-min(1,Y*(1-K)+K)
C'est solitaire de simplement générer des demi-teintes, j'ai donc créé un filtre de style dessin animé en utilisant des demi-teintes. Faites en sorte que Lena dans l'image d'exemple ressemble à un dessin animé.
img = Image.open("lenna.png ")
Convertissez en monochrome, clarifiez la lumière et l'obscurité en égalisant, puis éclaircissez la partie noire. Selon le goût du motif, si vous rendez la partie d'ombre trop sombre, les détails seront perdus, alors rendez la partie noire plus lumineuse. Veuillez assaisonner cette zone avec l'image du dessin terminé.
mono_img = img.convert("L")
mono_img = ImageOps.equalize(mono_img)
mono_img = mono_img.point(lambda x: x + 30 if x < 100 else x)
Réduisez la couleur à 4 couleurs pour créer la partie tonale. Si les détails sont corrects, cela ressemblera à une photo, alors appliquez le filtre de mode pour écraser les détails fins.
q_img = mono_img.quantize(4).convert("L")
q_img = q_img.filter(ImageFilter.ModeFilter(4))
Un traitement en demi-teinte est appliqué à cela.
dots = halftone(ImageOps.invert(q_img), 45, 4)
dot_img = dot_image(q_img.size, dots, 2, 0xFF, 0x00, scale=8).resize(q_img.size, Image.LANCZOS)
Superposer des composites d'images en demi-teintes et monochromes. Pour la méthode de composition de superposition, reportez-vous à "Implémentation de modes de dessin tels que PhotoShop à grande vitesse avec PIL / Pillow".
from PIL import ImageMath
def _over_lay(a, b):
_cl = 2 * a * b / 255
_ch = 2 * (a + b - a * b / 255) - 255
return _cl * (a < 128) + _ch * (a >= 128)
def over_lay(img1, img2):
eval_str = "func(float(a), float(b))"
return ImageMath.eval(eval_str, func=_over_lay, a=img1, b=img2).convert("L")
tone_img = over_lay(q_img, dot_img)
Le reste est complété par la synthèse de cette image tonale et du dessin au trait.
gray = img.convert("L")
gray2 = gray.filter(ImageFilter.MaxFilter(5))
line_inv = ImageChops.difference(gray, gray2)
line_img = ImageOps.invert(line_inv)
ImageChops.multiply(tone_img, line_img)
La demi-teinte couleur est un filtre très polyvalent qui peut être utilisé pour convertir les ombres d'une photo en un ton d'écran de dessin animé ou pour donner l'impression d'un imprimé tel que des bandes dessinées américaines.
Le code complet du traitement des demi-teintes est publié sur Gist.
https://gist.github.com/pashango2/487e5147015655a7fd6db3cbc1c7c833
Recommended Posts