レガシーなシステムのコードを勉強がてら、自分なりに最適化してみる

# Perlは好きですか？

こんにちは。自分は、Pythonを主言語として、いつも書いていますが、
今回、自社システムを学ぶためにも書かれているコードを勉強しつつ、コーディングしてみた話です。

システムは、かれこれ十数年クラスの凄いシステムです。
当時、師匠が一人で作ってくださり、今もなお現役として稼働し続けているシステムで、
全てPerlで書かれているCGiとなります。
セキュリティ的な面はさておき、コード自体は、特にPerlっぽさもないコーディングに仕上がっており、今後使われるかは不明です。
また、これら勉強を通して最も個人的にためになった箇所だけを抜粋し、こちらに投稿させて頂きます。

それでは、始まります。
###### ※なお、公開しているコードは一部を抜き出したもので、不完全であり、コピペでは動きません。　

## IPカメラ(ネットワークカメラ)から画像を取得したい。

弊社システムでは、必須な項目であり、現場のカメラから画像を取得し、システム上に表示させなくてはイケません。
そこで、弊社のシステムでは登録された情報からカメラに直接アクセス、または間接的に画像を拾ってこなくてはいけないのです。
今回ここを中心に、「ネットワークの知識+コーディング力」を養うため、システム改良と評して、自力でPerl(初めて触る言語)にて、コーディングしてみました。
なお、Perlだけではなく、Pythonでも同様なコーディングを行なっていますので、こちらも一緒に投稿します。

## コーディング前にやること(準備)
では、このような技術的な事をまず始める前に何から行えばいいのか？から...
まずは、デバイスに関する仕様を把握することから始めます。
例えば、今回の対象となるデバイスは、約7台が対象となっており、それぞれのカメラモデルに関する取得です。
(その内一台は自作で作ったラズパイも含みます)
基礎的な部分として、まずそれぞれに関する各種マニュアルが必要となります。
IPカメラと言っても色々な仕様があり、さらにはそれらを駆使して、得ることもあります。
一種のハッキング見たいなものです。こちらはかなり根気もいる場合もありますし、
慣れれば、直感で大体できるようになります。

...ただこのご時世、極力短時間で、成果を上げなくては行けないことも多々あると思います。
そんな時は、ひたすらググれば大抵のことは解決しますw
という事で、便利なサイトをご紹介します。

### 便利なサイト その1
 - [camera-sdk.com][camera-sdk]
ここは、有名なメーカーのIPカメラに関する情報が満載です。
また、C#に関するコーディング方法なら書かれています。

なお、今回利用するのは、コーディング方法ではなく、カメラ画像の取得方法となりますので、
カメラの型番からCGIやキャプチャ先のURLを探します。
最近のカメラならChromeなどのデバッガーモードからでも探ることはできます。
ただし、内部できなもの(CGIなど)だと取得するのは困難になりますので、そういった時に使えます。
今回は、「Hikvision IP camera」と「Axis IP camera」のIPカメラに関する情報を得ました。
例として、Axisのキャプチャ先を探します。
まず「manual > 1. Introduction to Ozeki Camera SDK > Supported cameras」へと進みます。
次に、アルファベット順の中からメーカーを探し、遷移します。
そのページから「Models」と同じ型番を探し、「Example URL」を確認します。
今回の場合は、「AXIS_207」でしたので、"Models"内の"207"に関する情報を得ます。

207のキャプチャ先URLは、複数存在しており、必要に応じて使い分けます。
例えば、IPカメラには、直接URLにアクセスして、画像だけが表示されるタイプのものと
映像が表示されるタイプがあります。
後者の方は、「RTSP(リアルタイム・ストリーミング・プロトコル)」というプロトコルで、アクセスします。
サーバーなどであれば、こちらの方式でも問題ないかと思いますが、取得した映像を画像化するのに少し手間がかかります。
また前者の場合は、ひたすら画像が切り替わり表示されるタイプとアクセスした時のカメラ画像が表示されるパターンなどがあります。
こちらはそもそもが画像なので、加工せずともただダウンロードするだけでいいので、今回は前者に関する情報を選ぶといいかと思います。
お目当てのURLは、次の通りです。

