Idiom: Cumulative Summation Summary

累積和を実装するとき、いつもインデックスの扱いに悩んでしまいます。これが結構時間を喰うので 、しっかりイディオムとしてまとめようと思いました。

例えば0-indexの配列、\(a_i\)があるとします。

$$ \begin{array}{c|ccccc} i & 0 & 1 & 2 & 3 & 4 & 5\\\hline a_i & 1 & 2 & 3 & -4 & -2 & 1 \end{array} $$

目的はO(1)で\(s_{[i,j)}\)を求めることです(準備にはO(N)かかります)。例えば、\(s_{[0,1)}\)は1、\(s_{[0,3)}\)は6、\(s_{[1,3)}\)は5といった具合です。

これを行うには累積和をテーブルとして用意するのが常套手段です。これを\(\mathit{acc}_i\)とします。\(\mathit{acc}_{i+1} = a_i + \mathit{acc}_i\)とします。また、\(\mathit{acc}_0\)は0です。つまり、

$$ \begin{cases} \mathit{acc}_0 = 0\\ \mathit{acc}_{i+1} = a_i + \mathit{acc}_i\ \text{where}\ 0 \leq i < \mathit{size}\\ \end{cases} $$

作成された累積和テーブルは以下のようになります。

$$ \begin{array}{c|cccccc} i & 0 & 1 & 2 & 3 & 4 & 5 & 6\\\hline \mathit{acc}_i & 0 & 1 & 3 & 6 & 2 & 0 & 1 \end{array} $$

ここで添字がずれるのが混乱の元なんですね。しっかり式を立てておきましょう。

$$ s_{[i, j)} = \mathit{acc}_j - \mathit{acc}_i $$

ポイントは、

  • 半開区間とすること(\(s_{[i,i)}\)は空集合なので0)
  • i、jは配列\(a_i\)のものをそのまま使う

となります。結果は以下のようになります。

$$ \begin{array}{c|c} s_{[0,1)} = \mathit{acc}_1 - \mathit{acc}_0 = 1 - 0 = 1\\ s_{[0,3)} = \mathit{acc}_3 - \mathit{acc}_0 = 6 - 0 = 6\\ s_{[1,3)} = \mathit{acc}_3 - \mathit{acc}_1 = 6 - 1 = 5\\ \end{array} $$

さて、1-indexの配列の場合にはどうしたらよいでしょう?

$$ \begin{array}{c|ccccc} i & 1 & 2 & 3 & 4 & 5 & 6\\\hline a_i & 1 & 2 & 3 & -4 & -2 & 1 \end{array} $$

\(a_0\)を0とおけば-0indexの場合と同様に扱うことができそうです。

$$ \begin{array}{c|ccccc} i & 0 & 1 & 2 & 3 & 4 & 5 & 6\\\hline a_i & 0 & 1 & 2 & 3 & -4 & -2 & 1 \end{array} $$

累積和テーブルは一つずれますが、0-indexと同じ考え方で扱うことができそうです。累積和テーブルの先頭の2つの要素を0で初期化しておかなければならないので注意が必要です。

$$ \begin{cases} \mathit{acc}_0 = 0\\ \mathit{acc}_1 = 0\\ \mathit{acc}_{i+1} = a_i + \mathit{acc}_i\ \text{where}\ 1 \leq i \leq \mathit{size}\\ \end{cases} $$

作成された累積和テーブルは以下のようになります。

$$ \begin{array}{c|cccccc} i & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7\\\hline \mathit{acc}_i & 0 & 0 & 1 & 3 & 6 & 2 & 0 & 1 \end{array} $$

\(s_{[i,j)}\)は以下のように算出することができます。

$$ s_{[i, j)} = \mathit{acc}_j - \mathit{acc}_i $$

もう一つ気になることといえば、配列は要素数 + 1、また累積和は要素数 + 2分確保しなければならないことでしょうか。

では累積和を一つずらすことを考えてみましょう。この場合の累積和は要素数 + 1分確保すればよいです。

$$ \begin{cases} \mathit{acc}_0 = 0\\ \mathit{acc}_{i} = a_i + \mathit{acc}_{i-1}\ \text{where}\ 1 \leq i \leq \mathit{size}\\ \end{cases} $$

作成された累積和テーブルは0-indexのときと変わりません。

