Project/LenticularLens

[Project] LenticularLens를 활용한 입체 사진 제작 - 2. 렌티큘러 이미지 제작

hotelshoe 2022. 2. 4. 17:13
반응형

이전 포스팅에서 부족하겠지만 대략적인 이론을 설명하였고 이제 렌즈에 사용될 특별한 이미지 제작을 시작하기로 합니다. 렌티큘러 렌즈의 특성에서 알 수 있듯 두 개 혹은 여러 개의 이미지를 사용해 시선에 따라 다른 이미지가 보이도록 해야 하므로 두 개 이상의 이미지를 여려 조각으로 잘라 교차로 배열되도록 합니다.


2.1 프로그래밍 계획

곡면의 렌즈에 의해 이미지가 굴절되어 우리 눈에 서로 다른 상이 맺히게 되는데, 그림과 같이 잘려진 두 이미지를 교차로 붙여 특별한 이미지를 제작하려 합니다. python을 활용하여 전체적인 코드를 작성하고, tkinter 라이브러리를 통해 간단한 gui를 제작해서 사용을 간편하게 하도록 구성하였습니다.

 


2.2 코드 작성

import os
import tkinter.ttk as ttk
import tkinter.messagebox as msgbox
from tkinter import * #__all__
from tkinter import filedialog
from PIL import Image
import sys
import os

root = Tk()
root.title("Making Lenticular ver 1.1")
#root.geometry("640x480+500+100") #-- 창 크기 설정 / 가로 * 세로 + x좌표 + y좌표

#-- 파일 추가
def add_file():
    files = filedialog.askopenfilenames(title="이미지 파일을 선택하세요.", \
        filetypes=(("모든 파일","*.*"), ("PNG 파일","*.png")), \
        initialdir=r"C:\.") #-- 기본 저장경로 설정
    
    #-- 사용자가 선택한 파일 목록
    for file in files:
        list_file.insert(END, file)

#-- 선택 삭제
def del_file():
    #print(list_file.curselection())
    for index in reversed(list_file.curselection()):
        list_file.delete(index)

#-- 저장 경로 (폴더)
def browse_dest_path():
    folder_selected = filedialog.askdirectory()
    if folder_selected == '':
        return 
    #print(folder_selected)
    txt_dest_path.delete(0, END)
    txt_dest_path.insert(0, folder_selected)

#-- 처리된 이미지를 출력
def combo(imgs, out):
    images = map(Image.open, imgs)
    widths, heights = zip(*(i.size for i in images))
    images = map(Image.open, imgs)
    total_width = sum(widths)
    max_height = max(heights)

    new_im = Image.new('RGB', (total_width, max_height))

    x_offset = 0
    for im in images:
        new_im.paste(im, (x_offset,0))
        x_offset += im.size[0]
    
    dest_path = os.path.join(txt_dest_path.get(), out)
    new_im.save(dest_path)

#-- 이미지 분할(자르기)
def crop(image_path, coords, saved_location):
    """
    image_path: 편집할 이미지 경로
    coords: x, y 좌표 tuple (x1, y1, x2, y2)
    saved_location: 잘려진 이미지가 저장되는 경로
    """
    image_obj = Image.open(image_path)
    cropped_image = image_obj.crop(coords)
    cropped_image.save(saved_location)

#-- 이미지 결합
def strips(image, pieces):
    gen = []
    im = Image.open(image)
    width, height = im.size
    mult = width/pieces #-- width/pieces: x 값과 height/width 를 사용하여 width/pieces에 기준을 세워 계산 
    for i in range(0,pieces):
        if i % 2 == 0: #-- 짝수 pieces만 추출
            continue
        x = 0+(i*mult)
        crop(image, (x, 0, x+mult, height), image.split(".")[0]+'_'+str(i+1)+'.png') #imagename_iteration.png
        gen.append(image.split(".")[0]+'_'+str(i+1)+'.png')
    return gen

#-- 이미지 처리
def merge_image():
  
    psz = int(piece_txt.get("1.0", "end"))
    image1 = list_file.get(0)
    image2 = list_file.get(1)
    print(image1)
    print(image2)
    im = Image.open(image1)
    w, h = im.size
    im2 = Image.open(image2)
    w2, h2 = im2.size

    print("입력한 두 이미지의 크기는 다음과 같습니다.")
    print("첫 번째 이미지:", im.size)
    print("두 번째 이미지:", im2.size)
    
    if w != w2 or h != h2:
        sys.exit("불러온 두 이미지 크기가 같아야 합니다!") #-- 불러온 두 이미지의 크기가 다른 경우 출력

    if psz > w:
        sys.exit("픽셀 수 보다 나누는 수가 더 큽니다.") #-- 이미지를 나눌 때 픽셀보다 큰 값을 입력할 경우

    list1 = strips(image1, psz)
    list2 = strips(image2, psz)
    new = []

    for i in range(0, len(list1)):
        new.append(list1[i])
        new.append(list2[i])
        progress = ((i + 1) / len(list1)) * 100
        p_var.set(progress)
        progress_bar.update()
    file_name = "lenticularImg.png"
    combo(new, file_name)
    msgbox.showinfo("알림", "작업이 완료되었습니다.")

    for n in new:
        os.remove(n)


