프로그램/파이썬(Python)

여러 사진 합치기 파이썬 소스 연습용 (Multiple photo combinations Python source)

EVEWorld 2021. 1. 14. 06:10

# 여러 이미지를 합치기

 

# [사용자 시나리오]

# 1. 사용자는 합치려는 이미지를 1개 이상 선택한다.

# 2. 합쳐진 이미지가 저장될 경로를 지정한다.

# 3. 가로넓이, 간격, 포맷 옵션을 지정한다.

# 4. 시작 버튼을 통해 이미지를 합친다.

# 5. 닫기 버튼을 통해 프로그램을 종료한다.

 

# [기능 명세]

# 1. 파일추가 : 리스트 박스에 파일 추가

# 2. 선택삭제 : 리스트 박스에서 선택된 항복 삭제

# 3. 찾아보기 : 저장 폴더를 선택하면 텍스트 위젯에 입력

# 4. 가로넓이 : 이미지 넓이 지정 (원본유지, 1024, 880, 640)

# 5. 간격 : 이미지 간의 간격 지정 (없음, 좁게, 보통, 넓게)

# 6. 포맷 : 저장 이미지 포맷 지정 (PNG, JPG, BMP)

# 7. 시작 : 이미지 합치기 작업 실행

# 8. 진행상황 : 현재 진행중인 파일 순서에 맞게 반영

# 9. 닫기 : 프로그램 종료

 

import os

import tkinter.ttk as tk

import tkinter.messagebox as msbx

from tkinter import *

from tkinter import filedialog # 위에 *에 의해 호출도 가능하지만 filedialog는 서브 모듈이기 때문에 별도로 지정을 해줘야 한다.

from PIL import Image

 

root = Tk()

root.title("이미지 합치기")

# root.geometry("640x480")  # 가로 x 세로 + x좌표 + y좌표 (창 크기, xy좌표 지정하지 않으면 가변되게 나타남)

root.resizable(FalseFalse)

 

def file_add():

    # r 의 의미는 "" 안에 탈출 문자 상관없이 그대로 이용하겠다는 의미이다.

    files = filedialog.askopenfilenames(title="이미지 파일을 선택하세요.", \

        filetypes=(("PNG 파일""*.png"), ("모든 파일""*.*")), initialdir=r"C:\Users\JHROOT\Documents\아이리치 고시텔" )

    

    # 사용자가 선택한 파일 목록 출력

    for i in files:

        lstBox.insert(END, i)

        # prograss.setvar

        # start(10) # 10ms 마다 움직인다.

 

        # print(i)

 

def select_delete():

    # 앞에것부터 삭제하면 인덱스가 변할 수 있으므로 뒤에꺼 부터 삭제한다. reversed는 변수의 뒤에서 부터라는 내부함수이다.

    # 예를 들어서 

    # lst = [1, 2, 3, 4, 5]

    # print(lst) 를 하면 순서대로 출력된다.

    # lst.reverse() 를 하고 출력을 하면 순서가 거꾸로 출력이 된다.

 

    # lst3 = reversed(lst)

    # print(list(lst3)) 를 하면 순서가 거꾸로 출력이 된다. 그러나 lst는 순서가 바뀌지 않는다.

    for deleteIdx in reversed(lstBox.curselection()):

        lstBox.delete(deleteIdx)

 

def search_file():

    folder_select = filedialog.askdirectory()

    if folder_select == ''# 사용자가 취소를 눌렀을때 작동

        return

    pathText.delete(0, END)

    pathText.insert(0, folder_select)

 