$$ \begin{array}{c|cccccc} i & 0 & 1 & 2 & 3 & 4 & 5 & 6\\\hline \mathit{acc}_i & 0 & 1 & 3 & 6 & 2 & 0 & 1 \end{array} $$

これは0-indexのときの累積和と全く同じです。\(s_{[i, j)}\)を計算するときは、以下とします。

$$ s_{[i,j)} = \mathit{acc}_{j-1} - \mathit{acc}_{i-1} $$

書き出したことで頭がスッキリしてきました。特に、0-indexのときの悩みはなくなりそうです。1-indexのときはどうしたらいいんでしょうか。もう少し経験を積んでから考えたほうがよさそうです。

Idiom: Check if given string starts with some string

std::stringとして与えられた文字列がある特定の文字列から始まるかどうかを記述するのは簡単なことのように思えます。これが自分にとっては意外に難しく、いつも時間を割いてしまうのです。先日、コードリーディングをしていた際にうまい方法を見つけたため、イディオムとして記載しておこうと思います。

先日(またもや)やってしまった実装は以下のようなものでした。

  const std::string s("abc");   // abcから始まる文字を探したい

  const int size = s.size();

  const std::string a[] = {
    "abcdef",
    "defabc",
    "abcabc",
    "a"
  };

  for (const auto& i : a)
    if (i.size() >= size)               // 文字列の長さが十分かどうかチェックする
      if (i.substr(0, size) == s)       // 先頭の文字列を切り出して比較する
        // 文字列iはsから始まっている!
};

この実装は色々と無駄があるだけではなく、自分にとっては注意すべき点が多く手早く書けないのです。

  • 文字列iのサイズを検証する -> この際等号を入れるべきだろうか?
  • 文字列iの先頭size文字をstd::stringとして生成する -> この際使うべきsubstrメソッドには引数を1つ与えるべきか。それとも2つ与えるべきか?

そもそもsubstrメソッドは末尾のサイズを適切にチェックしてくれるはずです。

  for (const auto& i : a)
    // if (i.size() >= size)            // サイズのチェックはしなくてよい
      if (i.substr(0, size) == s)
        // 文字列iはsから始まっている!

これでも悪くはないのですが、やはりsubstrの引数の数は悩んでしまいそうです。どうもあれだなあ、とコードリーディングをしていた際に、以下のイディオムを見つけました。

  for (const auto& i : a)
    if (i.find(s) == 0)
      // 文字列iはsから始まっている!

なるほど簡潔で覚えやすそうです。次回からはこのイディオムを使おうと思います。

もう少しだけ

findメソッドを使ったこのイディオムは簡潔でよいのですが、先頭から指定して文字列で始まっていない場合、無駄に探索を行ってしまいます(例えば文字列が"xyzxyzxyzxyzxyzxyzxyz"の場合)。これが無駄に思える場合はstrncmpを使った方がいいかもしれません。

  for (const auto& i : a)
    if (strncmp(i.c_str(), s.c_str(), size) == 0)
      // 文字列iはsから始まっている!

Redirecting Contents of File to Standard Input

1行シェルスクリプトEmacsで極力物事を済ます方針になって久しいのですが、コンテスト中は時間がないので多少のスクリプトを用意しています。先日コンテスト中にこんなスクリプトがあったらいいなあ、と考えついたので早速作成しました。

  1. Emacsでテキストを加工する。具体的には「入力」「空行」「出力」「空行」...という形式とする
  2. コマンドを適用すると、「入力」「出力」がsample1.txt、answer1.txt、...と出力される

例えば以下のようなファイルを用意します。

3 2
1 2 3
1 2
3 2

5

4 3
1 2 3 4
1 2
3 4
3 4

8

これにコマンドを適用すると、以下のようにファイルに格納されるようにしたいのです。

sample1.txt:
3 2
1 2 3
1 2
3 2

answer1.txt:
5

sample2.txt:
4 3
1 2 3 4
1 2
3 4
3 4

answer2.txt:
8

こういう処理はシェルスクリプトでさっさと書いてしまいましょう。

unpack.sh:

#!/bin/sh

i=1

bodyname="sample"

test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"

while read l; do
  if ! echo "$l" | grep '^ *$' > /dev/null; then
    echo $l >> "$bodyname$i.txt"
  else
    if [ "$bodyname" == "sample" ]; then
      bodyname="answer"
    else
      bodyname="sample"

      i=`expr $i + 1`
    fi

    test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"
  fi
