豪鬼メモ

一瞬千撃

アスペクト比と保存サイズ

ニコンとキャノンとパナソニックがフルサイズのミラーレスカメラを出すと発表したので、どんなのが出てくるかちょっと楽しみだ。「好きなカメラを買っていい券」をここまで温存してきた甲斐があるというものだ。像面位相差AFとチルト液晶と手ぶれ補正を備えたE-M5的な大きさの機種がオリンパスから出るのを待っていたが、出そうにないので諦めていたところだ。スペックが発表されているニコン機とキャノン機、そしてすでに市場にあるソニー機を見ると、やはり先行しているソニーが良さげだ。像面位相差でチルト液晶で手ぶれ補正がついて小さい筐体でセンサー性能が優秀なので。しかし、ソニー機はカメラ内RAW現像ができないのがネックで、出先でシェア的な使い方をする妻との共用を考えると採用しづらい。常にRAW+JPEGで撮るのも馬鹿馬鹿しい。そうすると、同様の機能でセンサー性能が優秀っぽくてカメラ内RAW現像ができるニコン機が欲しくなるけど、高くてでかい。ソニーがカメラ内RAW現像に対応するか、ニコンが小型な廉価版を出すかしてくれればいいのだが、どうなることやら。いったい私はいつになったらカメラを買い換えられるのか。つか、オリ頑張れよオリ。

閑話休題。最近は撮った写真を様々なサイズにトリミングして保存するので、アスペクト比ごとの最適サイズについてまた整理してみた。個人用の写真管理プログラムをRubyからPythonに移植するついでに、サイズ判定部分についても考え直してみた。以前の記事と述べていることはほぼ同じである。
f:id:fridaynight:20180815172822j:plain


総有効画素数がわかれば、√(画素数 / 短辺比率 * 長辺比率) で長辺のサイズが出て、√(画素数 / 長辺比率 * 短辺比率) で短辺のサイズが出る。例えば画素数16Mピクセルアスペクト比4:3のオリンパスE-M10は、長編4618ピクセルで短辺3464ピクセルくらいになるはずだ。画素数30Mピクセルだという噂のEOS-Rは、長辺7745ピクセルで短辺4472ピクセルくらいになるはずだ。ところで、私はGoogle Photosに写真をTIFF形式で保存しているのだが、無料枠だと1枚のデータ量が50MBまでという制限がある。50MBは8ビット深度のTIFFだと、50M / (8+8+8) で、16Mピクセルが上限になる。16ビット深度だとその半分で8Mピクセルということになる。実際はZIP圧縮TIFFにすると16ビット深度の10Mピクセルまでなら50MBに収まることが多いので、それがベストソリューションだと考えている。

で、16MP、10MP、8MPのそれぞれに画像をリサイズして保存する際に、長辺と短辺はいくつにすれば良いのか。まずはその画像のアスペクト比を知る必要がある。実際の画像サイズを調べて、1:1や3:2や4:3などのクリーンなアスペクト比に近い(誤差1%以内)なら、そういったクリーンな比であるとみなす。そうでない場合、実際の比を約分して使う。アスペクト比がわかれば、先ほどと同様の式で保存サイズを導けば良いわけだが、その際に各辺の長さをキリのいい数値にしたい。後でさらにリサイズした場合の画質劣化を最小限に抑えたいからだ。まず、アスペクト比が単純な場合には、長辺と短辺の比が正確になるように配慮する。そして、長辺、短辺とも、できれば60で割り切れてほしくて、無理なら12、4、2と可能性を探る。それでいて、実際の保存サイズが理論的な最大サイズの90%より小さくなってほしくはない。以上の条件を満たす設定の一覧を出すPythonスクリプトは以下のものになる。

#! /usr/bin/python

import math
import fractions

def clean_image_aspect(width, height):
  actual_ratio = float(width) / height
  clean_aspects = ((1, 1), (7, 6), (5, 4), (4, 3), (7, 5), (3, 2), (8, 5), (16, 9),
                   (2, 1), (12, 5), (3, 1), (4, 1), (5, 1), (6, 1))
  margin_ratio = 1.01
  for clean_aspect in clean_aspects:
    clean_ratio = float(clean_aspect[0]) / clean_aspect[1]
    if actual_ratio >= clean_ratio / margin_ratio and actual_ratio <= clean_ratio * margin_ratio:
      return clean_aspect
  for clean_aspect in clean_aspects:
    clean_aspect = (clean_aspect[1], clean_aspect[0])
    clean_ratio = float(clean_aspect[0]) / clean_aspect[1]
    if actual_ratio >= clean_ratio / margin_ratio and actual_ratio <= clean_ratio * margin_ratio:
      return clean_aspect
  gcd = fractions.gcd(width, height)
  return int(width / gcd), int(height / gcd)

