[Python] 회전된 사진 정상화하기 (feat. EXIF)

조오닭·2024년 12월 3일
1

오늘도 테스트 중 있었던 일을 기록하려 한다.

프로필 사진이 제대로 변경되는 지 테스트하던 중, 내가 촬영했던 사진을 업로드했더니 우측으로 90도 회전되는 문제가 발생했다.
하지만 갤러리 속의 사진은 정방향으로 잘 보여졌고 이를 구글링해보니 사진 정보 내 EXIF가 원인인 것을 알아냈다.

EXIF?

이미지 파일에 대한 메타데이터 포맷.
기본적인 정보로는 날짜와 시간, 많게는 위치정보와 Orientation까지 포함된 데이터.

이 중 문제가 되었던 부분은 orientation이다.

Orientation


Orientation은 이미지 회전과 반전 정보로, 기기의 가속도 센서 정보값을 담고 있다.
만약 내가 풍경 사진을 찍기 위해 핸드폰을 가로로 들어서 찍는다고 가정해보자.
이때 화면 속 풍경은 화면을 가득채우고 있지만, 갤러리에는 내가 풍경을 보고 있는 시야 즉 가로가 긴 형태가 보일 것이다.
이는 orientation 정보를 활용하여 올바르게 보이는 것이다.

그럼 orientation 정보를 활용하지 않았을 경우 어떻게 보이는걸까?
바로 90도 회전한 사진, 다시 말하면 내가 카메라를 돌렸을때 핸드폰에 저장된 사진 그대로 보인다.

우리는 orientation 정보를 미처 알지 못하였고, 테스트 중 기울여서 찍은 사진을 업로드했더니 90도로 회전된 것이다.

그래서 어떻게 돌렸니?


나는 Pillow(PIL; Python Image Library)를 활용하여 해당 이미지의 EXIF 내의 orientation 값을 조회한 뒤 회전했다.

전체 코드는 다음과 같다.

import io
from PIL import Image, ExifTags
from django.core.files.images import ImageFile

def correct_image_orientation(image_file: ImageFile):
    try:
    	# 1
        image = Image.open(image_file)
        # 2
        for orientation in ExifTags.TAGS.keys():
            if ExifTags.TAGS[orientation] == 'Orientation':
                break
        # 3
        exif = dict(image.getexif().items())
        if exif[orientation] == 3:
            image = image.transpose(Image.ROTATE_180)
        elif exif[orientation] == 6:
            image = image.transpose(Image.ROTATE_270)
        elif exif[orientation] == 8:
            image = image.transpose(Image.ROTATE_90)
        # 4
        buffer = io.BytesIO()
        image.save(buffer, format='JPEG')
        buffer.seek(0)
        return buffer
	#5
    except (AttributeError, KeyError):
        image_file.seek(0)
        return image_file

총 다섯 단계로 나누어 설명할 수 있다.

  1. 이미지 파일을 PIL 라이브러리로 오픈함
  2. EXIF 정보 내 Orientation이 담겨있는 키 값을 가져옴
  3. EXIF 정보를 dict 형태로 변환하여 키를 통해 orientation 값을 가져오고, 그 값에 따라 이미지를 회전시킴
  4. 회전시킨 이미지를 파일로 저장함
  5. 예외처리

이 중 구체적으로 말할 부분은 2, 3, 5번이다.

2. EXIF 정보 내 Orientation이 담겨있는 키 값을 가져오는 절차

# 2
for orientation in ExifTags.TAGS.keys():
	if ExifTags.TAGS[orientation] == 'Orientation':
		break

PIL에서 EXIF 데이터를 key, value로 나뉜 dict 형태로 저장하고 있다.

ExifTags.TAGS = {
    274: 'Orientation',
    306: 'DateTime',
    ...
}

이때 모든 EXIF 정보 중, value가 Orientation에 해당하는 key인 274를 찾기 위한 작업이다.
물론 orientation = 274 으로 저장해도 되지만, 하드코딩을 지양하고 혹시나 key 값이 변경될 수 있으므로 유지보수성 측면에서 동적으로 불러온 것이다.

3. orientation 값을 가져오고 이미지를 회전

# 3
exif = dict(image.getexif().items())
if exif[orientation] == 3:
	image = image.transpose(Image.ROTATE_180)
elif exif[orientation] == 6:
	image = image.transpose(Image.ROTATE_270)
elif exif[orientation] == 8:
	image = image.transpose(Image.ROTATE_90)

해당 이미지의 exif 정보를 getexif()를 통해 가져오고, dict 형태로 저장하였다.
또한 2번에서 얻은 orientation key값을 통해 orientation value는 exif[orientation]으로 불러오고 3, 6, 8일 경우에는 각각 180도, 270도, 90도 회전 작업을 하였다.

왜 3, 6, 8일까?
아래 사진을 통해 1부터 8까지 orientation 값이 배정됨을 알 수 있다.
하지만 1은 정상적인 사진, 2, 4, 5, 7은 반전된 사진이므로 기울임과 거리가 멀다.

(... 3, 6, 8 생각해보니까 이것도 하드코딩인데)

5. 예외처리

#5
except (AttributeError, KeyError):
	image_file.seek(0)
	return image_file

AttributeError, KeyError를 처리하고 있음을 알 수 있다.

  • 첫번째, AttributeError는 해당 이미지에 EXIF 데이터가 없을 때 처리된다.
    exif = dict(image.getexif().items()) 에서 발생한다.

  • 두번째, KeyError는 EXIF 데이터가 있지만 orientation 데이터가 없을 때 처리된다.
    exif[orientation]에서 발생한다.

마무리


이렇게 EXIF 내의 Orientation 정보를 통해 사진 기울기를 보정하는 작업을 진행해보았다.

나의 경우에서 처음엔 백엔드(나)가 처리했다가...
사실은 PIL이 좀 느린 이슈가 있고, 백엔드에서 처리하기엔 애매해서 결국 프론트엔드에서 처리되었다.
비록 현재는 사장된 코드지만 ^^
프레임워크를 떠나 지극히 근본적인 부분에서 생긴 이슈를 통해 알아갈 지식이 더욱 방대해졌음을 깨닫았다.

참고한 사이트는 ... 링크
끗!

profile
백엔드 응애

0개의 댓글

관련 채용 정보