戯言日記

Rの話だと思ったら唐突にサバゲーが混じってくる何か。

pngの保存形式が変わったと思ってたらggsave()の保存形式が変わっていた

去年の後半に作ったShinyアプリを久しぶりに動かしたら、処理の途中でいきなり落ちるとかいう恐ろしい目にあった。

具体的にはggplot2で作図したグラフをggsave()でpng保存し、それをEBImageパッケージを使って読込と解析をするアプリなのだが、その途中でアプリが落ちてしまった。
なお、一連の動作を全て纏めて走らせる形にしていたので、自作関数やらモジュール分割やらで原因まで辿り着くのに時間がかかって大変だった。

原因だが、直接の原因はpngのチャンネル数がアプリを組んだ時と変わっていたことで、以前は3チャンネル(RGB)だったのがいつの間にか透過レイヤー込みの4チャンネルになっていた。
で、その根本的な原因がggplot2パッケージに含まれているggsave()のアップデートだったっぽい。

チャンネル数の変化

チャンネル数の確認はEBImageパッケージ1で実施。
これに関しては見られれば何でもいい2

library(tidyverse)
library(EBImage)

g <- iris %>%
  ggplot(aes(Sepal.Length, Sepal.Width, colour=Species)) +
  geom_point() +
  theme_void()

ggsave(filename = "dat.png", plot = g)
readImage("dat.png") %>% channel("rgb")


以前の結果がこれ。
frames.totalがチャンネル数である。

> readImage("dat.png") %>% channel("rgb")
Image 
  colorMode    : Color 
  storage.mode : double 
  dim          : 640 360 3 
  frames.total : 3 
  frames.render: 1 

imageData(object)[1:5,1:6,1]
     [,1] [,2] [,3] [,4] [,5] [,6]
[1,]    1    1    1    1    1    1
[2,]    1    1    1    1    1    1
[3,]    1    1    1    1    1    1
[4,]    1    1    1    1    1    1
[5,]    1    1    1    1    1    1


それがこうなっていた。

> readImage("dat.png") %>% channel("rgb")
Image 
  colorMode    : Color 
  storage.mode : double 
  dim          : 640 360 4 
  frames.total : 4 
  frames.render: 1 

imageData(object)[1:5,1:6,1]
     [,1] [,2] [,3] [,4] [,5] [,6]
[1,]    1    1    1    1    1    1
[2,]    1    1    1    1    1    1
[3,]    1    1    1    1    1    1
[4,]    1    1    1    1    1    1
[5,]    1    1    1    1    1    1


Deviceの問題かと思ったのだが、RStudioのGlobal Option → General → Graphicsで設定を切り替えても変わらないので違うっぽい。
さらに、Rネイティブなpng()ではRGBの3チャンネルで問題なく出力できた。

ggsave("test_gg.png", g)
png("test_windows.png", type="windows"); g; dev.off()
png("test_cairo.png", type="cairo"); g; dev.off()
png("test_cairo-png.png", type="cairo-png"); g; dev.off()

c("test_gg.png", "test_windows.png", "test_cairo.png", "test_cairo-png.png") %>% 
  enframe(name=NULL) %>% 
  mutate(frame = value %>% map_dbl( ~.x %>% readImage() %>% numberOfFrames()) )
# A tibble: 4 x 2
  value              frame
  <chr>              <dbl>
1 test_gg.png            4
2 test_windows.png       3
3 test_cairo.png         3
4 test_cairo-png.png     3


ggsave()だけチャンネル数が増えているので、ggsave()かggplot2そのものが怪しい。
その前提でコードを眺めていて、何となくtheme_void()を外したら3チャンネルで出力された(出力結果は割愛)。
theme_void()はグラフの要素を全て消す設定だが、もしかして各要素を透過して見えないようにしているだけってこと……?

という訳で、theme_void()の中身やらggsave()のリファレンスやらを確認したところ、どうも背景色の設定が影響しているっぽい。

ggsave()で背景色を指定するbgオプションはデフォルト設定(bg=NULL)だとplot.backgroundのfillに指定した色を利用するのだが、theme_void()では色を設定しておらず3、その場合はtheme()の仕様としてNULLになる4

よって、ggplot2側でplot.backgroundの設定をするか、ggsave()側でgb="white"などのような設定をすれば透過レイヤ―が無くなるため、チャンネル数が3つで確定する。

g <- iris %>%
  ggplot(aes(Sepal.Length, Sepal.Width, colour=Species)) +
  geom_point() +
  theme_void() +
  theme(plot.background = element_rect(fill="white"))

ggsave("test_gg_bg.png", g, bg="white")


挙動が変わった理由

そもそもggsave()自体は冒頭で言及したShinyアプリで以前から使っていたので、何かアップデートに絡んで挙動が変わったんだろうと予想。
その方向性で情報を漁ってみたのだが、2020年1月のHadley神によるggsave()での保存時に関する提案をしたのがたぶん出発点……だと思う。

github.com

その後、いくつかやり取りがあって、最終的に背景色をthemeから引っ張ってくる形でまとまったっぽい。

github.com

github.com

github.com

機能としては2021-06-16のggplot2 3.3.4でリリースされた(Fixesの8番目に記載有り)。
思っていたより最近のアプデだったらしい。

ggplot2.tidyverse.org

なお、嫌な予感がして仕事のログを確認したら、冒頭のアプリを動かしてエラーを起こしたのがまさに3.3.4のリリース日だった。
そんなタイムリーなことってある???

アプデが最近だったこともあって、ここ最近も関連する話題が出ている模様。
気になる人は追ってみても面白いかも。

github.com

github.com

Enjoy!!


  1. Bioconductorに含まれている画像解析用のパッケージ。日本語の記事はそこまで多くないけど、比較的使いやすい印象。

  2. 画像解析ならPython使えよってツッコミはさておき、よく見かけるのはimagerパッケージとか。あとopencvパッケージも個人的に気になっている。更新止まったかもなぁとか思ってたら最新のPublishedが2021-05-04に来ているので、ちょこちょこ進めているのかも知れない。

  3. というかほとんどのテーマでplot全体の背景色は設定していないっぽい。グラフエリアの背景色は大体設定されているので混乱しそうになる。

  4. 今回見ているtheme_void()やよく使われるtheme_gray()などのtheme*()関数については、theme()で設定できる全てのパラメータがNULLになっているtheme(theme_all_null)に対して必要なところだけ上書きするような形で設定しているため、こういう挙動になる。……ということを今回初めてtheme()関連の仕組みを調べて知って、軸の設定やら何やらを弄った後でtheme*()を使うとリセットされるのはそういうことだったんだなっていうド素人ムーブをかましてました。