def merge_image():

    try:

        # 가로넓이 옵션 적용

        img_width = widthCombobox.get()

        if img_width == "원본유지":

            img_width = -1

        else:

            img_width = int(img_width)

 

        img_between = betweenCombobox.get()

        if img_between == "좁게":

            img_between = 30

        elif img_between == "보통":

            img_between = 60

        elif img_between == "넓게":

            img_between = 90

        else:

            img_between = 0

 

        img_format = formatCombobox.get().lower() # 소문자로 변경

 

        # print(lstBox.get(0, END))

        # lstBox의 모든 Image 파일 값을 images 에 넣겠다는 의미

        images = [Image.open(x) for x in lstBox.get(0, END)]

 

        # 이미지 사이즈 리스트에 넣어서 하나씩 처리

        images_sizes = [] # (widht1, height1), (widht2, height2) 로 만들어서 리스트에 넣는다.

        if img_width > -1:

            # 이미지의 width를 변동 했을 때 height 구하는 계산

            # (원본 width : 원본 height) = (변경 width : 변경 height)

            # 원본 height 곱하기 변경 width = 원본 width 곱하기 변경 height 가 나와야한다. 

            #      100    :   60        =       80    :     ?

            #       x     :    y        =        x'   :     y'

            #       xy'   =    x'y

            #       y'    =    x'y / x  => 이 식을 적용하면 된다.

            # x = width = size[0]

            # y = height = size[1]

            # x' = img_width

            # y' = img_width * size[1] / size[0]

            images_sizes = [(int(img_width), (int(img_width) * x.size[1]) / x.size[0]) for x in images]

        else:

            images_sizes = [(x.size[0], x.size[1]) for x in images]

 

        # size -> size[0] : width, size[1] : height 의 값을 갖는다.

        # width = [x.size[0] for x in images] # images의 size[0] 즉 width 값을 모두 갖는다.

        # height = [x.size[1] for x in images] # images의 size[1] 즉 height 값을 모두 갖는다.

        # width, height = zip(*(x.size for x in images)) # 위 두문장과 같은 의미이다. (unzip 기능)

        width, height = zip(*(images_sizes))

 

        # print("width : {0}, height : {1}".format(width, height))

 

        # 최대넓이, 전체 높이 구하기

        max_width, total_height = max(width), sum(height)

        # print("max width : {0}, total height : {1}".format(max_width, total_height))

 

        if img_between > 0# 이미지 간격

            img_between += (img_between * (len(images)-1))

 

        # 스케치북 준비 (배경은 흰색)

        result_image = Image.new("RGB", (max_width, total_height), (255255255))

        y_offset = 0 # Y 위치 정보

 

        # 이미지들 아래로 붙여 넣기

        # for img in images:

        #     result_image.paste(img, (0, y_offset)) # img 즉, images의 값을 새로운 스케치북에 붙여넣기를 한다.

        #     y_offset += img.size[1] # height 값 만큼 더해줌

 

        for idx, img in enumerate(images): # enumerate에 의해 images의 인덱스와 이미지 값을 갖고 온다.

            # width 가 원본이 아닌 경우에는 이미지 크기 조절

            if img_width > -1:

                img = img.resize(images_sizes[idx])

 

            result_image.paste(img, (0, y_offset))

            y_offset += (img.size[1] + img_between) # height 값 + 사용자가 지정한 간격

 

            # 프로그래스바에 몇 %의 값을 채울것인지 설정한다.

            # idx는 0부터 시작하기 때문에 + 1을 해준다.

            progressVar = (idx + 1) / len(images) * 100 # 100은 퍼센트 표시를 위함

            pVar.set(float(progressVar))

            prograss.update()

 

        # 포맷 옵션 처리

        file_name = "save." + img_format

        # save.jpg 화일로 저장한다.

        dest_path = os.path.join(pathText.get(), file_name)

 

        result_image.save(dest_path)

        msbx.showinfo("알림""작업이 완료되었습니다.")

    except Exception as err:

        msbx.showerror("에러", err)




def start():

    # 선택한 화일이 존재하는 지 검증

    if lstBox.size() == 0:

        msbx.showwarning("경고""선택한 이미지가 없습니다.")

        return

 

    # 저장 경로가 선택 되어져 있는지 검증

    if len(pathText.get()) == 0:

        msbx.showwarning("경고""저장 경로가 설정되지 않았습니다.")

        searchButton.focus()

        return

 

    merge_image()

 

    # print("가로옵션 : {0}, 간격 : {1}, 포맷 : {2}".format(widthCombobox.get(), betweenCombobox.get(), formatCombobox.get()))

 

