素描

2024/11/22 2-minute read

素描

淅淅沥沥的雨声,点点滴滴落在心上。眼睛是一场惊世骇俗的杰作,目之所即,思之所往。

犹记得中学时候,有个同学会绘画,画的二次元角色,我不知道怎么评价画画的水平如何,只是依稀记得当时觉得很好看。未曾想当时不经意的一眼, 竟然能在这么多年的岁月侵蚀后,依旧难以忘却。那时候的我还不知道,别的孩子都在发展其他特长的时候,我除了对付那写不完的作业,就只剩下了对长大的期待。

一时兴起买了素描大礼包,总觉得只有黑白的世界一定很酷,等我拆开大礼包,看见里面一堆铅笔,看到一支2B铅笔,一下子就把我的思绪拉到了好多好多年前。 它应该是所有里面,我唯一熟悉的了,其他的3B,4B,5B…,2H,3H,4H…,还有其他的也看不懂。上来就浇灭了我仅有的一丝热情,真令人头大。 第一步,毫无疑问,毋庸置疑,那肯定就是削铅笔了。于是,我就吭哧吭哧拿起美工刀开始削了起来。当刀尖与铅笔接触时,刀慢慢嵌进去的感觉让我很是意外。 我记得以前总觉得削铅笔时间很费力的事情,还很难削,想来是现在的铅笔改进了品质吧。整个过程倒是有种解压的爽感,一时间也不明白为什么会有这种感觉, 现在看来,应该是不用费脑子的机械重复运动,可以让人很放松吧。

跟着B站的不知名UP主,开始学了起来,明明是跟着一笔一画,结果最后我的作品用惨不忍睹来形容都是一种夸奖了。 有时候人真的会气馁,当发现自己连根直线都画不好的时候。当一件事情被认为是简单的,而实际上做不到的时候,就会产生严重的自我批评,就比如, 我竟然连根线都不会画。在一个地方来回画线,控制线条笔直,且反复绘在同一或极其接近的位置,实在是有些为难我了。 总是不自觉地会跟随手臂摆动,用手画弧线。

素描是光与影的艺术,在素描的时候,太阳不在画中,但它却在画中无处不在,光与影的结合就在表达着它的位置。开始绘制的时候,是一条条骨架线, 然后慢慢的才能看出来它的真实样貌,如果经常素描,是不是看世间万物都是先去其形,观其骨。

如果事情得不到正反馈,那么就很难再继续下去了,没多久就放弃了手绘。握不稳笔的手,不如趁早放弃。此时,另一个念头萌生出来,我把目光投向了 OpenCV, 没错,就是大名鼎鼎的开源的计算机视觉和机器学习库。

手画不出来的素描,那就用代码“画出来”。没多久我就发现,我好像进入了一个过于广阔的空间。大部分素描的代码都大差不差,图像灰度处理,然后翻转高斯模糊, 各类滤波操作,然后出图。

图像没有经过元素剔除,亮度,暗部矫正,以及一系列然处理后再进行素描化。我忽然意识到这不就是 ISP 吗,拿到图片不是直接素描化,而是经过图像重建,色彩重建, 噪声重建,以及其他部分的修正,最后处理好的图片再进行素描处理。这么一想,这不是没完没了了吗?

再想到对于不同的图,有些地方亮度太大,我希望调低,可这种针对某一张图的修正很难应用到所有图片,这不就是摄影师拍了大量的图,然后 PS 对每一张图单独进行修图吗? 意识到不能再胡思乱想了,这得把我累坏了啊。怪不得,素描化的过程基本上通用的就是那几个流程,至于每张图的细节,就需单独设置调整了。

下面是简单的代码:

import math

import cv2
import numpy as np

from matplotlib import pyplot as plt