- AXIS_207
`http://IPADDRESS/axis-cgi/jpg/image.cgi?date=1&clock=1&resolution=[WIDTH]x[HEIGHT]`

この場合、cgiに直接アクセスし、パラメータで画像表示の状態を表していると想像できます。
ただしIPカメラの場合、セキュリティ上の問題で、`IPADDRESS`だけでは、取得することはできません。
大抵は、「Basic認証かdigest認証」が適用されており、ログイン状態を保ったまま、指定のURLへアクセスしなくては行けないのです。
またこれは、「RTSP」も同様です。

これで、下準備が整いました。
次は、コーディングに移ります。

## コーディング(Perl)
先ほども言ったように自分は、Pythonが主なので、Perlが書けませんでした。
そこで、師匠のコードを解読しつつ、260ラインオーバーのコードを80ラインまで抑えました。
書いてる内容も単調なコードで、同じことの繰り返しだったので、一部のコードを読み解き、
自作で、自分なりにコーディングし直してみました。
なお、師匠のコードも一つのカメラであれば、23行程度なので、3個くらいまでなら自分のコードよりも短いです。
がしかし、カメラは年々変わり、製造関係などで入手困難になることもあります。
では、いろんなパターンに対応できるようなコードが望ましいのではないかと＞＜？と思いつつ、
既存システムに使えるような想定で、コードを書いてみました。

### 最適化前
```Perl:最適化前
... elsif ($ctype eq 'AXIS_207') {
      # AXIS 207
      my $param = 'axis-cgi/jpg/image.cgi' ;
      $param .= '?' . $camera_aux if ($camera_aux ne "") ;
      my $agentreq = LWP::UserAgent->new ;
      $agentreq->agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/39237369 Firefox/25.0") ;
      $agentreq->timeout(30) ;
      my $httpreq = HTTP::Request->new (GET => $curl . $param) ;
      $httpreq->authorization_basic($cuser,$cpass) if (($cuser ne "") || ($cpass ne "")) ;
      my $resultimg = $agentreq->request($httpreq) ;
      if ($resultimg->is_success) {
        my $outimg = img_resize_content(320,240,'jpg',$resultimg->content) ;
        $rfile     = sprintf("%s/%03d/%d/%d%02d%02d%02d%02d%02d.jpg", $fftpdir,$cgienv{'innum'},$cgienv{'camid'},@imgtim) ;
        check_imgdir($cgienv{'innum'},$cgienv{'camid'}) ;
        if (open(IMG,"> $rfile")) {
          print IMG $outimg ;
          close (IMG) ;
        }
      }
      undef ($httpreq) ; undef ($agentreq) ;

    } elsif ($ctype eq '????') ...

```

