【MATLAB】figureの内側の余白を調整し、そのまま、見栄えの良い画像ファイルに変換する

:::note warn
ご注意

　<font color="#f00">余白調整後のfigureでは、zoom操作が効かなくなります。</font>
 
　最終的に画像としてのファイル化が必要になるまでは、figure_trimmer の行はコメント化しておくのが賢明です。
:::

## はじめに
　MATLAB の figure は、昔に比べると解像度が各段に良くなっています。そのため、Qiitaなどに掲載する図版は、スクリーンキャプチャしたものを適当にトリミングして使うことが多くなっていました。

 　ところが、老化による指の震えもあり、画像編集アプリを使った手作業によるトリミングは、だんだんと面倒に感じるようになってきました。また、スクリーン上の figure の解像度は、通常は十分に満足できていますが、目が肥えてきたせいか、ときどき、ビットマップ的な細かいことが気になるようになってきました。しかし、スクリーン上に不満がある場合でも、画像ファイルとして print 出力すれば綺麗な結果が得られます。

　そこで、トリミングの手間の軽減と、綺麗な図版の獲得のために、figure 内の余白を調整するローカル関数を作ってみました。

使用環境：
　MATLAB R2019a（Toolbox不使用）

## 使い方と効果
　図版化したい figure をアクティブにしておいて、次のコマンドを入力するだけです。```dtop```,```dbottom```, ```dleft```,```dright``` は、上,下,左,右 の余白の調整量です。増量は正、減量は負の値とし、pixel単位で記述します。
```
figure_trimmer (gcf,[dtop dbottom dleft dright]);
```
この結果は、例えば次のようになります。

　中身が横長の図になって、上下の余白が大きすぎるとき、```[-80 -80 0 0]``` とすれば、図版のスペースが減る分だけ、無駄な上下スクロールも少なくなって記事も読みやすくなります。

**図１**
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/19a2393d-5ced-2680-72df-3f6e4b89526e.gif">

　逆に、中身が縦長の図になるときは、```[0 0 -150 -160]``` とすれば、スリムで見やすい図になります。Qiita のような Markdown の文書内では、図版の左右への文字の回り込みができないので、あまり得にはなりませんが、ワープロの一般文書ではページ数が減って助かります。

**図２**
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/e4f2fea7-06fc-ec63-cada-be2000f17161.gif">

 　subplot 図で6行3列の枠を予約しながら、余ってしまってできた下2行分の不自然なスペースや、figure の圏外に打ち上げてしまった残念な花火も、```[70 -110 0 0]``` とすれば簡単に解決します。

**図３**
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/0381bbf7-c3e4-0f1d-14a9-7693af923ddc.gif">

　最後に、本来の目的だった綺麗な画像ファイルの結果も掲載しておきます。図１～３の「after」の状態で、下記の操作をして得られたものです。

```
print(gcf,'-dpng','出力ファイル名.png','-r600')
```
 
図３では、3, 4行目の図枠の幅がやや狭くなっていて気になりますが、図６では問題なく表示できています。

**図４**
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/2d7a7a3f-1edf-5ec6-962a-a958cd012e83.png">

**図５**
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/210fb8d8-4af6-79f7-ac65-157e989fa604.png">

**図６**
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/64d1b590-152b-3ac5-73b7-bd40b21b8d5e.png">

## おわりに
　黙っていようと思いましたが、良心が咎めるので白状しておきます。実は、ここで紹介した方法は、**図の中に annotation コマンドで作ったオブジェクトがある場合には使えません**。

 　annotation は figure の一人前の Children ではないようです。"allchild" を指定すると現れますが、一つもプロパティを持っていない特殊な存在なので、制御しようにも手も足も出ません。

## プログラム

### ローカル関数部