#-----------------------------

# 버튼 frame

mainFrame = Frame(root)

mainFrame.pack(fill="x"padx=5pady=5)

 

# 파일추가 버튼

btnFileAdd = Button(mainFrame, width=20height=1text="파일추가"command=file_add)

btnFileAdd.pack(side="left")

 

#선택삭제 버튼

btnSelectDelete = Button(mainFrame, width=20height=1text="선택삭제"command=select_delete)

btnSelectDelete.pack(side="right")

 

#-----------------------------

# 리스트 프레임

lstFrame = Frame(root)

lstFrame.pack(fill="both"padx=5pady=5)

 

# 리스트박스에 스크롤바 정의

lstScrollbar = Scrollbar(lstFrame)

lstScrollbar.pack(side="right"fill="y")

 

#리스트 박스

lstBox = Listbox(lstFrame, selectmode="extended"height=15yscrollcommand=lstScrollbar)

lstBox.pack(side="left"fill="both"expand=Trueipady=4# ipady 높이변경

lstScrollbar.config(command=lstBox.yview)

 

#-----------------------------

# 저장경로 프레임

# padx : 너비 간격, pady : 높이 간격

# ipady : 프레임의 너비 설정, ipady : 프레임의 높이 설정

# fill : 채우기 (Align)

savePathFrame = LabelFrame(root, text="저장경로")

savePathFrame.pack(fill="x"padx=5pady=5ipady=5)

 

# text

pathText = Entry(savePathFrame)

pathText.insert(0r"C:\Users\JHROOT\Documents\아이리치 고시텔")

pathText.pack(side="left"fill="x"expand=Truepadx=5pady=5ipady=4)

 

# 찾기

searchButton = Button(savePathFrame, text="찾아보기"width=10command=search_file)

searchButton.pack(side="right"padx=5pady=5)

 

#-----------------------------

# 옵션 프레임

optionFrame = LabelFrame(root, text="옵션")

optionFrame.pack(fill="x"padx=5pady=5ipady=5)

 

# 가로넓이 옵션지정 (레이블, 콤보박스)

widthLabel = Label(optionFrame, text="가로넓이"width=8).pack(side="left"pady=5)

widthValue = ["원본유지""1024""880""640"]

widthCombobox = tk.Combobox(optionFrame, height=5state="readonly"values=widthValue)

widthCombobox.current(0)

widthCombobox.pack(side="left"pady=5)

 

# 간격 옵션

betweenLabel = Label(optionFrame, text="간격"width=8).pack(side="left")

betweenValue = ["없음""좁게""보통""넓게"]

betweenCombobox = tk.Combobox(optionFrame, height=5state="readonly"values=betweenValue)

betweenCombobox.current(0)

betweenCombobox.pack(side="left")

 

# 파일 포맷 옵션

formatLabel = Label(optionFrame, text="파일포맷"width=8).pack(side="left")

formatValue = ["PNG""JPG""BMP"]

formatCombobox = tk.Combobox(optionFrame, height=5state="readonly"values=formatValue)

formatCombobox.current(0)

formatCombobox.pack(side="left"padx=5)

 

#-----------------------------

# 진행상황 프레임

prograssFrame = Frame(root)

prograssFrame.pack(fill="x"padx=5pady=3)

 

pVar = DoubleVar()

prograss = tk.Progressbar(prograssFrame, maximum=100variable=pVar, mode="determinate")

prograss.pack(fill="x"padx=5pady=3)

 

#-----------------------------

# 실행 프레임

exeFrame = Frame(root)

exeFrame.pack(fill="x"padx=5pady=5)

 

#종료 버튼

btnEnd = Button(exeFrame, width=12height=1text="종료"command=root.quit)

btnEnd.pack(side="right")

 

# 시작 버튼

btnStart = Button(exeFrame, width=12height=1text="시작"command=start)

btnStart.pack(side="right"padx=5)

 

root.mainloop()