done

早速テストして...いい感じですね。内容もよいようです。

> ls
a.txt
> unpack.sh < a.txt
> ls
a.txt       answer2.txt sample1.txt sample3.txt
answer1.txt answer3.txt sample2.txt

さて、これを自分の環境にリリースしたのですが、どうも自分の操作と相性が合わないのです。どうしてだろう? と観察してみたのですが、Emacs diredから!でファイル操作ができないのが原因でした。Emacs diredから!とするにはシェルスクリプトがファイル名を引数として得て処理できなければなりません。

これを解決するのは簡単で、以下の1行を代入するだけでした。

           :
test -f "$bodyname$i.txt" && rm -f "$bodyname$i.txt"

test -f "$1" && exec <"$1"   # この行を挿入

while read l; do
           :

つまるところ、引数としてファイルが与えられていた場合、ファイルの内容を標準入力にリダイレクトしてしまえばよいのです。

さて、これでEmacs diredから快適にコマンドを使うことができるようになりました。今から使う機会が楽しみです。

少しばかりの続き

さて、このスクリプトであると空行が2つ以上続くとよくない結果となります。

> ls
a.txt
> cat a.txt
1  # sample1.txtに書き込まれるはず

2  # answer1.txtに書き込まれるはず


3  # sample2.txtに書き込まれるはず

4  # answer2.txtに書き込まれるはず
> unpack < a.txt
> ls # sample2.txtがない? sample3.txtがある??
a.txt       answer1.txt answer2.txt sample1.txt sample3.txt

空白毎に出力するファイルを更新しているのが原因です。これを防ぐために、予めsedを適用して調整しています。

           :
test -f "$1" && exec <"$1"

sed -ne '
s/^  *$//
H
$ {
  g
  s/^\n*//g
  s/\n*$//g
  s/\(\n\n\)\n*/\1/g
  p
}
' | while read l; do
           :

このsedスクリプトも面白いので興味があれば是非処理を追ってみてください。具体的には、「全て読み込んでから連続する改行を取り除く。ただし、各行が空白を含む空行であればこれも処理する」といったことをしています。また、最新のスクリプトこのGistで管理しています。

Idiom: Calculating Power of 10 using pow Function in Integer Arithmetic

今日参加したコンテストで有効な電話番号の組み合わせ数を求める問題が出題されました。実装ではlong longの大きな値、例えば\(10^{17}\)から小さな値、例えば10や1を引く演算を行う必要がありました。

powを使って10の冪乗を計算する解法も少なくなかったのですが、おそらくそのほとんどはテスト時に落ちてしまいました。どうやら計算誤差が原因のようです。

これは自分にとっても大きな驚きでした。xが整数である場合、pow(10, x)がきっちりと10の冪乗を返すことを知っていたからです。

  long long x = 1;
  
  for (int i = 0; i < 18; i ++, x *= 10)
    assert(pow(10, i) == x);            // pow(10, i)は問題なし

どこで計算誤差が出ているかを考えるために以下のような実装を用意しました。\(10^{17} - (10^0 + 10^1 + 10^2)\)を計算する実装です。期待している答えは末尾が...,999,889となるはずです。

long long c = pow(10, 17);

assert(c == 100000000000000000LL); // ここは問題なし

std::vector<int> a = {0, 1, 2};

for (const auto& i : a)
  c -= pow(10, i);

assert(c == 99999999999999889LL);  // ここでアサーションエラー!

変数cの内容をチェックしました。期待とは異なり、99,999,999,999,999,888でした。

さて、以下は期待通り...,999,889を返します。

long long c = pow(10, 17);

assert(c == 100000000000000000LL);      // ここは問題なし

astd::vector<int> a = {0, 1, 2};

for (const auto& i : a)
  c -= static_cast<long long>(pow(10, i));

assert(c == 99999999999999889LL);       // ここも問題なし!

どうも自分はこれまで、-=演算子は左側の変数の型で演算される、と思い込んでいたようです。つまり右辺の値がまずlong longにキャストされると考えていました。実際には二項の加減算演算子の振る舞いと同様に、doubleにキャストされてから計算され、改めてlong longにキャストされるようです。