def shrink_image_size(max_area, width, height):
  if width * height <= max_area:
    return width, height
  aspect = clean_image_aspect(width, height)
  is_simple = aspect[0] <= 20 and aspect[1] <= 20
  ratio = float(aspect[0]) / aspect[1]
  max_width = int(math.sqrt(max_area * aspect[0] / aspect[1]))
  max_height = int(math.sqrt(max_area * aspect[1] / aspect[0]))
  tries = int(max_width * 0.04)
  dividers = [60, 12, 4, 2, 1]
  for divider in dividers:
    for i in range(0, tries):
      width = max_width - i
      if width <= 1000:
        break
      if width % divider != 0:
	continue
      height = width / ratio
      if is_simple and height != int(height):
        continue
      height = int(height)
      if height % divider != 0:
        continue
      return width, height
  return max_width, max_height

for mp in [16, 10, 8]:
  print("%dMP" % mp)
  aspects = [(1, 1), (7, 6), (5, 4), (4, 3), (7, 5), (3, 2), (8, 5), (16, 9),
             (2, 1), (12, 5), (3, 1), (4, 1), (5, 1), (6, 1)]
  for aspect in aspects:
    (width, height) = shrink_image_size(mp * 1000000, aspect[0] * 10000, aspect[1] * 10000)
    print("|%d|%d|%d|%d|%d" % (aspect[0], aspect[1], width, height, width * height))


結果として、8ビットTIFF用の16Mピクセルだと、この設定がよさげ。Google PhotoはJPEGでも16Mピクセルが上限となるので、この値を使うといいだろう。

長辺比 短辺比 長辺サイズ 短辺サイズ 素数
1 1 3960 3960 15681600
7 6 4172 3576 14919072
5 4 4440 3552 15770880
4 3 4560 3420 15595200
7 5 4620 3300 15246000
3 2 4860 3240 15746400
8 5 4992 3120 15575040
16 9 5184 2916 15116544
2 1 5640 2820 15904800
12 5 6192 2580 15975360
3 1 6840 2280 15595200
4 1 7920 1980 15681600
5 1 8700 1740 15138000
6 1 9720 1620 15746400

16ビットTIFFのZIP圧縮で投機的に保存するならこの10Mピクセル設定を使う。これで保存して50MBを超えてしまった場合には、8Mピクセルにフォールバックすればよい。

長辺比 短辺比 長辺サイズ 短辺サイズ 素数
1 1 3120 3120 9734400
7 6 3360 2880 9676800
5 4 3480 2784 9688320
4 3 3600 2700 9720000
7 5 3696 2640 9757440
3 2 3780 2520 9525600
8 5 3936 2460 9682560
16 9 4160 2340 9734400
2 1 4440 2220 9856800
12 5 4896 2040 9987840
3 1 5400 1800 9720000
4 1 6240 1560 9734400
5 1 6900 1380 9522000
6 1 7560 1260 9525600

8ビットTIFFを保存するならこれ。確実に50MB以下のサイズになるはずだ。

長辺比 短辺比 長辺サイズ 短辺サイズ 素数
1 1 2820 2820 7952400
7 6 2940 2520 7408800
5 4 3120 2496 7787520
4 3 3264 2448 7990272
7 5 3276 2340 7665840
3 2 3420 2280 7797600
8 5 3552 2220 7885440
16 9 3648 2052 7485696
2 1 3960 1980 7840800
12 5 4320 1800 7776000
3 1 4860 1620 7873200
4 1 5520 1380 7617600
5 1 6300 1260 7938000
6 1 6840 1140 7797600


こうして出てきたサイズをもとに画像を保存してから、Google Photosやその他のサービスにアップロードすればいい。、Lightroomの書き出し時にリサイズしてもいいし、一括で書き出してからImageMagick等でリサイズしてもいい。バッチで回してアスペクト比の判定とリサイズを同時に行うスクリプトはこれから書く。