画像の輝度を調整する

画像の輝度を任意の値に合わせる方法を考えます。例えばある画像の平均輝度が 0.5 だとして、これを 0.3 など別の値になるように調整したい、ということです。

一見すると、この例の場合だと画像の全ピクセルの画素値を -0.2 すればいいだけのように思えるかもしれませんが、これは上手くいきません。画素値が元から 0.2 より小さなピクセルの画素値は、変換後も 0 で下げ止まりにする必要があります。そのようなピクセルが存在する場合、輝度を一律にシフトさせても全てのピクセルの輝度が 0.2 だけ下がるわけではないため、平均輝度の変化量も -0.2 にはなりません。

ここではガンマ補正を用いた方法でこの問題に取り組みましょう。
ガンマ補正とはピクセルの輝度をある指数のべき乗で変換することです。式で書くと

Output = Input^a
(Input: 変換前の画素値、Output: 変換後の画素値、a: 実数値)

画像内の全てのピクセルについてこの変換を行います。指数 a の値によって画像がどう変わるかを調整できます。指数が 1 より大きければ輝度は低下し、1 より小さければ輝度は増加します。

画像を明るくしたい(目標とする輝度が画像の現在の輝度よりも大きい)のなら1より小さい指数で、画像を暗くしたい(目標とする輝度が画像の現在の輝度よりも小さい)のなら1より大きい指数で、画像をべき乗してやれば、画像の輝度は目標とする輝度に近付くことになります。

問題は、指数として具体的にどの値を与えればよいのか、ということです。指数の値に適当な初期値を与え、その指数における画像の平均輝度と目標輝度の誤差を計算し、その誤差を減らすように指数の値を更新する、というアルゴリズムによって、指数の値を段階的に真値に近づけるという方法を使いましょう。

作成した関数をまず示します。

luminance = function( cimg, weight = "Lab" ){
  if( spectrum( cimg ) < 3 ){
    return( imsub( cimg, cc == 1 ) )
  } else if( spectrum( cimg ) > 3 ){
    cimg = imsub( cimg, cc < 4 )
  }
  
  # For definitions of lightness formulas, see https://en.wikipedia.org/wiki/HSL_and_HSV#Lightness
  if( is.character( weight ) ){
    if( weight == "Lab" ){
      return( as.array( imsub( sRGBtoLab( cimg ), cc == 1 ) * 0.01 ) )
    } else if( weight == "HSI" ){
      return( as.array( imsub( RGBtoHSI( sRGBtoRGB( cimg ) ), cc == 3 ) ) )
    } else if( weight == "HSV" ){
      return( as.array( imsub( RGBtoHSV( sRGBtoRGB( cimg ) ), cc == 3 ) ) )
    } else if( weight == "HSL" ){
      return( as.array( imsub( RGBtoHSL( sRGBtoRGB( cimg ) ), cc == 3 ) ) )
    } else if( weight == "Luma" ){
      return( 0.3 * as.array( R( cimg ) ) + 0.59 * as.array( G( cimg ) ) + 0.11 * as.array( B( cimg ) ) )
    } else {
      print( "Caution: set a proper value for weight." )
      return( as.array( cimg ) )
    }
  } else if( is.numeric( weight ) & length( weight ) == 3 ){
    weight = weight / sum( weight )
    return( weight[1] * as.array( R(cimg) ) +weight[2] * as.array( G(cimg) ) + weight[3] * as.array( B(cimg) ) )
  } else {
    print( "Caution: set a proper value for weight." )
    return( as.array( cimg ) )
  }
}

lum.mean = function( cimg, weight = "Lab" ){
  return( mean( luminance( cimg, weight = weight ) ) )
}

lum.match = function( cimg, target.lum = NULL, weight = "Lab", return.image = T, save.dir = "NULL" ){
  if( !is.null( save.dir ) ){
    dir.create( file.path( save.dir ), showWarnings = F )
  }
  if( class( cimg )[ 1 ] != "list" ){
    cimg = list( image.png = cimg )
  }
  if( is.null( target.lum ) ){
    target.lum = mean( unlist( lapply( cimg, lum.mean ) ) )
  }
  
  progress = 1
  staging = unique( floor( seq( from = 1, to = length( cimg ), length.out = 5 ) ) )
  cat( "[lum.match] Progress: " )
  for( i in 1:length( cimg ) ){
    # Display progress
    if( i == staging[ progress ] ){
      cat( paste0( sprintf( "%1.0f", 100 * ( i - 1 ) / length( cimg ) ), "% ... " ) )
      progress = progress + 1
    }
    # Matching routine
    minimumError = 1 / 1000
    shiftValue = 0.5
    power = 1
    prevDiff = 0
    while( T ){
      current = lum.mean( cimg[[ i ]]^power, weight )
      diff = current - target.lum
      if( abs( diff ) < minimumError ){
        break;
      }
      if( sign( diff ) * sign( prevDiff ) < 0 ){
        shiftValue = shiftValue / 2
      }
      power = power + sign( diff ) * shiftValue
      prevDiff = diff
    }
    matched.image = cimg[[ i ]]^power
    if( return.image ){
      cimg[[ i ]] = matched.image
    }
    if( ! is.null( save.dir ) ){
      imager::save.image( matched.image, paste0( save.dir, "/", names( cimg )[ i ] ) )
    }
  }
  cat( "done!\n" )
  
  if( return.image ){
    if( length( cimg ) == 1 ){
      return( cimg[[ 1 ]] )
    } else {
      return( cimg )
    }
  }
}

luminance 関数は cimg クラスの変数を引数にとり、輝度値の配列(array クラス)を返します。weight は輝度の計算にどの色空間を使うかのオプションです。デフォルトでは Lab 空間を使います。

lum.mean 関数は cimg クラスの変数を引数にとり、平均の輝度値を返します。

lum.match 関数は cimg クラスの変数、または cimg のリストを引数に取ります。target.lum は目標とする輝度の値です。return.image は輝度変換後の画像を返り値として返すかのフラグです。save.dir には変換後の画像を保存するディレクトリを指定します。指定しない場合は画像の保存を行いません。

次に使用例です。

lum.mean( boats ) # 平均輝度の計算
# [1] 0.5510842

# 輝度の調整
layout( t( 1:3 ) )
plot( boats, rescale = F, main = "original" )
plot( lum.match( boats, target.lum = .8 ), rescale = F, main = "0.8" )
plot( lum.match( boats, target.lum = .2 ), rescale = F, main = "0.2" )
layout( 1 )


元画像の平均輝度はおよそ 0.55 です。これを 0.8 や 0.2 に調整しています。

lum.match 関数には複数の画像をリストとして入力することができます。複数の画像について、各画像の平均輝度の平均を計算し、その輝度になるよう各画像の調整を行います。結果的に、入力されたすべての画像が同じ平均輝度を持つようになります。

load.img.dir = function( dir ){
  names = list.files( dir, pattern = "\\.(jpg|jpeg|png|bmp|JPG|JPEG|PNG|BMP)$" )
  l = vector( "list", length( names ) )
  for( i in 1:length( names ) ){
    l[[ i ]] = load.image( paste0( dir, "/", names[ i ] ) )
    names( l )[ i ] = names[ i ]
  }
  return( l )
}

images = lum.match( load.img.dir( "img" ), return.image = T, save.dir = "output" )

load.img.dir は指定されたディレクトリ内にあるすべての画像を読み込んで cimg リストを返す関数です。この例では img という名前のフォルダー内にある画像を読み込んで輝度マッチングを行なっています。変換後の画像は images という変数に返され、同時に output という名前のフォルダを作りその中に作成された画像たちが保存されます。

コメント