### 最適化後
```Perl:最適化後

sub camUrlPath {
    my ($camera_type, $url, $user, $password) = @_ ;
    $url = $url*100+1 if ($camera_type =~ /(DS-2|DS-7204)/) ;
    my $urlPath  = "" ;
    my @params = (  
                    'axis-cgi/jpg/image.cgi',
                    ...,
                    'ISAPI/Streaming/channels/%s/picture'
                ) ;
    $urlPath = ($camera_type =~ /BBHCM527/) ? $params[1]:$params[0] if ($camera_type =~ /(BLC1|BBHCM511|VLCM240|BBHCM527)/) ;
    ...
    $urlPath = $params[4] if ($camera_type =~ /(AXIS_207|AXIS_M1124)/) ;
    ...
    $urlPath = sprintf($params[11], $url) if ($camera_type eq "DS-7204") ;
    $urlPath .= ($urlPath =~ /\?/) ? '&'. $url:'?' . $url if (($url ne "") && !($camera_type =~ /(DS-2|DS-7204)/)) ;
    return ($urlPath, $camera_type) ;
}

sub cameraImageRequest {
    my ($reW, $reH, $quality) = (320, 240, 95);
    my ($urlPath, $user, $password, %parameters) = @_ ;
    my $renewflg = 0 ;
    my @imgtim = (localtime())[5,4,3,2,1,0] ;
    my $rfile = "";
    $imgtim[0] += 1900 ;
    $imgtim[1] ++ ;
    my $curl = sprintf('http://%s%s/', $parameters{'host'}, ($parameters{'port'} == 80) ? "":":" . $parameters{'port'});
    $curl =~ s%^(http://)(.*)%$1$user:$password\@$2%g if( $parameters{'camera_type'} =~ /(AXIS_207|AXIS_M1124|DS-2|DS-7204)/) ;
    my $agentreq = LWP::UserAgent->new ;
    my $useragent = ($parameters{'useragent'} eq "") ? $config::campull_useragent:$parameters{'useragent'} ;
    $agentreq->agent($useragent);

    $agentreq->timeout($parameters{'waittime'}) ;
    my $httphead = HTTP::Request->new (GET => $curl . $urlPath) ;
    if( $parameters{'camera_type'} =~ /(DS-2|DS-7204)/) {
        my $authreq  = $agentreq->request($httphead) ;
        my $authdata = $authreq->header('WWW-Authenticate') ;
        my $realm    = utile::trim_digest_param('realm',$authdata) ;
        $agentreq->credentials($curl,$realm,$user,$password) ;
        my $req = $agentreq->request($httphead) ;
    }else{
        $httphead->authorization_basic($user,$password) if (($user ne "") || ($password ne "")) ;
    }

    my $resultimg = $agentreq->request($httphead) ;
    if ($resultimg->is_success) {
        my $outimg   = $resultimg->{_content} ;
        my $dirPath  = sprintf("%s/%03d/%d", $config::fftpdir,$parameters{'innum'},$parameters{'camid'}) ;
        my $filePath = sprintf("%s/%d%02d%02d%02d%02d%02d.jpg", $dirPath, @imgtim) ;
        ($rfile = $filePath) =~ s/.*([0-9]{14}.jpg)/$1/ ;
        utile::create("", $dirPath) if !utile::fileCheck($filePath, $dirPath) ;
        if (!utile::fileCheck($filePath, $dirPath)) {
            open (IMG,"> $filePath") or die "$!" ;
                print IMG $outimg ;
            close (IMG) ;
            if(!imgResizeContent($reW, $reH,'jpg', $filePath)){
                if (utile::fileCheck($filePath, $dirPath)){
                    my $InputImg  = Image::Imlib2->load($filePath) ;
                    my $OutputImg = $InputImg->create_scaled_image($reW, $reH) ;
                    $OutputImg->set_quality($quality) ;
                    unlink($filePath) if utile::fileCheck($filePath, $dirPath);
                    $OutputImg->save($filePath) ;
                }
            }
      }
      return $rfile ;
    }
    undef ($httphead) ; undef ($agentreq) ;
}

```

### README
利用方法は、次の通りとなります。

```Perl:README
    @host_data = ("<IP address>", PORTnumber) ;
    @camparam   = ("<CAMERA_MODELS>","","","<URL_PATH>","<USER>","<PASSWORD>") ;
    @parameters = ($camparam[0],$camparam[3],$camparam[4],$camparam[5]) ;
    my ($url_path, $camera_type) = camUrlPath(@parameters) ;
    my %pam = ( innum => 10001, 
                camid => 1,
                host => $host_data[0],
                port => $host_data[1],
                camera_type => $camera_type,
                useragent => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36",
                waittime => 30
            ) ;
    cameraImageRequest($url_path, $camparam[4], $camparam[5], %pam);
```


