[PYTHON] À propos du traitement des demi-teintes couleur des images

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.

grad.png <iclass="fafa-arrow-right"aria-hidden="true"> grad2.png

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.

rect4168.png

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.

Vue d'ensemble du traitement des demi-teintes

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.

範囲を選択_025.png

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 °.

scan2.png

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 °.

CMYK拡大.png

Mise en œuvre par oreiller

Le demi-teinte est implémenté par "oreiller" qui est une bibliothèque d'images standard de Python.

Scan par angle

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érer une image en demi-teinte

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")

ダウンロード (11).png

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)

ダウンロード (13).png

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)

ダウンロード (14).png

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)

ダウンロード (15).png

On voit un peu de moiré, mais je pense qu'il est de qualité suffisante.