#-- 시작
def start():
    # 각 옵션들 값을 확인
    # print("가로 넓이 : ", cmb_width.get())
    # print("간격 : ", cmb_space.get())
    # print("포맷 :", cmb_format.get())

    #-- 파일 목록 확인
    if list_file.size() == 0:
        msgbox.showwarning("경고", "이미지 파일을 추가하세요.")
        return

    #-- 저장 경로 확인
    if len(txt_dest_path.get()) == 0:
       msgbox.showwarning("경고", "저장 경로를 선택하세요.")
       return 

    #-- 이미지 통합 작업
    merge_image()

#-- 파일 프레임 (파일 추가와 선택)
file_frame = Frame(root)
file_frame.pack(fill="x", padx="5", pady="5" ) #-- 간격 띄우기

btn_add_file = Button(file_frame, padx=5, pady=5, width=12, text="파일추가", command=add_file)
btn_add_file.pack(side = "left")

btn_del_file = Button(file_frame, padx=5, pady=5, width=12, text="선택삭제", command=del_file)
btn_del_file.pack(side="right")

#-- 리스트 프레임
list_frame = Frame(root)
list_frame.pack(fill="both", padx="5", pady="5")

scrollbar = Scrollbar(list_frame)
scrollbar.pack(side="right", fill="y")

list_file = Listbox(list_frame, selectmode = "extended", height=15, yscrollcommand = scrollbar.set)
list_file.pack(side="left", fill="both", expand=True)
scrollbar.config(command=list_file.yview)

#-- 저장 경로 프레임
path_frame = LabelFrame(root, text="저장 경로")
path_frame.pack(fill="x", padx="5", pady="5", ipady ="5")

txt_dest_path = Entry(path_frame)
txt_dest_path.pack(side="left", fill="x", expand=True, padx="5", pady="5", ipady=4) # 높이 변경
txt_dest_path.insert(END, "C:/Users/신태호/Desktop")

btn_dest_path = Button(path_frame, text="찾아보기", width=10, command=browse_dest_path)
btn_dest_path.pack(side="right", padx="5", pady="5")

#-- 조각수 프레임
piece_frame = LabelFrame(root, text = "나눌 조각수")
piece_frame.pack(fill="x", padx="5", pady="5", ipady ="5") #padx="5", pady="5", ipady ="5"

piece_txt = Text(piece_frame, width = 70, height = 1)
piece_txt.pack()

#-- 진행 상황 Progress Bar
frame_progress = LabelFrame(root, text="진행상황")
frame_progress.pack(fill="x", padx="5", pady="5", ipady ="5")

p_var = DoubleVar()
progress_bar = ttk.Progressbar(frame_progress, maximum=100, variable=p_var)
progress_bar.pack(fill="x", padx="5", pady="5")

#-- 실행 프레임
frame_run = Frame(root)
frame_run.pack(fill="x", padx="5", pady="5")

btn_closed = Button(frame_run, padx=5, pady=5, text="닫기", width=12, command=root.quit)
btn_closed.pack(side="right", padx="5", pady="5")

btn_start = Button(frame_run, padx=5, pady=5, text="시작", width=12, command = start)
btn_start.pack(side="right", padx="5", pady="5")

root.resizable(False, False) #-- x(너비), y(높이) 값 변경 불가
root = mainloop()

대략적인 원리는 두 이미지를 아주 작게 잘라 다시 붙여 완성하는 것입니다.

주의할 점은 두 이미지의 크기가 같아야 하며 두 이미지 간의 시선 차이가 크지도 작지도 않아야 합니다.(이는 양안시차의 원리와 밀접한 연관이 있는데, 수학적으로 상의 위치와 양안시차인 6.5cm만큼을 잘 계산하여 그 차이를 명확하게 나타낼 수 있으나 여기선 대략적인 눈대중으로 테스트하였습니다) 

 