細かな説明は、省きますが、Perlの強みである文章解析(正規表現)を有効に活用し、
三項演算子をフル活用してみました。
集めたURLは実際のところ、11個ありましたw
古いカメラも中には存在しており、それらに合わせるために何度も同じようなコードをif文で、
分岐させるのではなく、追加の手間を省くためにも「モジュール化」しました。

Perlで苦戦したところは、Listや連想配列の入れ方ですね。
Pythonに慣れていると意識せずに指定して、出し入れしてたからPerlで書き起こした際は、かなり癖が強くて苦戦しましたw

## Pythonバージョン
と、こちらに合わせて、Pythonでも書いてみたので、みてやって下さい。

```Python3:Pythonバージョン
AMERA_TYPES      = ['hikvision','axis','rasppi']
CAMERA_TYPES_URLS = [['Streaming/Channels/$channel_id/','ISAPI/Streaming/channels/$channel_id/picture'],['axis-media/media.amp','axis-cgi/jpg/image.cgi'],['','snap?action=snapshot']]
CAMERA_TYPE_URLS  = dict(zip(CAMERA_TYPES,CAMERA_TYPES_URLS))

...
        target_list = {
            "camera_type-hik":r'(hikvision)',
            "camera_type-axis":r'(axis_[0-9]{,4})',
            "camera_type-rasppi":r'(rasppi)'
        }
        pattern_list = {"_default":{k:re.compile(v) for (k,v) in target_list.items()}}

...
    def shot(self):
        if self.check_port(self.scheme,self.port,1):
            self.cap = cv2.VideoCapture(f'{self.protocol}://{self.user}:{self.passwd}@{self.host}/{self.url_path}')
            if ((self.cap.__class__.__name__ == 'VideoCapture')):
                with Pool(max_workers=MAX_CPU) as pool:
                    pool.submit(self.__run__)
            self.cap.release()
        return self.file_name

...
    def __run__(self):
        try:
            self.IMAGES_DATABASE = f'{self.DATABASES}/images_tinydb.json'
            self.token           = secrets.token_hex()
            ret, img    = self.cap.read()
            save_path = self.FILE_PATH
            self.check_dir(save_path)
            time_stamp = '{0:%Y%m%d%H%M%S}'.format(datetime.datetime.now())
            file_name = f'{save_path}/{time_stamp}_'+''.join(str(uuid4()).split('-'))+'.jpg'
            cv2.imwrite(f'{file_name}', img)
            _status   = "NG"
            TYDB      = f'{self.IMAGES_DATABASE}'
            self.check_dir(self.DATABASES)
            json_data = {"status": _status,"token":f'{self.token}',"innum_id":self._innum_id,"img_path":f'{file_name}','time_stamp':f'{time_stamp}'}
            if file_name != "":
                json_data["status"] = "OK"
                with tydb(TYDB) as db:
                    db.insert(json_data)
            self.file_name = f'imgdata?token={self.token}&time_stamp={time_stamp}&camera_type={self.camera_type}'
        except:
            print(sys.exc_info())
            self.file_name = self.no_img_data
        return True

```
Pythonバージョンは、調子に乗って、RTSPにも対応しており、OpenCVなどでカバーしています。
使い方はとても簡単で、ホスト、ユーザー、パスワード以外に、プロトコルやカメラタイプ(モデル)などを指定してあげることで、
正規表現された内容から読み解き、URLを合わせます。
画像の取得部分は、shotの箇所で、`check_port`で、実際にポートが開いているかも検証し、キャプチャしています。
その他にもサーバーでの動作を想定しているので、非同期でできるように並列タスクで実行するようにしています。
画像処理が完了次第、別タスクにて、データベースへの登録や画像の引き渡し用URLの発行、画像の保存などを行なっています。


## JavaScriptバージョン

