【MATLAB】線の先端に丸みをつける方法

## １．はじめに
　MATLAB の plot で線を描いたとき、線の先端は角ばった形になります。細い線を描いているだけであれば、先端の形状などはあまり気になりませんが、線幅が太くなると目立つようになります。すると、ときには先端が丸いほうが好ましく感じることもあります。
 
　ところが、MATLAB figure の線自体のプロパティには、先端の形状に関係するものはなく、これを変えるための正規な手段は特にありません。しかし、なんとか解決できそうな姑息な方法を思いついたのでここで紹介します。

動作環境：　MATLAB R2019a、Toolboxなし、Windows11

## ２．解決方法
　MATLAB には線の先端の形状を指定するプロバティの代わりに、折線の「折れ曲がり部分」の形状を指定するための LineJoin プロパティというものがあります。これは規定で 'round' に設定されているため、丸みを帯びながら折れ曲がっているような線が描かれます。
 
　ここで紹介する方法は、分かりやすく言えば、線の先端を「折れ曲がり点」のように見せかけて MATLAB を騙す方法です。プログラムの基本部分を抜き出せば、次のようになります。いったん描いた線の XData, YData プロパティを操作して、線の両端を折り返すように細工しています。ここで示している例は１本だけの単線分ですが、複雑な折線の場合でも、２行目の set コマンドはこのままで大丈夫です。

```
h=plot([-1.5 1.5],[-1.5 1.5],'LineWidth',10);
set(h,'XData',[h.XData(2) h.XData h.XData(end-1)], ...
      'YData',[h.YData(2) h.YData h.YData(end-1)]);     % 丸め指示
```
　**図１**は MATLAB が正規に描いた図で、線の端が角ばっており、少々厳めしく感じます。**図２**は上で示した方法で処理したあとの図で、先端が丸くなり穏やかな感じがします。ところが、黄色の点線は、始端と終端は丸みを帯びていますが、それ以外の端部は角ばったままです。やはり、これでは十分には満足できません。
 
**図１．**　MATLAB の純正方法による、先端が角ばった線
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/988b9585-12bf-4195-b19d-58a1a18559a8.png">

**図２．**　上記コードによる、主な先端が丸みを帯びた線
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/47d44780-bb49-4b16-a0af-467ad0549f2a.png">

　そこで、XData や YData に NaN を含んだような不連続線でも、全ての端部に丸みを持たせられるようなローカル関数も作りました。これを使って描いたのが**図３**です。この関数は、点線だけでなく、青色や赤色のような NaN を含まない一般の線にも有効です。

**図３．**　ローカル関数による、全ての先端が丸みを帯びた線
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/932484/f289d26f-0d48-45c9-a043-e5d3195a3f66.png">

## ３．おわりに
　先に投稿した「[MATLABを 三角定規やコンパスのように使って 図を描く](https://qiita.com/tsubolabo/items/25dbb2e21442666a978e)」では、「MATLAB 様への要望」として「線の端を丸めるプロパティがあると便利です」という提案をしましたが、当面は、以上の方法でなんとか解決できそうです。

## ４．プログラム
　図１,２,３を描いたプログラムを添付しています。どのような線にも使えるローカル関数は、このプログラムの最後に書かれています。

```matlab
% round_line_tip01.m

% 線の先端を丸める裏技

clear
close all

%basename=mfilename('fullpath');  % プリント出力のための準備

% ======= 線の先端に丸めなし
figure(1)

% 単線分
plot([-1.5 1.5],[-1.5 1.5],'LineWidth',10);
hold on
axis equal
axis([-2 2 -2 2]);
grid on

% 多点折線
th=linspace(30,330,101);
plot(cosd(th),sind(th),'LineWidth',10);

% NaN入りの不連続線
th=[];
for n=1:11
  th=[th (n-1)*30+[0:20] NaN];
end
th(end)=[];
plot(1.5*cosd(th),1.5*sind(th),'LineWidth',10);

%set(gcf,'InvertHardcopy','off');
%print(gcf,'-dpng',[basename '01.png'],'-r300');

% ======= 線の先端に丸めあり（簡易法）
figure(2);

% 単線分
h=plot([-1.5 1.5],[-1.5 1.5],'LineWidth',10);
set(h,'XData',[h.XData(2) h.XData h.XData(end-1)], ...
      'YData',[h.YData(2) h.YData h.YData(end-1)]);     % 丸め指示
hold on
axis equal
axis([-2 2 -2 2]);
grid on

% 多点折線
th=linspace(30,330,101);
h=plot(cosd(th),sind(th),'LineWidth',10);
set(h,'XData',[h.XData(2) h.XData h.XData(end-1)], ...
      'YData',[h.YData(2) h.YData h.YData(end-1)]);     % 丸め指示

% NaN入りの不連続線
th=[];
for n=1:11
  th=[th (n-1)*30+[0:20] NaN];
end
th(end)=[];
h=plot(1.5*cosd(th),1.5*sind(th),'LineWidth',10);
set(h,'XData',[h.XData(2) h.XData h.XData(end-1)], ...
      'YData',[h.YData(2) h.YData h.YData(end-1)]);     % 丸め指示

%set(gcf,'InvertHardcopy','off');
%print(gcf,'-dpng',[basename '02.png'],'-r300');

% ======= 線の先端に丸めあり（丁寧法）
figure(3);

% 単線分
X=[-1.5 1.5]; Y=[-1.5 1.5];
[Xr,Yr]=round_linecap(X,Y);         % 丸め関数の呼び出し
plot(Xr,Yr,'LineWidth',10);
hold on
axis equal
axis([-2 2 -2 2]);
grid on

% 多点折線
th=linspace(30,330,101);
X=cosd(th); Y=sind(th);
[Xr,Yr]=round_linecap(X,Y);         % 丸め関数の呼び出し
plot(Xr,Yr,'LineWidth',10);

% NaN入りの不連続線
th=[];
for n=1:11
  th=[th (n-1)*30+[0:20] NaN];
end
th(end)=[];
X=1.5*cosd(th); Y=1.5*sind(th);
[Xr,Yr]=round_linecap(X,Y);         % 丸め関数の呼び出し
plot(Xr,Yr,'LineWidth',10);

%set(gcf,'InvertHardcopy','off');
%print(gcf,'-dpng',[basename '03.png'],'-r300');

% ===========================================
% ローカル関数（NaNを含んだ不連続線の全ての先端部の丸め）
% ===========================================

function [Xr,Yr]=round_linecap(X,Y)

  X=[NaN X NaN];      % 未丸めデータの前後に強制的にNaNを追加
  Y=[NaN Y NaN];
  Xr=[];              % 丸め済データの初期値
  Yr=[];

  % X,Yデータ中で、NaNが存在する要素番号
  % 　（強制的なNaNの追加で、最初と最後は明らかにNaN）
  nnan=find(isnan(X)|isnan(Y));

  Nn=length(nnan)-1;  % NaNに挟まれた区間の数
  for n=1:Nn
    nb=nnan(n);
    ne=nnan(n+1);
    Xt=X(nb+1:ne-1);
    Yt=Y(nb+1:ne-1);
    if length(Xt)<=1  % NaNの連続による空データと単なる点データは無視
      continue
    else
      Xt=[Xt(2) Xt Xt(end-1)];   % 丸め処理
      Yt=[Yt(2) Yt Yt(end-1)];
      Xr=[Xr Xt NaN];            % 丸め済データの逐次集積
      Yr=[Yr Yt NaN];
    end
  end
  Xr(end)=[];         % 最後にあるNaNを削除
  Yr(end)=[];
end
```