코드를 실행하면 위와 같은 gui가 나타나며, 촬영된 사진 두 장(해상도가 같은)을 프로그램을 통해 렌티큘러 이미지를 생성합니다. 프로그램은 파일 선택/삭제 및 저장 경로 프레임을 구성하였고, 다양한 렌티큘러 렌즈의 사이즈에 맞추어 이미지를 편집할 수 있도록 이미지 분할 사이즈(나눌 조각수)를 조절할 수 있도록 하였습니다. 이때 조각수는 이미지의 해상도(가로)보다 작게하되, 사용될 렌티큘러 렌즈의 피치에 맞게 계산하여 기입해야 합니다.

 

예) 사용될 렌티큘러 렌즈 시트가 40lpi(lens per inch)의 경우 피치가 약 0.635mm이므로 출력물의 이미지 배열 간격이 약 0.3175mm가 되도록 합니다. 프린터 별 성능에 따라 실제 출력물에 인쇄된 간격이 상이하니 이 부분은 개별적으로 계산하거나 하나하나 출력하여 근사치를 대략적으로 추측해야 합니다.

 

이제 프로그램에 대한 준비는 끝났고, 렌티큘러 렌즈와 촬영 이미지를 준비하기만 하면 됩니다.

 


2.3 렌즈 준비 및 사진 촬영

렌티큘러 렌즈 시트

렌티큘러 렌즈의 경우 사진과 같이 시트의 형태로 제작됩니다. 과학 용품 판매처나 기타 쇼핑몰에는 찾아볼 수 없었으며 해외 사이트의 경우 대량판매만 가능하여 난감한 상황이었으나, 여러 수소문끝에 유선으로 제작 업체와 컨텍하여 a4 용지 크기의 40lpi 렌즈를 주문할 수 있었습니다.

 

촬영 방법은 그림과 같이 물체를 양안 시차의 간격인 6.5cm의 간격으로 두 번 촬영하였습니다.(사실 이 부분은 여러 렌티큘러 제작 방법에 관한 사이트에서 설명하는 방법이나, 실제로는 카메라와 물체와의 거리에 따라 두 이미지 간의 차이가 크게 생기기 때문에 이 부분도 대략적인 눈대중으로 실시하였습니다.)  이때 주의할 점은 양안 시차의 간격보다 큰 간격으로 촬영할 경우 렌티큘러 이미지를 만들었을 때 뒤틀림이 심하고 양안 시차의 효과를 보기 어렵습니다. 촬영 거리의 경우 육안으로 보았을 때 물체를 식별할 수 있을 정도의 거리가 적절하며, 명확한 결과물을 얻기 위해 2m 거리에서 촬영된 샘플 이미지를 사용하였습니다.


2.4 이미지 제작

이제 작성된 코드를 run해서 프로그램을 실행시킵니다. 샘플 이미지 두 장을 선택하고 저장경로를 선택한 뒤 나눌 조각수를 입력합니다. 사용될 이미지의 해상도는 1024*768 이므로 가로사이즈 보다 작게 적당히 512 정도로 설정했습니다.(프로그램 실행 여부를 알아보기 위한 테스트 이므로 렌즈 시트에 맞는 계산은 생략)

 

작업이 완료되면 알림창과 함께 저장 경로 위치에 파일이 생성된다.

 

1번 이미지
2번 이미지
출력 예시

완성된 이미지의 모습

눈 하나가 카메라 렌즈 하나라고 생각했을 때 이 정도의 시선 차이가 생긴다고 보면 될 것 같습니다.

 

사용 예

적절한 배열 간격 사이즈를 구하여 실제 렌즈 시트를 덮은 모습. 완벽하진 않지만 어느 정도 입체감(눈과 렌즈 사이에 상이 떠있는 듯한?)을 느껴볼 수 있었고, 당연하겠지만 실제 렌즈 앞에서 육안으로만 관측 가능합니다.

 

출력 예시

실제 촬영물을 통해 제작해 본 결과

이 경우도 100 조각 단위로 출력하여 최적의 입체 효과를 볼 수 있도록 하나하나 대조해 보았고, 실제 프린터로 출력하여 렌즈 시트를 덮었을 때 위 사진에서 가장 명확하게 입체효과를 볼 수 있었습니다.


결론적으로 렌티큘러 렌즈를 통해 입체 이미지를 보는 효과의 원리를 이용하여 프로그램을 통해 소소하게나마 구현해 볼 수 있었습니다. 이론적인 부분도 많이 부족하고 특히 좀 더 수학적으로 접근하여 정확하게 결과물을 내고 싶었지만 어떤 방법으로 다가가야할지 고민이 되었습니다. 그 결과 하나하나 대조하며 결과를 내려하니 시간적으로도 많이 낭비되었고 물질적으로도(프린트 종이 및 잉크) 낭비가 심했습니다... 좀 더 공부하고 노력해야겠습니다.

반응형