これまで挙動を正確に把握していなかったこともあり、pow関数を使って10の冪乗を計算しないようにしていましたが、次のようなイディオムとして今後積極的に使っていきたいな、と考えています。またこの知見を大好きなハックにも役立てたいです。

long long y = static_cast<long long>(pow(10, x));

最後になりますが、TLで気さくに質問に答えてくださった@n_vipさん、ありがとうございました。

追記

これは実装依存の挙動だそうです。一般的に、pow(x, y)のyが整数のときはpowを使うべきではないとのこと。naoya_tさんにご指摘いただきました。どうもありがとうございます)。

参考: c++ - Definitions of sqrt, sin, cos, pow etc. in cmath - Stack Overflow

Series of Xor Arithmetics

先日参加したコンテストにこんな問題が出題されました。排他的論理和を使う問題です。

nとxが与えられる。n個の異なる整数のビットごとに排他的論理和を取ってxとなるようにしたい。n個の非負整数を出力せよ

ただし、1 ≦ n ≦ 105、0 ≦ x ≦ 105とし、出力する非負整数は106までとする

排他的論理和は融通が効く印象がありました。n - 1個の0から始まる連続した整数を用意してそのビットごとの排他的論理和を取り、最後の一個で辻褄を合わせられるんじゃないか? と考えました(その方針の元で実装した解法は無事、システムテストを通過しました!)

排他的論理和の計算は色々なアルゴリズムに登場します。例えばZobrist HashingやRolling Hashを考えるとき、排他的論理和は欠かせません。今回のエントリでは連続した排他的論理和の計算方法について紹介したいと思います。

さて、{1, 0, 0, 1, 0, 1, 0, 1}という数列の排他的論理和を計算することを考えてみましょう。左から演算を行っていくと以下のように計算することができます。

f:id:agw:20171004051714p:plain

なかなか時間がかかりますね。工夫をしてもっと速く計算できるようにならないでしょうか? 排他的論理和の演算は順序を入れ替えても結果が変わりません。これは利用できないでしょうか?

f:id:agw:20171004051700p:plain

試しに、先ほどの数列の隣り合った数を入れ替えてみましょう。

f:id:agw:20171004051719p:plain

先ほどと同様、左から計算します。

f:id:agw:20171004051722p:plain

無事同じ結果になりました(まあ答えは0か1にしかならないので、あまり説得力がないようにも思えますが...)。

もっと入れ替えれば数列を整列できそうです。具体的には0の数字の集まりと1の数字の集まりにできそうです。

f:id:agw:20171004051728p:plain

さて、0と0の排他的論理和は0なのでした。つまり、0 xor 0 = 0。また、0 xor 0 xor 0は(0 xor 0) xor 0と計算すればいいので、0 xor 0となります。この結果も0。つまり、0同士の排他的論理和のグループは0と置き換えることができます。

f:id:agw:20171004051733p:plain

1のかたまりについても同じように工夫ができるでしょうか? 1と1の排他的論理和は0であることを利用すると先ほどの式は以下のようにまとめることができそうです。1の個数が偶数個であれば、0となることが分かります。

f:id:agw:20171004051741p:plain

1の個数が奇数個である場合も考えてみましょう。

f:id:agw:20171004051746p:plain

連続した排他的論理和の計算結果は1が偶数個あれば0、1が奇数個あれば1となることが分かります。1の数を数えるだけで演算結果が分かってしまうのです。

もしこれまで、検算などをする場合に最初の図のように左から計算していたのであれば是非この方法も試してみてください。計算の速さはもちろん、正確さが格段に上がるはずです。

Idiom: Getting the largest number that is less than x

Idiom: Getting the largest number that is less than x

プログラムを書いているとき、思ったことが思った計算量でできることは分かっているのに細かい処理がパッと書けないことってありますよね。今日はこの問題を解いているときに、「std::mapのキーがx未満で最大のものを得たい!」と考え至ったのですが、すぐに繰り出すことができず、悔しい思いをしました。

コンテスト中にこういうことが起こるのはよくあります。よくあることなのであればイディオムとしてまとめられるのではないかな、と思いました。

今回のイディオムは以下です。ただし、要素は整列されているものとします。

  • x未満の最大の値を得る

これはO(logN)で行うことができます。具体的には以下のようにします。

  • std::lower_boundでxの下限を探す。反復子が示す要素の一つ前の値が求める値である