```MATLAB
% ==========================================
% ==========================================
% figureの上下左右の余白調整用のローカル関数
% ==========================================
% ==========================================

function figure_trimmer (hf,KK)

  % 【入力】
  % 　hf:  余白調整したいfigureのハンドル（呼び出し側では常にgcf固定）
  % 　KK:  余白の変更量のベクトル。 単位は[pixel]。
  % 　     　（[上余白 下余白 左余白 右余白]。増量は+、減量は-）
  % 
  % 【出力】
  % 　戻り値は無し

  posf=hf.Position;            % 余白変更前のfigureのposition
                               % 　[左端 下端 幅 高さ]、単位は[pixel]。
  naxe=size(hf.Children,1);    % figure内のChildren（axesやcolorbar）
                               % 　などの総数。

  % 全Childrenのposition行列を作成
  % 　各行がそれぞれのChildrenに対応。
  % 　各列は[左端 下端 幅 高さ]、単位は[normalized]。

  Posa=[];                     % position行列の初期値
  for n=1:naxe
    axes(hf.Children(n))
    hn(n)=gca;
    Posa=[Posa ; hn(n).Position];
  end

  % 各Childrenの、余白変更前のfigure内でのpixel単位でのPositionを計算。
  Posp(:,[1 3])=Posa(:,[1 3])*posf(3);
  Posp(:,[2 4])=Posa(:,[2 4])*posf(4);
  
  % 余白変更後のfigureのpixel単位でのPositionを計算。
  posfx=posf+[-KK(3) -KK(2) KK(3)+KK(4) KK(1)+KK(2)];
  
  % 余白変更後のfigure内での各Childrenのpixel単位でのPositionを計算する。
  Pospx(:,1)=Posp(:,1)+KK(3);
  Pospx(:,2)=Posp(:,2)+KK(2);
  Pospx(:,3)=Posp(:,3);
  Pospx(:,4)=Posp(:,4);
  
  % 余白変更後の各Childrenのnormalized単位でのPositionを計算する。
  Posax(:,[1 3])=Pospx(:,[1 3])/posfx(3);
  Posax(:,[2 4])=Pospx(:,[2 4])/posfx(4);
  
  % figureとChildrenのpositionを更新
  hf.Position=posfx;
  for nn=1:naxe
    hn(nn).Position=Posax(nn,:);
  end

end

```


### 利用例の全体

　冗長な部分も多いので、折り畳んであります。それでも見たい方はクリックしてください。

<details><summary>←ここをクリック</summary>

