✅ 이미지 리사이징
현재 카메라로 사진을 찍고 앱에 저장하면 사진 하나에 3MB로 앱의 사이즈가 지속적으로 커지는 문제가 있다.
서버에 저장한다고 해도 여러 사용자가 하나의 3MB의 사진을 올린다? 누가봐도 문제가 있어 보인다.
사진 하나의 용량이 큰 이유는 아래와 같다.
일반적으로 촬영되어 저장된 사진의 경우 카메라 촬영 시 한 장의 이미지가 아닌 순간적으로 여러 장의 이미지를 합성하여 화질을 개선해주는 솔루션을 사용하고 있으며, 일반 사진 모드는 화면 비율에 따라 차이가 있을 수 있지만 일반적으로 3:4 화면 비율 기준 약 2 ~ 3MB 정도의 사진 크기로 저장되며, 고화소(50/108MP) 사진 촬영의 경우 약 20 ~ 30MB 정도의 사진 크기로 저장되고 있습니다.
https://r1.community.samsung.com/t5/갤럭시-s/스마트폰-마다-사진-용량이-다른-이유가-뭔가요/td-p/18817391
리사이즈(Resize) / 리사이징(Resizing)
핸드폰으로 사진을 찍으면 해당 사진의 해상도는 보통 3000X4000
이다.
안드로이드 UI 기준으로 너무 높은 해상도를 갖고 있고, 이로 인해 사이즈 또한 매우 크기 때문에 Out Of Memory
발생 위험 또한 높아진다.
따라서 사진의 해상도를 낮춰 줄 필요가 있는데 이를 Reszie
라고 한다.
압축(Compress)
이미지 파일을 더 작게 만드는 프로세스 (이미지 최적화)
바이트 단위의 정보를 제거하거나 이미지 압축 알고리즘을 사용하여 저장 공간을 덜 차지하는 방식으로 이미지 파일을 다시 작성하는 방식으로 작동
이미지가 빠르게 로드되도록 하는 효과적인 방법
압축 방식
- 손실 압축
- 쳐낼 거 쳐내면서 압축하여 압축률을 높임
- 완벽한 원복 복원이 불가능
- JPEG, MP3, MPEG 등
- 무손실 압축
- 정보를 유지하며 압축
- 완벽한 복원이 가능한 대신 압축률이 낮음
- ZIP, GZIP, RAR, PNG, FLAC 등
파일 형식 종류
- JPEG(Joint Photographic Experts Group)
- 가장 일반적으로 사용되는 이미지 파일 형식 중 하나로, 압축된 파일로 이미지를 저장합니다. JPEG는 압축률이 높아 저장 공간을 적게 차지하면서도 고화질 이미지를 제공합니다.
- JPG 압축 과정에서 경계나 선이 뭉개지거나 흐려질 수 있기 때문에 Compression artifact이 발생할 소지가있는 일러스트, 드로잉, 텍스트(선명한 경계와 선이 많이 들어간 이미지)보다 사진에 적합합니다.
- PNG
- 무손실 압축 형식으로 투명한 배경을 지원합니다. 이미지 편집을 위한 레이어 형식의 파일로 사용하기도 합니다.
- WebP
- 구글에서 개발한 이미지 파일 형식으로 JPEG, PNG, GIF와 같은 기존 형식들의 단점을 보완하며 압축률과 이미지 품질이 우수합니다.
- Lossy
- WebP의 lossy compression 알고리즘은 VP8의 key frame encoding 기반으로 동작하며, JPEG 대비 25-34% 높은 압축률을 보인다.
- Lossless
- WebP의 lossless 알고리즘은 새롭게 고안된 방식으로, 여러 과정의 transform 후에 LZ777을 활용한 entropy coding을 사용한다. PNG 대비 26% 높은 압축률을 보인다.
테스트 결과 WEBP_LOSSY로 압축했을 때의 결과가 제일 좋았다.
그러나 WEBP_LOSSY
는 API 30 이상부터 사용 가능하기 때문에 30 미만은 그 다음 결과가 좋은 JPEG
로 압축
구현
1. 리사이징 및 압축 이미지를 저장 할 파일 생성
val storage = context.filesDir
val fileName = "${System.currentTimeMillis()}.jpg"
val imageFile = File(storage, fileName)
imageFile .createNewFile()
val fos = FileOutputStream(imageFile ) //파일에 쓰기 위한 객체 생성
2. 이미지 리사이징
val input = BufferedInputStream(context.contentResolver.openInputStream(uri))
input.mark(input.available()) //입력 스트림의 특정 위치 기억
var bitmap: Bitmap?
BitmapFactory.Options().run {
// 1. 이미지 정보 설정
inJustDecodeBounds = true // 메모리 할당 방지
bitmap = BitmapFactory.decodeStream(input, null, this)
input.reset() // 입력 스트림의 마지막 mark() 위치로 재설정
// 2. 사이즈 축소
inSampleSize = calculateInSampleSize(this)
inJustDecodeBounds = false
bitmap = BitmapFactory.decodeStream(input, null, this)
// 돌아간 이미지 원상복구
if (bitmap != null) bitmap = rotateImageIfRequired(bitmap = bitmap!!, uri = uri)
}
input.close()
BufferedInputStream 클래스는 인스턴스가 만들어질때 내부에 버퍼링 배열을 만듭니다.
스트림의 바이트가 입력되면 스트림으로부터 필요에 따라 한번에 다수의 바이트가 내부 버퍼에 추가 됩니다.
available() : 객체로부터 읽어올 수 있는 바이트 수를 반환
mark() : 입력 스트림의 특정 위치를 기억합니다.
reset() : 입력 스트림의 마지막 mark 된 위치로 재설정 됩니다.
- BitmapFactory: Bitmap 객체를 생성
- 이미지 정보 설정
inJustDecodeBounds
값을 true
로 설정했으므로 실제로 Bitmap 객체가 만들어지지 않는다.
그 대신 읽은 이미지의 각종 정보가 option 객체에 설정된다.
- 사이즈 축소
private fun calculateInSampleSize(options: BitmapFactory.Options): Int {
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > MAX_HEIGHT || width > MAX_WIDTH) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
while (halfHeight / inSampleSize >= MAX_HEIGHT && halfWidth / inSampleSize >= MAX_WIDTH) {
inSampleSize *= 2
}
}
return inSampleSize
}
https://developer.android.com/topic/performance/graphics/load-bitmap?hl=ko
- inSampleSize: 이미지 해상도/inSampleSize
- ex) 2048×1536 / 4 >>변환>> 512×384
3. 이미지 압축
if (Build.VERSION.SDK_INT >= 30) {
// quality(0~100) 낮을 수록 사이즈 작아짐 대신 화질구지
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, fos)
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
}
//bitmap은 heap영역에 쌓여 GC로 메모리 정리가 안되기 때문에 recycle()로 정리를 해줘야 함
bitmap.recycle()
4. 마무리
fos.flush() // 버퍼에 남아있는 데이터 출력 후 비우기 >> close 전에 선행되어야 함
fos.close()
이미지 압축이란? | 알고리즘 및 기술 | Cloudflare
'안드로이드 > 이미지' 카테고리의 다른 글
[안드로이드] 이미지 Glide (0) | 2023.11.03 |
---|