これを図で表せば以下となります。例では4未満の最大の値を探索しています。

要素群にx未満の値がない場合、std::lower_boundは先頭を指し示す反復子を返します。このとき、一つ前の要素にアクセスすることはできないので注意が必要です。


コンテナ独自のlower_boundメソッドを持つstd::setを使って観察してみましょう。std::setの内容は2、3、5、9、15とします。

std::set<int> set = {2, 3, 5, 9, 15};

さて、STLの反復子は要素の間を指し示すものとするとすっきりと考えることができるものも多いです。ここからは以下の図の左の表現方法を取りたいと思います。

3未満の最大の値を探してみましょう。lower_boundの結果は以下のようになるため、反復子を一つ戻したものが求める値、2になります。

auto it = set.lower_bound(3);

//  set: 2 3 5 9 15
//        ^
//        `-- it

*(-- it);	// 3未満の最大の値は2

次に9未満の値を探してみましょう。

auto it = set.lower_bound(9);

//  set: 2 3 5 9 15
//            ^
//            `-- it

*(-- it);	// 9未満の最大の値は5

7のように要素として存在しない数値の場合はどうでしょうか? その場合も問題なく動作します。

auto it = set.lower_bound(7);

//  set: 2 3 5 9 15
//            ^
//            `-- it

*(-- it);	// 7未満の最大の値は5

もちろん、21のように大きな値を入れてもきちんと動作します。この場合、itはstd::end(set)を指しますが特に問題はありません。

auto it = set.lower_bound(21);

//  set: 2 3 5 9 15
//                 ^
//                 `-- it

*(-- it);	// 21未満の最大の値は15

最後に先頭の値、例えば、2未満の最大の値を探す場合ですが、これは注意が必要です。itの一つ手間の要素にアクセスすることはできないからです。

auto it = set.lower_bound(2);

//  set: 2 3 5 9 15
//      ^
//      `-- it (= std::begin(set))

*(-- it);	// これはアクセス違反!

そのため、lower_boundを試したあとに反復子が先頭にあるかどうか調べる必要があります。

auto it = set.lower_bound(x);

if (it == std::begin(set)) {
  // x未満の値は存在しない
}
else {
  *(-- it);	// x未満の最大の値
}

イディオムがはっきり見えてきました。aをコンテナとすると以下のように書くことができそうです。

auto it = std::lower_bound(std::begin(a), std::end(a), x);

// または、auto it = a.lower_bound(x)

if (it == std::begin(a)) {
  // x未満の値は存在しない
}
else {
  *(-- it);	// x未満の最大の値
}

To a better understanding of the Otsu’s method

大津の方法の最も有名な応用例として画像の2値化が挙げられます。この応用での圧倒的ともいえる知名度とその貢献度からか他の問題(例えば機械学習)への応用に関して明示的に言及されることが少ないですが、英語版のWikipediaの記事に記載されているように、その本質は一次元の離散な線形判別分析法の一種です(余談ですが判別分析法という呼び名の他にも判別分別法等、色々な呼び方があるようです)。


大津の方法に関しては画像解析ハンドブックのp. 1520、8 2値画像処理、8.1.2 しきい値自動決定法で詳しく紹介されています。

[2] 大津の方法