```MATLAB
% figure_arrange04.m

% figureの上下左右の余白調整用のローカル関数"figure_trimmer"の利用例

clear
close all

% このプログラムファイルのベース名を取得
% 　（figureのファイル出力名の一部に使うため）
basename=mfilename;

% デモ用のgifアニメのためのスクリーンキャプチャの範囲 [pixel]
% 　（本題の余白の調整量とは無関係）
Poss=[353 121 574.7 577.3];

% ============================
% 余白調整用のサンプル図の描画
% ===========================

vv=1;                     % 正弦波の実効値 [pu]
tt=linspace(0,10,200);    % 位相ベクトル [rad]

% ■ サンプル図1
figure(1)

% 余白調整後もfigure全体がスクリーン内に収まるように、
% 　figureの位置を既定よりも若干下げておく。
fpos=get(gcf,'Position');
fpos=fpos+[0 -150 0 0];
set(gcf,'Position',fpos);

plot(tt,sqrt(2)*vv*sin(tt),'LineWidth',2);
hold on
plot(tt,sqrt(2)*vv*sin(tt-2*pi/3),'LineWidth',2);
plot(tt,sqrt(2)*vv*sin(tt-4*pi/3),'LineWidth',2);
axis equal
axis([0 10 -2 2]);
grid on
title('サンプル図形')
xlabel('位相[rad]')
ylabel('出力[pu]')

% デモ用gifアニメのためのスクリーンコピー（オリジナル図1）
commandwindow;            % コマンドウィンドウを背景にする。
figure(1);
fig_capture(gcf,Poss,10); % スクリーンキャプチャ（ローカル関数）

% ■ サンプル図2
figure(2)
set(gcf,'Position',fpos);
plot(sqrt(2)*vv*sin(tt),tt,'LineWidth',2);
hold on
plot(sqrt(2)*vv*sin(tt-2*pi/3),tt,'LineWidth',2);
plot(sqrt(2)*vv*sin(tt-4*pi/3),tt,'LineWidth',2);
axis equal
axis([-2 2 0 10]);
grid on
title('サンプル図形','FontSIze',10)
xlabel('入力[pu]')
ylabel('位相[rad]')

% デモ用gifアニメのためのスクリーンコピー（オリジナル図2）
commandwindow;
figure(2);
fig_capture(gcf,Poss,20);

% ■ サンプル図3
figure(3)
set(gcf,'Position',fpos);
for nn=1:12
  subplot(6,3,nn);
  plot(tt,sqrt(2)*vv*sin(tt),'LineWidth',2);
  hold on
  plot(tt,sqrt(2)*vv*sin(tt-2*pi/3),'LineWidth',2);
  plot(tt,sqrt(2)*vv*sin(tt-4*pi/3),'LineWidth',2);

  axis equal
  axis([0 10 -1.9 1.9])
  camva(3.0)
  grid on

  if ~isempty(find(nn==[1 2 3]))
    plot([5 5],[0 5],'r:','Clipping','off','LineWidth',2);
    ran=rand(3,100);
    R=3*sqrt(ran(1,:));
    A=2*pi*ran(2,:);
    col=jet;
    C=floor(63*ran(3,:)+1);
    for n=1:size(ran,2)
      plot(5+R(n)*cos(A(n)),5+R(n)*sin(A(n)),'p','Clipping','off', ...
                       'MarkerSize',10,'MarkerFaceColor',col(C(n),:));
    end
  end

  if ~isempty(find(nn==[1:9]))
    xticklabels('');
  end

  if ~isempty(find(nn==[2 3 5 6 8 9 11 12]))
    yticklabels('');
  end

  if ~isempty(find(nn==[10:12]))
    xlabel('位相 [rad]');
  end

  if ~isempty(find(nn==[1 4 7 10]))
    ylabel('出力 [pu]');
  end
end

% デモ用gifアニメのためのスクリーンコピー（オリジナル図3）
commandwindow;
figure(3);
fig_capture(gcf,Poss,30);

% ==========
% 余白の調整
% ==========

% 余白調整したいfigureを呼び出して、調整代（pixel単位）を指定する。
% 　念のため、画像ファイルとしても出力する。

% ■ 図1
figure(1);
figure_trimmer (gcf,[-80 -80 0 0]);     % 余白調整（ローカル関数）

% デモ用gifアニメのためのスクリーンコピー（余白調整後の図1）
commandwindow;                % コマンドウィンドウを背景にする。
figure(1);
fig_capture(gcf,Poss,11);     % スクリーンキャプチャ（ローカル関数）

% 余白調整後の図の高解像度ファイル化
print(gcf,'-dpng',[basename '_01.png'],'-r600')

% ■ 図2
figure(2);
figure_trimmer (gcf,[0 0 -150 -160]);   % 余白調整（ローカル関数）

% デモ用gifアニメのためのスクリーンコピー（余白調整後の図2）
commandwindow;
figure(2);
fig_capture(gcf,Poss,21);     % スクリーンキャプチャ（ローカル関数）

% 余白調整後の図の高解像度ファイル化
print(gcf,'-dpng',[basename '_02.png'],'-r600')

% ■ 図3
figure(3);
figure_trimmer (gcf,[70 -110 0 0]);     % 余白調整（ローカル関数）

% デモ用gifアニメのためのスクリーンコピー（余白調整後の図3）
commandwindow;
figure(3);
fig_capture(gcf,Poss,31);     % スクリーンキャプチャ（ローカル関数）

% 余白調整後の図の高解像度ファイル化
print(gcf,'-dpng',[basename '_03.png'],'-r600')

% =======================
% before、after動画の作成
% =======================

% before文字のイメージ取得
figure(10)
set(gcf,'Color','w');
axis([0 1 0 1]);
text(0.01,0.01,'before','FontSize',35,'VerticalAlign','base','Color','r')
axis off
F_before=getframe(gca);
Chr_before=F_before.cdata;
Chr_before(1:end-60,:,:)=[];
Chr_before(:,200:end,:)=[];
close 10;

% after文字のイメージ取得
figure(11)
set(gcf,'Color','w')
axis([0 1 0 1]);
text(0.01,0.01,'after','FontSize',35,'VerticalAlign','base','Color','b')
axis off
F_after=getframe(gca);
Chr_after=F_after.cdata;
Chr_after(1:end-60,:,:)=[];
Chr_after(:,200:end,:)=[];
close 11

imfname={
'figure_arrange04_screen_10.png'
'figure_arrange04_screen_11.png'
'figure_arrange04_screen_20.png'
'figure_arrange04_screen_21.png'
'figure_arrange04_screen_30.png'
'figure_arrange04_screen_31.png'
};

for ni=1:3

  % グラフ図の読み込み
  Img_before=imread(imfname{(ni-1)*2+1});
  Img_after=imread(imfname{(ni-1)*2+2});

  % グラフ図に文字の書き込み
  Img_before(end-size(Chr_before,1)+1:end,1:size(Chr_before,2),:)=Chr_before(:,:,:);
  Img_after(end-size(Chr_after,1)+1:end,1:size(Chr_after,2),:)=Chr_after(:,:,:);

  % before ⇔ after のgifアニメを作成
  animename=[basename '_anime_' num2str(ni) '.gif'];
  [A1,map1]=rgb2ind(Img_before,256);
  imwrite(A1,map1,animename,'gif','DelayTime',1,'LoopCount',Inf);
  [A2,map2]=rgb2ind(Img_after,256);
  imwrite(A2,map2,animename,'gif','DelayTime',1,'WriteMode','append');

end

% ==========================================
% ==========================================
% figureの上下左右の余白調整用のローカル関数
% ==========================================
% ==========================================

function figure_trimmer (hf,KK)

  % 【入力】
  % 　hf:  余白調整したいfigureのハンドル（呼び出し側では常にgcf固定）
  % 　KK:  余白の変更量のベクトル。 単位は[pixel]。
  % 　     　（[上余白 下余白 左余白 右余白]。増量は+、減量は-）
  % 
  % 【出力】
  % 　戻り値は無し

  posf=hf.Position;            % 余白変更前のfigureのposition
                               % 　[左端 下端 幅 高さ]、単位は[pixel]。
  naxe=size(hf.Children,1);    % figure内のChildren（axesやcolorbar）
                               % 　などの総数。

  % 全Childrenのposition行列を作成
  % 　各行がそれぞれのChildrenに対応。
  % 　各列は[左端 下端 幅 高さ]、単位は[normalized]。

  Posa=[];                     % position行列の初期値
  for n=1:naxe
    axes(hf.Children(n))
    hn(n)=gca;
    Posa=[Posa ; hn(n).Position];
  end

  % 各Childrenの、余白変更前のfigure内でのpixel単位でのPositionを計算。
  Posp(:,[1 3])=Posa(:,[1 3])*posf(3);
  Posp(:,[2 4])=Posa(:,[2 4])*posf(4);
  
  % 余白変更後のfigureのpixel単位でのPositionを計算。
  posfx=posf+[-KK(3) -KK(2) KK(3)+KK(4) KK(1)+KK(2)];
  
  % 余白変更後のfigure内での各Childrenのpixel単位でのPositionを計算する。
  Pospx(:,1)=Posp(:,1)+KK(3);
  Pospx(:,2)=Posp(:,2)+KK(2);
  Pospx(:,3)=Posp(:,3);
  Pospx(:,4)=Posp(:,4);
  
  % 余白変更後の各Childrenのnormalized単位でのPositionを計算する。
  Posax(:,[1 3])=Pospx(:,[1 3])/posfx(3);
  Posax(:,[2 4])=Pospx(:,[2 4])/posfx(4);
  
  % figureとChildrenのpositionを更新
  hf.Position=posfx;
  for nn=1:naxe
    hn(nn).Position=Posax(nn,:);
  end

end

% ===============================================================
% ===============================================================
% 指定範囲のスクリーンコピーからpngファイルを生成するローカル関数
% ===============================================================
% ===============================================================

function fig_capture(hf,Poss,Nr)

  % 【入力】
  % 　hf:   png 画像化する figure のハンドル（gcf）
  % 　Poss: スクリーンのキャプチャ範囲 [左端 下端 幅 高さ] [pixel]
  % 　Nr:   ファイル名の後に付ける識別番号(1～99の数値)
  % 　　　　ファイル名は、
  % 　　　　[呼び出し側のmファイルのbase名]_screen_[識別番号].pngとなる
  % 【出力】
  % 　png ファイル（返り値は無し）

  basename=mfilename;       % [呼び出し側のmファイルのbase名]を取得

  figure(hf);
  drawnow
  pause(0.2);               % これがないと未完成図を取り込んでしまう

  Ss=get(0,'ScreenSize');   % モニタのサイズ [1 1 幅 高さ] [px]

  % スクリーンキャプチャする範囲。
  %   MATLAB は左下が [0 0]、JAVA は左上が [0 0]。
  % 　また、1920×1200[px] ディスプレイを 150% 倍率の 1280×800[px] 
  % 　として使っているが、
  % 　MATLAB は 1280×800[px] 、JAVA は 1920×1200[px] と見なしている。

  pos=[Poss(1) ...
       Ss(4)-(Poss(2)+Poss(4)) ...
       Poss(3) ...
       Poss(4)]*1.5;  

  % スクリーンキャプチャ
  robot = java.awt.Robot();                                % JAVA
  rect = java.awt.Rectangle(pos(1),pos(2),pos(3),pos(4));  % JAVA
  cap = robot.createScreenCapture(rect);                   % JAVA

  WW=cap.getWidth;                               % MATLAB ← JAVA
  HH=cap.getHeight;                              % MATLAB ← JAVA
  % RGB イメージの取り出し
  aaa=cap.getRGB(0,0,cap.getWidth,cap.getHeight, ...
                            [],0,cap.getWidth);  % MATLAB ← JAVA
         % 画素数分の要素を持つ int32 形式の列ベクトル
  rgb = typecast(aaa,'uint8');
         % 画素数の4倍の要素を持つ uint8 形式の列ベクトル

  % MATLAB の画像形式に合うように並べ替え。
  % 　4 個ごとにあるダミーデータ(alpha かも)は捨てる。
  imgData = zeros(HH,WW,3,'uint8');
  imgData(:,:,1) = reshape(rgb(3:4:end),WW,[])';
  imgData(:,:,2) = reshape(rgb(2:4:end),WW,[])';
  imgData(:,:,3) = reshape(rgb(1:4:end),WW,[])';

  % 画像の png ファイル化

  fname=[basename '_screen_' num2str(Nr,'%02u') '.png'];
  imwrite(imgData,fname);  % 取得画像の png ファイル化
end
```

</details>