Full moon

  • 지난 추석, 간만에 긴장을 풀었습니다.
  • 가끔 취미로 그림을 그리고는 하는데 python으로는 안그렸네요.
  • 자다가 문득 코드가 떠올라 보름달을 그렸습니다.

1. 보름달은?

  • 보름달이 어떻게 생겼는지 모르는 사람은 없을 겁니다.


  • 검은 밤 하늘에 떠 있는 하얀 동그라미로 단순화할 수 있습니다.

  • 토끼가 방아를 찧고 있는 듯한 모양이 있지만 잠시 잊기로 합니다.

  • 하지만 그냥 동그라미를 그리기엔 심심합니다. 작은 동그라미를 여럿 겹칩니다.

2. 코드로 그리는 보름달

2.1. 코드로 그리는 그림

젠미디어: 이주행 ETRI 연구원 인터뷰
Techcrunch: The digital age of data art

  • 무미건조한 코드나 데이터로 아름다움을 만들어내는 분들이 있으십니다.
  • 데이터 시각화의 심미적 요소를 한껏 활용하는 것으로 볼 수도 있고
  • 그림을 그리는 도구가 바뀌었을 뿐 데이터와 무관한 아름다움을 추구하기도 합니다.
  • 이런 분들을 따라해 보기로 합니다

    (좌) 이주행, "Pixel Stack", (우) Mark Napier "Black and White"

2.2. 약간의 기하학

  • 중심을 (0,0)으로 하는 극좌표계 공간을 만듭니다.

  • 반지름이 1인 공간 안에 랜덤하게 한 점을 골라 원의 중심을 잡습니다.

  • 극좌표계를 사용하여 원점으로부터의 거리과 방위각으로 좌표를 잡으면 편리합니다.

  • matplotlib의 Circle을 이용해서 원을 생성합니다.

  • 원의 반지름을 1-원점으로부터의 거리로 설정하면 원의 윤곽선에 항상 맞닿습니다.

  • 10개만 그려봅시다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    from matplotlib.patches import Circle

    num = 10 # 원의 수

    fig, ax = plt.subplots(figsize=(5, 5),
    constrained_layout=True)

    # 극좌표계에서 원 생성
    R = np.linspace(0, 1, 100) # 반지름의 범위: 0~1
    pos_r = np.random.choice(R, size=num) # 랜덤 위치 (반지름)
    pos_a = 2*np.pi*np.random.uniform(size=num) # 랜덤 위치 (방위각)

    # 직교좌표계 변환
    pos_x = pos_r * np.cos(pos_a) # 직교좌표계 x
    pos_y = pos_r * np.sin(pos_a) # 직교좌표계 y

    for x, y, r in zip(pos_x, pos_y, pos_r):
    r_circle = 1-r # 원의 반지름
    o = Circle((x, y), r_circle, fc="none", ec="k", alpha=1)
    ax.add_patch(o)

    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax.axvline(0, c="g")
    ax.axhline(0, c="g")


