【MATLAB】プロットにサクッと拡大図を追加する ZoomPlot 

# はじめに

拡大図をちゃちゃっと挿入することが可能になる [ZoomPlot](https://jp.mathworks.com/matlabcentral/fileexchange/93845-zoomplot?s_eid=PSM_29435) 関数（by Kepeng Qiu さん）の紹介です。

![image_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/507039c4-2440-84e2-f6d5-eca7f94283d0.png)


使い方はベースのプロットを描いた後に

```matlab:Code(Display)
zp = BaseZoom();
zp.plot;
```

の 2 行！

個人的に今年一番感動した File Exchange 関数だったので、これがどうやって実装されているのか、内部の処理を簡単に紹介します。

:::note info 
現バージョンでは zp.plot でなく zp.run; で起動するよう変更されていますので要注意（2024/2/26 追記）
:::
  
## インストール方法：アドオンの入手

MATLAB の File Exchange で 公開されている（[ZoomPlot](https://jp.mathworks.com/matlabcentral/fileexchange/93845-zoomplot?s_eid=PSM_29435)）のでそちらからコードをダウンロードするのもよいですが、おススメは「アドオンの入手」からのインストール。

![image_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/227ccc70-8fa8-6a2e-720e-4b0fb42df1a2.png)

「アドオンの入手」をクリックすると「アドオンエクスプローラー」が開きますので、「zoomPlot」で検索して「追加」で OK です！

::: note
パスの設定などもよしなにやってくれるのでお手軽です。
:::


![image_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/9255f60a-e010-4a77-1d2a-0b3b92e25969.png)


## 必要な環境

バージョンは R2014b 以降であれば OK。

拡大領域を選択する UI のために Image Processing Toolbox の [`drawrectangle`](https://jp.mathworks.com/help/images/ref/drawrectangle.html?s_eid=PSM_29435) 関数（R2018a 以前だと [`imrect`](https://jp.mathworks.com/help/images/ref/imrect.html?s_eid=PSM_29435) 関数）を使っているので、[Image Processing Toolbox](https://jp.mathworks.com/products/image.html?s_eid=PSM_29435) が必要な点にご注意くださいませ。

  
# コード解説

コードを開くと BaseZoom クラスが 500 行程度で作られていることが分かります。R2018a 以前のバージョン対応のために、バージョンによって [`drawrectangle`](https://jp.mathworks.com/help/images/ref/drawrectangle.html?s_eid=PSM_29435) 関数か [`imrect`](https://jp.mathworks.com/help/images/ref/imrect.html?s_eid=PSM_29435) 関数を使うかに分けていて、丁寧に作り込まれています。 

```matlab:Code
version_ = version('-release')
```

```text:Output
version_ = '2021b'
```

こんな感じで [version](https://jp.mathworks.com/help/matlab/ref/version.html?s_eid=PSM_29435) 関数で使われている環境（バージョン）が確認されています。

## 拡大図はどうやって挿入されている？

一番重要な部分がこれ。ですが、大変シンプルです。

   1.  新しい axes オブジェクトを作成 
   1.  元の axes オブジェクトから要素をコピー 
   1.  拡大したい部分だけ表示 
   1.  拡大した部分に長方形を表示 

の流れです。元の axes オブジェクトの Children をマルっとコピーしちゃっています。

::: note
オブジェクト操作に慣れている方であればイメージがわくかもしれませんが、一部要素だけ拡大したい場合はここの検索条件を Tag などを使って指定すればいいですね。
:::

拡大図作成操作部分をマネするとこんな感じ。

```matlab:Code
figure
fplot(@sin); % 例えばこんなプロット

% 元 axes のオブジェクトハンドル
h_original = gca; 
% 新しい axes 作成 [x,y,width,height]
h_zoom = axes('Position',[0.3,0.6,0.2,0.2]);
% 元 axes からプロットコピー
copyobj(get(h_original, 'children'), h_zoom);
% 拡大したい部分を指定
xlim(h_zoom,[-2,-1]);
ylim(h_zoom,[-1,-0.8]);
% 拡大した部分に長方形表示 [x,y,width,height]
rectangle(h_original,'Position',[-2,-1,1,0.2],...
    'EdgeColor','red',...
    'LineWidth',2);
```

![figure_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/d3f3199e-2c03-0004-fda3-5c888c0a1b87.png)

後は新しい軸（拡大図）と拡大した部分（赤枠）の頂点を線で結べば完了です。とはいえ、実際実装しようとするとヤヤコシイですね。位置関係によってどの２つの頂点をそれぞれから選ぶかが問題。この辺は `connectAxesAndBox` メソッドで定義されていますが、そこで呼ばれている `getLineDirection` メソッドで長方形との位置関係によって丁寧に（地道に）条件分岐しています。

### マニュアルで指定する機能が欲しい・・

この例では拡大箇所・拡大図を置く場所をマニュアルで指定しています。以前（v1.1）ではできましたが、現時点（v1.2.1）の [ZoomPlot](https://jp.mathworks.com/matlabcentral/fileexchange/93845-zoomplot?s_eid=PSM_29435) ではマニュアル指定ができません。

そんな時は File Exchange のページの「View Version History」をクリックして古いバージョンを使うのも手です。マニュアル指定・マウスでの指定、両方使えるのが一番ですけど。

![image_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/21fbf56b-f9a3-0807-c161-8a4e9cb643d4.png)

こんな感じで使えます（使えました）

```matlab:Code(Display)
parameters = struct('axesPosition', [0.6, 0.1, 0.2, 0.4],...
                    'zoomZone', [1.5, 2.5; 0.6, 1.3],...
                    'lineDirection', [1, 2; 4, 3]);

zp = BaseZoom();
zp.plot(parameters)
```

これで、

   1.  新しい axes オブジェクトを作成 
   1.  元の axes オブジェクトから要素をコピー 
   1.  拡大したい部分だけ表示 
   1.  拡大した部分に長方形を表示 

の 2 と 3 ができました。

## 領域選択はどうやって？

領域選択は [`drawrectangle`](https://jp.mathworks.com/help/images/ref/drawrectangle.html?s_eid=PSM_29435) 関数か [`imrect`](https://jp.mathworks.com/help/images/ref/imrect.html?s_eid=PSM_29435) 関数ですが、ここでは R2018b 以降推奨の [`drawrectangle`](https://jp.mathworks.com/help/images/ref/drawrectangle.html?s_eid=PSM_29435) 関数で。Image Processing Toolbox の関数であるので、本来は画像の ROI（Region of Interest）を選択する機能です。

```matlab:Code
I = imread('cameraman.tif');
figure
imshow(I); 
roi = drawrectangle('Color','r')
```

![figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/0ff453a5-1983-ef9e-db9a-47f0cc12a080.png)


```text:Output
roi = 
  Rectangle のプロパティ:

         Position: [115 51 59 54]
    RotationAngle: 0
      AspectRatio: 0.9153
            Label: ''
            Color: [1 0 0]
           Parent: [1x1 Axes]
          Visible: on
         Selected: 0

  すべてのプロパティ を表示

```

こんな感じで `Position` として選択した領域の情報が取れます。

この情報をもとに

   1.  新しい axes オブジェクトを作成 
   1.  元の axes オブジェクトから要素をコピー 
   1.  拡大したい部分だけ表示 
   1.  拡大した部分に長方形を表示 

の 1 と 4 を実施します。

### 領域選択中の動作はどうする？

この動画の後半部分、拡大部分を選択しているときに拡大図が動的に変化していることが分かります。

![image_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/94d50b75-8772-c6c1-52d6-1feb06d5011d.png)

これは ROI 移動イベントのリスナーを設定することで実現します。イベントを検知すると指定されたコールバック関数を実行します。`BaseZoom.m` の中を見ると

```matlab:Code(Display)
addlistener(obj.roi, 'MovingROI', @obj.allEventsForRectangleNew);
addlistener(obj.roi, 'ROIMoved', @obj.allEventsForRectangleNew);
```

の設定があります。使用可能なイベントのリストは [Rectangle のイベント](https://jp.mathworks.com/help/images/ref/images.roi.rectangle.html#mw_165f149b-e6e2-4e32-b321-a346ab575eb3?s_eid=PSM_29435) にありますが以下の通り。

   -  `DeletingROI`：ROI が対話的に削除されようとしている。 
   -  `DrawingStarted`：ROI が対話的に描画されようとしている。 
   -  `DrawingFinished`：ROI が対話的に描画された。 
   -  `MovingROI`：ROI の形状または位置が対話的に変更中である。 
   -  `ROIMoved`：ROI の形状または位置が対話的に変更された。 
   -  `ROIClicked`：ROI がクリックされた。 

ここでは `MovingROI` と `ROIMoved` が設定されており、ROI の形状の変更中・そして変更後にコールバック関数（`allEventsForRectangleNew`）が呼ばれる仕様であることが分かります。

試しにこちら[サンプルのコード](https://jp.mathworks.com/help/images/ref/images.roi.rectangle.html#mw_ca2d4df8-02c6-4f53-83e1-5c9dcfa7deeb?s_eid=PSM_29435)を動かすとこんな感じです。

![image_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/d59d25a4-cff7-aa28-0013-eac82532c2bb.png)


ROI を変更中の位置と変更後の位置の表示が確認できますね。リスナーの設定は

```matlab:Code
addlistener(roi,'MovingROI',@allevents);
addlistener(roi,'ROIMoved',@allevents);
```

で、コールバック関数 `allevents` の定義は以下の通り。

```matlab:Code(Display)
function allevents(src,evt)
    evname = evt.EventName;
    switch(evname)
        case{'MovingROI'}
            disp(['ROI moving previous position: ' mat2str(evt.PreviousPosition)]);
            disp(['ROI moving current position: ' mat2str(evt.CurrentPosition)]);
        case{'ROIMoved'}
            disp(['ROI moved previous position: ' mat2str(evt.PreviousPosition)]);
            disp(['ROI moved current position: ' mat2str(evt.CurrentPosition)]);
    end
end
```

  
## 関数化しちゃう

他のデータで同じ位置に拡大図をつける・・そんなケースは少ないかもしれませんが、もしあるなら

![image_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/d9db5177-0655-b501-1edb-e83e6884aab9.png)

これですね。

幸い拡大図も含んだ関数が自動生成されてますので、その関数を使えば同じプロットが再現できます。ただ今回のケースですと下のように表示範囲を指定するコードがコメントアウトされているので、ここはコメント解除する必要はありますので要注意。

いろんなデータで同じ拡大図を付けたい時は便利になるかと。

```matlab:Code
% Axes の X 軸の範囲を保持するために以下のラインのコメントを解除
% xlim(axes1,[0 12.5663706143592]);
% Axes の Y 軸の範囲を保持するために以下のラインのコメントを解除
% ylim(axes1,[0 300]);
```

# さいごに

 [ZoomPlot](https://jp.mathworks.com/matlabcentral/fileexchange/93845-zoomplot?s_eid=PSM_29435) 関数は自分が作った関数でもなんでもないんですが、実装方法がエレガントだなぁ・・と感動したので勝手にざっくり解説しました。