大津の判別分析法(Otsu's adaptive thresholding method)とよばれる方法であり、画像の濃淡ヒストグラムから統計的な意味での最適しきい値を決定する。あるしきい値によってヒストグラムを2つのクラスに分割した場合のクラス間分散σB2(k)が最大になるkをしきい値k*に選ぶという原理である。

画像解析ハンドブック

ここで言及されているクラス間分散σB2(k)は以下のように計算される値です。

またNを全画素数、niをレベルiの画素数とします。ここからNとniからレベルiの画素が占める割り合いを算出することができます。これをpiとします。

これらを元に、ω0やω1、μ0やμ1、μτは以下のように定義します。Lは画像全体の諧調数を表します。

これらの式を元に以下の係数ηを最大化するk*を計算します。

右辺の分子は先に挙げた「最大化させる」式です。分母のστ2は全分散です。その名の通り、全体の分散を表します。対象が画像の2値化であれば、画像全体での分散となります。全分散はしきい値にどんな値を設定しても不変なので、ηを最大化するk*を探すときには定数として考えてしまって構いません。先ほどの抜粋では全分散に関して言及されなかったのはこのためです。

画像解析ハンドブックでの大津の方法の解説は以上ですが、これだけだと少し分かり辛いですよね。少なくとも私はそう感じ続けていました。しかし、最近わかりやすいパターン認識という書籍を読み、一気に理解を深めることができました。これを紹介したいと思います。


わかりやすいパターン認識では上述のηを変形したJσ、「クラス内分散・クラス間分散比」を採用し、これを「クラス間分離度」としてしきい値を評価します。

そのためにまず、「クラス内分散」と「クラス間分散」の定義をしています。以下は第5章 特徴の評価とベイズ誤り確率、5.2 クラス内分散・クラス間分散比からの抜粋です。

クラスωiに属するパターン集合をχiとし、χiに含まれるパターン数をni、平均ベクトルをmiとする。また、全パターン数をn、全パターンの平均ベクトルをmとする。ここで、クラス内分散(within-class variable)をσW2、クラス間分散(between-class variance)をσB2で表す

わかりやすいパターン認識

ここで、σW2とσB2は以下とします。

続けて、わかりやすいパターン認識ではクラス内分散とクラス間分散を以下のように説明しています。

クラス内分散はクラスの平均的な広がりを表し、クラス間分散はクラス間の広がりを表している。したがってそれらの比Jσを定義すれば、Jσが大きいほど優れた特徴であると判定することができる。上記Jσはクラス内分散・クラス間分散比(within-class variance between-class variance ratio)と呼ばれる。これはクラス内距離で正規化したクラス間距離とみることもできる。

わかりやすいパターン認識

画像解析ハンドブックで解説されていた大津の方法の式と比べると

  • 連続である
  • 多次元のベクトルを扱っている

という差はありますが、式として圧倒的に見通しのよいものになっていることが分かります。例えばクラス内分散を算出する式は分散(全分散)を算出する式とほとんど変わず、平均値の代わりにそれぞれのクラス(ωi)の平均値miとの差を取ります。逆に言えば、これに模して分散(全分散)の式を書くとすれば以下のようになるでしょう。

クラスに属するデータの分散をσi2とすると、以下のようにも計算できます。

クラス間分散も見た通りです。こちらはクラスに属するデータの平均値の分散を算出しています。クラスの平均値がばらつけばばらつくほど分離度が高いというのは想像と容易に合致することでしょう。


さて、Jσを一見すると、最大化するしきい値を探すときにクラス内分散とクラス間分散の双方を計算しなければならないように見えます。わかりやすいパターン認識ではその点について言及していません。しかし、全分散とクラス内分散、クラス間分散には以下の等式が成り立つことが広く知られているようです。

これを用いると、Jσを以下のように変形することができます。

分散は負の値を取りません。そのためσB2は[0, στ2]の範囲を取ります。この区間でJσはσB2について単調増加する関数ですので、Jσを最大化するしきい値を探すにはσB2を考えれば十分、ということになります(σB2をx、στ2をaとするとJσ = x / (a - x)であり、この微分a / (a - x)2であることを考えてみるとよいでしょう)。

まとめ

  • クラス内分散、クラス間分散を正確に把握することにより、クラス内分散・クラス間分散比をより深く理解することができる
  • 大津の方法は一次元の離散な線形判別分析法の一種である。そのため、上記を理解することにより迷いなく応用することが期待できる

大津の方法は大変有名な手法で独立して紹介されていることが多く、またアルゴリズムとしてもコンパクトに記述することが可能なことからその式の成り立ちが見えにくいように感じていました。そもそもの成り立ちであるクラス内分散・クラス間分散比から考えていったほうが理解が進むことに気付いたときは目から鱗でした。

機械学習や緩和アルゴリズム等で大津の方法のような簡単な判別分析法が活躍する場面は少なくないように思います。それらの応用についてもいつか時間を取って紹介出来たらいいな、と思います。

さて、本エントリでは以下の2つの書籍の記述を元に文書を構築しました。両書とも大変な良著だと思います。

新編 画像解析ハンドブック

新編 画像解析ハンドブック

わかりやすいパターン認識

わかりやすいパターン認識

中でも画像解析ハンドブックは良著中の良著です。多少値は張りますが機会があったら是非目を通してもらいたい書籍です。