2.3. 확률 제어

  • 100개를 그리면 이렇습니다.

  • 뭔가 특이한 점을 느끼셨을까요?

  • 원의 중심점 분포를 그리면 이렇습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 점들 사이 최단거리 계산
    pos_xy = np.array(list(zip(pos_x, pos_y)))
    d_shortests = []
    for p in pos_xy:
    dp = pos_xy-p
    d_shortest = np.inf
    for d in dp:
    sd = np.sqrt(d[0]**2 + d[1]**2)
    if 0 < sd < d_shortest:
    d_shortest = sd
    d_shortests.append(d_shortest)

    # 시각화
    fig, axs = plt.subplots(ncols=2, figsize=(8, 4), constrained_layout=True)

    axs[0].scatter(pos_x, pos_y)
    axs[0].axvline(0, c="g")
    axs[0].axhline(0, c="g")
    sns.kdeplot(d_shortests, cut=0, ax=axs[1], fill=True)
    axs[1].set_xlim(0, max(d_shortests))


  • 원점 부근을 중심으로 하는 점들이 월등히 많습니다.

  • 앞서 그림을 그릴 때 원의 반지름을 1-원점으로부터의 거리로 설정했지요.

  • 큰 원과 작은 원의 수는 비슷하더라도 작은 원은 여기저기 퍼져있고 큰 원은 뭉쳐있다는 의미입니다.

  • 나쁘다는 것은 아닙니다.
  • 전체 원의 가운데보다 바깥 부분에 많은 선이 그려진다는 뜻이고,
  • 원의 갯수를 키우는 것 만으로도 뭔가 3D 느낌이 납니다.
  • 500개를 그리면 이렇게 됩니다.

  • 다만, 약간의 확률 조작을 통해 연출이 가능하다는 의미입니다.

  • 예를 들어 이런 확률분포를 사용하면,

  • 그림은 이렇게 바뀝니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    fig, ax = plt.subplots(figsize=(5, 5), 
    constrained_layout=True)

    # 극좌표계에서 원 생성
    R = np.linspace(0, 1, 100) # 반지름의 범위: 0~1

    # np.random.choice에 매개변수 p 적용, 확률 제어
    f = np.float_power(np.sin(R), 10)
    p = (f/max(f))/sum(f/max(f))
    pos_r = np.random.choice(R, size=num, p=p) # 랜덤 위치 (반지름)
    pos_a = 2*np.pi*np.random.uniform(size=num) # 랜덤 위치 (방위각)

    # 직교좌표계 변환
    pos_x = pos_r * np.cos(pos_a) # 직교좌표계 x
    pos_y = pos_r * np.sin(pos_a) # 직교좌표계 y

    r_circles = []
    for x, y, r in zip(pos_x, pos_y, pos_r):
    r_circle = 1-r # 원의 반지름
    r_circles.append(r_circle)
    o = Circle((x, y), r_circle, fc="none", ec="k", alpha=1, lw=0.1)
    ax.add_patch(o)

    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax.axvline(0, c="g")
    ax.axhline(0, c="g")


2.4. 달 띄우기

  • 달을 그리는 함수를 만들어봅니다.

  • 앞에서 만든 함수에 딱 하나, faceccolor와 ‘edgecolor`를 제어하는 매개변수를 추가했습니다.

  • facecolor에 numpy array를 넣으면 랜덤하게 색을 입히는 기능을 추가했고요.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    def plot_fullmoon(size, scale=5, fc="w", ec="none", filename="fullmoon", **kwargs):
    scale = np.float(scale)
    fig, ax = plt.subplots(figsize=(20, 20), constrained_layout=True)
    ax.set_facecolor("k")
    ax.spines[["top", "left", "bottom", "right"]].set_visible(False)
    ax.set_xticks([])
    ax.set_yticks([])

    # random circle
    R = np.linspace(0, 1, size)
    F = np.float_power(np.sin(R), 10)
    P = (1-F/max(F))/sum(1-F/max(F))

    pos_r = np.random.choice(R, size=size, p=P)
    pos_a = 2*np.pi*np.random.uniform(size=size)
    pos_x = pos_r * np.cos(pos_a)
    pos_y = pos_r * np.sin(pos_a)

    for x, y, r in zip(pos_x, pos_y, pos_r):
    if isinstance(fc, np.ndarray):
    fc = np.array([0.8, 0.8, 0.8]) + np.array([np.random.normal(loc=0.1, scale=0.05), np.random.normal(loc=0.1, scale=0.05), np.random.normal(loc=0.1, scale=0.05)])
    fc[0] = min(fc[0], 1)
    fc[1] = min(fc[1], 1)
    fc[2] = min(fc[2], 1)

    o = Circle((x, y), 1-r, fc=fc, ec=ec, alpha=scale/size, **kwargs)

    ax.add_patch(o)

    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)

    fig.set_facecolor("k")
    fig.savefig(f"{filename}.png", dpi=300)

    plot_fullmoon(10, fc=np.array([0.6, 0.6, 0.6]), filename="fmc_10", ec="w", lw=3)


  • 원 10개로 그리면 이런 달이 떠오릅니다.

  • 원 20개, 50개, 100개로도 그려볼 수 있겠죠.


  • 꼭 추석이 아니더라도 모두들 둥근 달처럼 행복하시기 바랍니다.



도움이 되셨나요?

카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share