def show_compare(draw_dict: dict, dpi=100):
    """
    draw_dict: key = title , value = bgr
    """
    
    # import matplotlib
    # matplotlib.use('TkAgg')
    
    def factorize_nearby_factors(n: int):
        a = int(math.sqrt(n))
        b = a
        while a * b < n:
            a += 1
        return a, b
    
    image_numbers = len(draw_dict)
    w, h = factorize_nearby_factors(image_numbers)
    # figsize 和 dip 共同决定显示,默认dpi是100 (10,5),默认 dpi 下,是1000的宽和500的高
    fig, axs = plt.subplots(w, h, figsize=(16, 9), dpi=dpi)
    for index, value in enumerate(draw_dict.items()):
        title, gray = value
        row = index // w
        colum = index % h
        if w == 1 and h == 1:
            axs.imshow(gray, cmap='gray')
            axs.set_title(title)
            axs.axis('off')
        elif w == 1 or h == 1:
            axs[index].imshow(gray, cmap='gray')
            axs[index].set_title(title)
            axs[index].axis('off')
        else:
            axs[row][colum].imshow(gray, cmap='gray')
            axs[row][colum].set_title(title)
            axs[row][colum].axis('off')
    plt.tight_layout()  # 自动调整子图参数,使之填充整个图形区域
    plt.show()


def style_normal(image):
    """
    基础素描效果
    使用高斯模糊和边缘检测实现
    """
    # 转换为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 进行高斯模糊
    blur = cv2.GaussianBlur(gray, (5, 5), 0)
    
    # 边缘检测
    edges = cv2.Canny(blur, 30, 150)
    
    # 图像反转
    sketch = cv2.bitwise_not(edges)
    return sketch


def style_shape(image):
    """
    高级素描效果
    使用双边滤波和自适应阈值实现
    """
    # 转换为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 使用双边滤波进行降噪,同时保持边缘锐利
    bilateral = cv2.bilateralFilter(gray, 9, 75, 75)
    
    # 使用自适应阈值处理
    thresh = cv2.adaptiveThreshold(bilateral, 255,
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY, 11, 2)
    # sketch = cv2.bitwise_not(thresh)
    sketch = thresh
    return sketch


def style_pencil(image):
    """
    铅笔素描效果
    使用除法混合模式实现
    """
    # 转换为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 对图像进行反转
    inv = cv2.bitwise_not(gray)
    
    # 高斯模糊
    blur = cv2.GaussianBlur(inv, (21, 21), 0)
    
    inv_recover = cv2.bitwise_not(blur)
    # 混合模式除法
    sketch = cv2.divide(gray, inv_recover, scale=256.0)
    
    return sketch


def style_artistic(image):
    """
    艺术素描效果
    结合多种滤波器和形态学操作
    """
    # 转换为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 边缘增强
    kernel = np.array([[-1, -1, -1],
                       [-1, 9, -1],
                       [-1, -1, -1]])
    sharpened = cv2.filter2D(gray, -1, kernel)
    
    # 双边滤波减少噪声
    bilateral = cv2.bilateralFilter(sharpened, 9, 75, 75)
    
    # 自适应阈值
    sketch = cv2.adaptiveThreshold(bilateral, 255,
                                   cv2.ADAPTIVE_THRESH_MEAN_C,
                                   cv2.THRESH_BINARY, 11, 2)
    
    # 形态学操作优化线条
    kernel = np.ones((2, 2), np.uint8)
    sketch = cv2.morphologyEx(sketch, cv2.MORPH_CLOSE, kernel)
    
    return sketch


if __name__ == '__main__':
    filepath_in = r"demo.png"
    show_compare({
        'sketch1': style_normal(image=cv2.imread(filepath_in)),
        'sketch2': style_shape(image=cv2.imread(filepath_in)),
        'sketch3': style_pencil(image=cv2.imread(filepath_in)),
        'sketch4': style_artistic(image=cv2.imread(filepath_in))
    }, dpi=300)

一开始都梦想着代码改变世界,到了后来才发现,不被世界所改变就难能可贵。