さらに調子に乗ってしまい、JavaScriptバージョンでも書いてみましたので、一部分を公開しておきます。
これは、単にプラグインがMacOSに対応していないカメラだったので、
画像をブラウザ上で表示させるためにChromeの拡張機能として、カキカキしたものです。

```JavaScript:JavaScriptバージョン
import { HASH, BASE64 } from './lib/utile.js';
import { WEBSDK } from './lib/websdk.js';

const hash = new HASH();
const base64 = new BASE64();
const WebSDK = new WEBSDK();
export const init = async () => {
  let timeId = 0;
  protocol.addEventListener("change", ()=>{
    port.value = (protocol.value == "http") ? 80:(protocol.value == "https") ? 443:554;
  });
  const hikview = async () => {

    let user     = username.value;
    let passwd   = password.value;

    const hik_uri     = (cameraType.value.match(/NVR/)) ? `ISAPI/Streaming/channels/${cid.value}/picture`:`Streaming/Channels/${cid.value}/picture`;
    const img_element = document.createElement('img');
    const canvas_element = (!content.firstChild) ? document.createElement('canvas') : content.firstChild;
    const ctx = canvas_element.getContext("2d");
    
    const headers = new Headers();
    headers.append("Authorization", "Digest " + hash.MD5(`${user}:${passwd}`));
    headers.append("credentials","include");
    const requestOptions = {
        method: 'GET',
        headers: {headers}
    };
    fetch(`${protocol.value}://${address.value}:${port.value}/${hik_uri}`, requestOptions)
      .then(response => response.blob())
      .then(result => {
          const img = new Image();
          img.onload = () => {
            ctx.drawImage(img, 0, 0);
          }
          img_element.style.width = '100%';
          img_element.classList.add("col");
          let reader = new FileReader();
          reader.readAsDataURL(result);
          reader.onload = () => {
              img_element.src =  reader.result;
              img.src = reader.result;
              if(!content.firstChild){
                img.src = reader.result;
                content.appendChild(canvas_element);
              }else{
                img.src = reader.result;
              }
            };
          
      })
      .catch(error => console.log('error', error));
  }
  const SetUp = async (e) => {
    const xhr = new XMLHttpRequest();
    const url = `${protocol.value}://${address.value}:${port.value}/ISAPI/Streaming/channels`;

    let user       = username.value;
    let passwd     = password.value;
    const cameraId = document.querySelector("#cid");

    xhr.onreadystatechange = async () => {
      if(xhr.readyState === 4 && xhr.status !== 0) {
        if(cameraId.children){
          for(const chid of cameraId.children){
            // cameraId.removeChild(chid);
          }
        }
        for(const cid of xhr.responseXML.children[0].children){
          const option = document.createElement("option");
          option.text  = cid.firstChild.nextSibling.textContent;
          option.value = cid.firstChild.nextSibling.textContent;
          cameraId.appendChild(option);
        }
      } else if(xhr.readyState === 4 && xhr.status === 0) {
          console.log("Error while getting XML.")
      }
    }
    xhr.overrideMimeType('text/html;charset=UTF-8');
    xhr.open("GET", url, true, user, passwd);
    xhr.responseType = "document";
    xhr.overrideMimeType("text/xml");
    xhr.send();
    getBtn.innerText = "起動";
  }
...
}
```
これは、`/ISAPI/Streaming/channels`にアクセスして、
カメラのチャンネル数を取得し、`option`タグを生成した上で、起動できるようになっています。
また、カメラ画像は、canvasタグに設定された秒間隔で、更新される仕組みになっています。

### 後書き
調子に乗って、3言語で書いてみましたが、それぞれ特徴があってそこがいいって感じでした。
Perlを学んで感じた事は、正規表現を身につけるなら、Perlが一番相性がいい気がしました。
直感的に、描きやすいし、その後の処理も似たパターンを最小化でかける感じが良かったと思います。

<!-- Links -->
[camera-sdk]:https://camera-sdk.com/p_6514-online-manual-for-the-ozeki-camera-sdk.html