2019年8月23日金曜日

ヒストグラムマッチングで画像の色を変換

最近、画像処理関係でヒストグラムマッチングというアルゴリズムがあるのを知りました。Pythonのライブラリscikit-imageにはこれがあるので、実際に試してみました。

ヒストグラムマッチングとは

ヒストグラムから累積分布関数を作成して、2つの累積分布関数$S$と$G$について、$S(r)=G(z)$となるように$r$を$z$にマッピングするらしいです。
参考:Wikipedia : Histogram matching

実施1

1つ目の画像の色を2つ目の画像の色に変換します。

画像はどちらもぱくたそ(www.pakutaso.com)から使わせて頂いています。
コードはほとんどscikit-imageのサンプルコードの写経です。
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from skimage.transform import match_histograms

referenceimage = Image.open("reference.jpg")
reference=np.asarray(referenceimage)

srcimage = Image.open("src.jpg")
src = np.asarray(srcimage)

matched = match_histograms(src, reference, multichannel = True)

fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3),
                                    sharex=True, sharey=True)
for aa in (ax1, ax2, ax3):
    aa.set_axis_off()

ax1.imshow(src)
ax1.set_title('Source')
ax2.imshow(reference)
ax2.set_title('Reference')
ax3.imshow(matched)
ax3.set_title('Matched')

plt.tight_layout()
plt.show()

結果1

次のような結果になりました。

個人的には植物の葉の部分がもっと紅くなるかと思ったのですが、そうはなりませんでした。

実施2

先ほどは画像のRGBそれぞれに対してヒストグラムマッチングを行いましたが、より人間の直感に近そうなHSV色空間に変換してからヒストグラムマッチングをしたらどうなるか見てみました。
import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from skimage.transform import match_histograms

def RGBtoHSV(r, g, b):
    r = int(r)
    g = int(g)
    b = int(b)
    max = r if r > g else g
    max = max if max > b else b
    min = r if r < g else g
    min = min if min < b else b
    h = max - min
    if h > 0:
        if max == r:
            h = 60 * ((g - b) / h)
            if h < 0.:
                h += 360
        elif max == g:
            h = 60 * ((b - r) / h) + 120
        else:
            h = 60 * ((r - g) / h) + 240
    s = max - min
    if max != 0.:
        s = s / max
    s = s * 100
    v = max / 255 * 100
    return h, s, v

def HSVtoRGB(h, s, v):
    max = v * 255 / 100
    min = s
    min = min * max
    min = min / 100
    min = max - min
    if h >= 0 and h < 60:
        r = max
        g = (h / 60) * (max - min) + min
        b = min
    elif h >= 60 and h < 120:
        r = (120 - h) / 60 * (max - min) + min
        g = max
        b = min
    elif h >= 120 and h < 180:
        r = min
        g = max
        b = (h - 120) / 60 * (max - min) + min
    elif h >= 180 and h < 240:
        r = min
        g = (240 - h) / 60 * (max - min) + min
        b = max
    elif h >= 240 and h < 300:
        r = (h - 240) / 60 * (max - min) + min
        g = min
        b = max
    else:
        r = max
        g = min
        b = (360 - h) / 60 * (max - min) + min
    return r, g, b

referenceimage = Image.open("reference.jpg")
reference=np.asarray(referenceimage)
referencehsv = np.asarray([RGBtoHSV(reference[i,j,0], reference[i,j,1], reference[i,j,2]) for i in range(reference.shape[0]) for j in range(reference.shape[1])], dtype = np.float)
referencehsv = referencehsv.reshape(reference.shape)

srcimage = Image.open("src.jpg")
src = np.asarray(srcimage)
srchsv = np.asarray([RGBtoHSV(src[i,j,0], src[i,j,1], src[i,j,2]) for i in range(src.shape[0]) for j in range(src.shape[1])], dtype = np.float)
srchsv = srchsv.reshape(src.shape)

matchedhsv = match_histograms(srchsv, referencehsv, multichannel = True)
matched = np.asarray([HSVtoRGB(matchedhsv[i,j,0], matchedhsv[i,j,1], matchedhsv[i,j,2]) for i in range(matchedhsv.shape[0]) for j in range(matchedhsv.shape[1])],dtype = np.uint8)
matched = matched.reshape(matchedhsv.shape)

fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3),
                                    sharex=True, sharey=True)
for aa in (ax1, ax2, ax3):
    aa.set_axis_off()

ax1.imshow(src)
ax1.set_title('Source')
ax2.imshow(reference)
ax2.set_title('Reference')
ax3.imshow(matched)
ax3.set_title('Matched')

plt.tight_layout()
plt.show()

結果2

次のような結果になりました。

全体の色の分布しか見ないで変換しているため違和感はありますが、結果1よりは紅葉っぽい色合いになった気がします。
違和感なく画像の色を自動で変えるにはもっと複雑な処理が必要そうですが、これだけでも手軽に動かせて面白いと思いました。

2019年8月19日月曜日

オオカミとヤギとキャベツをVB.NETで解く(全コード)

(1)(2)で作ったコードの全体を載せます。

Enum Operate    '可能な操作
    Start       '初回
    Solo        '農夫のみ
    WithWolf    '農夫とオオカミ
    WithGoat    '農夫とヤギ
    WithCabbage '農夫とキャベツ
End Enum

Class State
    Private Farmer As Boolean   '農夫が東岸に居るか
    Private Wolf As Boolean     'オオカミが東岸に居るか
    Private Goat As Boolean     'ヤギが東岸に居るか
    Private Cabbage As Boolean  'キャベツが東岸にあるか

    Private Parent As State     '1つ前の状態
    Private Operation As Operate '実行した操作
    Private Steps As Integer    '移動回数

    '1つ前の状態を取得
    Public ReadOnly Property ParentState As State
        Get
            Return Me.Parent
        End Get
    End Property

    Public Sub New(ByVal Farmer As Boolean,
                   ByVal Wolf As Boolean,
                   ByVal Goat As Boolean,
                   ByVal Cabbage As Boolean,
                   ByVal Parent As State,
                   ByVal Operation As Operate)
        Me.Farmer = Farmer
        Me.Wolf = Wolf
        Me.Goat = Goat
        Me.Cabbage = Cabbage
        Me.Parent = Parent
        Me.Operation = Operation
        If Parent Is Nothing Then
            Me.Steps = 0
        Else
            Me.Steps = Parent.Steps + 1
        End If
    End Sub

    'メンバーが同じなら等価とする
    Public Overloads Function Equals(ByVal obj As State)
        Return Me.Farmer = obj.Farmer AndAlso Me.Wolf = obj.Wolf AndAlso Me.Goat = obj.Goat AndAlso Me.Cabbage = obj.Cabbage
    End Function

    'メンバーが同じなら等価とする
    Public Overrides Function Equals(obj As Object) As Boolean
        'どちらかがNothingまたは型が違えば等価ではない
        If obj Is Nothing OrElse Me Is Nothing OrElse Me.GetType() IsNot obj.GetType() Then
            Return False
        Else
            Return Equals(CType(obj, State))
        End If
    End Function

    'EqualsがTrueの時に同じ値を返す
    Public Overrides Function GetHashCode() As Integer
        Return -(CInt(Me.Farmer) * 8 + CInt(Me.Wolf) * 4 + CInt(Me.Goat) * 2 + CInt(Me.Cabbage))
    End Function

    'ゴールかどうか(農夫、オオカミ、ヤギ、キャベツが東岸にある状態)
    Public Function IsGoal() As Boolean
        Return Farmer AndAlso Wolf AndAlso Goat AndAlso Cabbage
    End Function

    '初期状態(農夫、オオカミ、ヤギ、キャベツが西岸にある状態)
    Public Shared Function StartState() As State
        Return New State(False, False, False, False, Nothing, Operate.Start)
    End Function

    '失敗かどうか(農夫が居ない時にオオカミとヤギが一緒に居る、またはヤギとキャベツが一緒にある状態)
    Public Function IsFailure() As Boolean
        Return Farmer <> Goat AndAlso (Wolf = Goat OrElse Goat = Cabbage)
    End Function

    '移動方法に合わせて次の状態を生成
    Public Function MakeNext(ByVal Operation As Operate) As State
        Select Case Operation

            Case Operate.Solo
                Return New State(Not Me.Farmer, Me.Wolf, Me.Goat, Me.Cabbage, Me, Operation)

            Case Operate.WithWolf
                If Me.Farmer = Me.Wolf Then
                    Return New State(Not Me.Farmer, Not Me.Wolf, Me.Goat, Me.Cabbage, Me, Operation)
                End If

            Case Operate.WithGoat
                If Me.Farmer = Me.Goat Then
                    Return New State(Not Me.Farmer, Me.Wolf, Not Me.Goat, Me.Cabbage, Me, Operation)
                End If

            Case Operate.WithCabbage
                If Me.Farmer = Me.Cabbage Then
                    Return New State(Not Me.Farmer, Me.Wolf, Me.Goat, Not Me.Cabbage, Me, Operation)
                End If

        End Select

        Return Nothing

    End Function

    '状態を出力
    Public Sub Print()

        If Me.Operation <> Operate.Start Then
            Dim OnBoat As String = ""
            Select Case Me.Operation
                Case Operate.Solo
                    OnBoat = "F "
                Case Operate.WithWolf
                    OnBoat = "FW"
                Case Operate.WithGoat
                    OnBoat = "FG"
                Case Operate.WithCabbage
                    OnBoat = "FC"
            End Select
            If Me.Farmer Then
                OnBoat = "---" & OnBoat & "-->"
            Else
                OnBoat = "<--" & OnBoat & "---"
            End If
            Console.WriteLine(Space(6) & OnBoat)
        End If

        Dim West As String = ""
        Dim East As String = ""

        If Farmer Then
            East &= "F"
        Else
            West &= "F"
        End If

        If Wolf Then
            East &= "W"
        Else
            West &= "W"
        End If

        If Goat Then
            East &= "G"
        Else
            West &= "G"
        End If

        If Cabbage Then
            East &= "C"
        Else
            West &= "C"
        End If

        Console.WriteLine(West.PadRight(5) & "~~~~~" & East.PadLeft(5))

    End Sub

End Class

Module Module1

    '前の状態をたどって結果を出力
    Private Sub Result(ByVal NowState As State)
        If NowState.ParentState IsNot Nothing Then Result(NowState.ParentState)
        NowState.Print()
    End Sub

    Sub Main()
        Dim SearchQueue As Queue(Of State) = New Queue(Of State)    '未探索の状態を格納するキュー
        Dim SearchedList As List(Of State) = New List(Of State)     '探索済みの状態を格納するリスト
        Dim NowState As State = State.StartState()                  '現在調べている状態
        Dim NextState As State                                      '次の状態
        SearchQueue.Enqueue(NowState)
        While SearchQueue.Count > 0
            NowState = SearchQueue.Dequeue()

            If NowState.IsFailure() Then Continue While '失敗なら他の状態を探索

            If SearchedList.IndexOf(NowState) >= 0 Then Continue While '探索済みなら他の状態を探索

            If NowState.IsGoal() Then   'ゴールなら結果を出力して終了
                Result(NowState)
                Exit While
            End If

            For Each Operation As Operate In [Enum].GetValues(GetType(Operate)) '現在の状態から移れる状態を生成してキューに格納
                NextState = NowState.MakeNext(Operation)
                If NextState IsNot Nothing Then SearchQueue.Enqueue(NextState)
            Next

            SearchedList.Add(NowState)  '探索済みに追加

        End While
        Console.ReadLine()
    End Sub

End Module

オオカミとヤギとキャベツをVB.NETで解く(2)

前回の続きになります。

探索

次のようにゴールを探索することにします。

  1. 初期状態をキューに格納する
  2. キューから状態をひとつ取り出し今の状態とする
  3. 今の状態が失敗なら2に戻る
  4. 今の状態が過去にチェックした状態と同じなら2に戻る
  5. 今の状態がゴールなら終了する
  6. 今の状態から移れる状態を全てキューに格納する
  7. 今の状態をチェック済みリストに加える
  8. 2に戻る

次のようなコードになりました。

Module Module1
    Sub Main()
        Dim SearchQueue As Queue(Of State) = New Queue(Of State)    '未探索の状態を格納するキュー
        Dim SearchedList As List(Of State) = New List(Of State)     '探索済みの状態を格納するリスト
        Dim NowState As State = State.StartState()                  '現在調べている状態
        Dim NextState As State                                      '次の状態
        SearchQueue.Enqueue(NowState)
        While SearchQueue.Count > 0
            NowState = SearchQueue.Dequeue()

            If NowState.IsFailure() Then Continue While '失敗なら他の状態を探索

            If SearchedList.IndexOf(NowState) >= 0 Then Continue While '探索済みなら他の状態を探索

            If NowState.IsGoal() Then   'ゴールなら終了
                Exit While
            End If

            For Each Operation As Operate In [Enum].GetValues(GetType(Operate)) '現在の状態から移れる状態を生成してキューに格納
                NextState = NowState.MakeNext(Operation)
                If NextState IsNot Nothing Then SearchQueue.Enqueue(NextState)
            Next

            SearchedList.Add(NowState)  '探索済みに追加

        End While
        Console.ReadLine()
    End Sub

End Module

過去にチェックした状態と同じか判定するためにState同士を比較できるようにします。Equalsメソッドをオーバーライドし、農夫、オオカミ、ヤギ、キャベツの状態が同じであれば同じ状態となるようにします。

'メンバーが同じなら等価とする
Public Overloads Function Equals(ByVal obj As State)
    Return Me.Farmer = obj.Farmer AndAlso Me.Wolf = obj.Wolf AndAlso Me.Goat = obj.Goat AndAlso Me.Cabbage = obj.Cabbage
End Function

'メンバーが同じなら等価とする
Public Overrides Function Equals(obj As Object) As Boolean
    'どちらかがNothingまたは型が違えば等価ではない
    If obj Is Nothing OrElse Me Is Nothing OrElse Me.GetType() IsNot obj.GetType() Then
        Return False
    Else
        Return Equals(CType(obj, State))
    End If
End Function

'EqualsがTrueの時に同じ値を返す
Public Overrides Function GetHashCode() As Integer
    Return -(CInt(Me.Farmer) * 8 + CInt(Me.Wolf) * 4 + CInt(Me.Goat) * 2 + CInt(Me.Cabbage))
End Function

最後に探索した結果を確認できるように結果を出力する部分を作っていきます。
状態を表すStateクラスは1つ前の状態を覚えているため、再帰的に前の状態をたどることで初期状態からゴールまでの状態を出力できそうです。

まず1つ前の状態にアクセスできるようStateにプロパティを用意します。

'1つ前の状態を取得
Public ReadOnly Property ParentState As State
    Get
        Return Me.Parent
    End Get
End Property

次にStateに今の状態を出力する関数を作ります。

'状態を出力
Public Sub Print()

    If Me.Operation <> Operate.Start Then
        Dim OnBoat As String = ""
        Select Case Me.Operation
            Case Operate.Solo
                OnBoat = "F "
            Case Operate.WithWolf
                OnBoat = "FW"
            Case Operate.WithGoat
                OnBoat = "FG"
            Case Operate.WithCabbage
                OnBoat = "FC"
        End Select
        If Me.Farmer Then
            OnBoat = "---" & OnBoat & "-->"
        Else
            OnBoat = "<--" & OnBoat & "---"
        End If
        Console.WriteLine(Space(6) & OnBoat)
    End If

    Dim West As String = ""
    Dim East As String = ""

    If Farmer Then
        East &= "F"
    Else
        West &= "F"
    End If

    If Wolf Then
        East &= "W"
    Else
        West &= "W"
    End If

    If Goat Then
        East &= "G"
    Else
        West &= "G"
    End If

    If Cabbage Then
        East &= "C"
    Else
        West &= "C"
    End If

    Console.WriteLine(West.PadRight(5) & "~~~~~" & East.PadLeft(5))

End Sub

アルファベットF,W,G,Cはそれぞれ農夫、オオカミ、ヤギ、キャベツを表していて、それぞれどっちに移動したか、どっちに居るかを表現しています。

Module1内に次の関数を付け足します。再帰的に前の状態をたどり、初期状態から順番に状態を出力します。

'前の状態をたどって結果を出力
Private Sub Result(ByVal NowState As State)
    If NowState.ParentState IsNot Nothing Then Result(NowState.ParentState)
    NowState.Print()
End Sub

ゴールしたら結果を出力するようにMainのゴール判定の部分で関数Resultを呼びます。

If NowState.IsGoal() Then   'ゴールなら結果を出力して終了
 Result(NowState)
 Exit While
End If

結果

作ったプログラムを実行すると次のような結果が出力されました。

FWGC ~~~~~
      ---FG-->
WC   ~~~~~   FG
      <--F ---
FWC  ~~~~~    G
      ---FW-->
C    ~~~~~  FWG
      <--FG---
FGC  ~~~~~    W
      ---FC-->
G    ~~~~~  FWC
      <--F ---
FG   ~~~~~   WC
      ---FG-->
     ~~~~~ FWGC

文章にすると、手順は

  1. ヤギを運ぶ
  2. 農夫だけ戻る
  3. オオカミを運ぶ
  4. ヤギを戻す
  5. キャベツを運ぶ
  6. 農夫だけ戻る
  7. ヤギを運ぶ

という結果になりました。

次の記事で今回作ったコードを載せておきます。

2019年8月8日木曜日

オオカミとヤギとキャベツをVB.NETで解く(1)

オオカミとヤギとキャベツをVB.NETで解く(1)

背景

プログラミングで何か作ろうと思い、難しすぎない手頃な課題がないかと探していたら「オオカミとヤギとキャベツ」というのがあったのでやってみました。

オオカミとヤギとキャベツとは

オオカミとヤギを連れ、キャベツを持った農夫が川岸にいる。川には1艘のボートがある。

  • ボートを漕げるのは農夫のみ。
  • ボートには農夫のほか、動物1頭かキャベツ1個しか乗せられない。
  • 農夫がいないときにオオカミとヤギを岸に残すと、オオカミがヤギを食べてしまう。
  • 農夫がいないときにヤギとキャベツを岸に残すと、ヤギがキャベツを食べてしまう。

すべてを無事に対岸に渡すにはどうしたらよいか?

(Wikipediaより)

方針

状態を表すクラスStateを用意して、そこに農夫、オオカミ、ヤギ、キャベツがそれぞれどちら側の岸にいるかをメンバーとして持たせる方針で行きます。ここでは西岸から東岸に渡るとして、農夫、オオカミ、ヤギ、キャベツが東岸にいるかどうかをBoolean型で格納することにします。

Class State
    Private Farmer As Boolean   '農夫が東岸に居るか
    Private Wolf As Boolean     'オオカミが東岸に居るか
    Private Goat As Boolean     'ヤギが東岸に居るか
    Private Cabbage As Boolean  'キャベツが東岸にあるか
End Class

Stateクラスにその状態が終了条件と一致しているか判定する関数を付け足します。
終了条件は「農夫、オオカミ、ヤギ、キャベツが全て東岸にいる(つまり全部True)」となります。

Public Function IsGoal() As Boolean
 Return Farmer AndAlso Wolf AndAlso Goat AndAlso Cabbage
End Function

同様にその状態が失敗であるか判定する関数を付け足します。
失敗条件は「農夫がいない時にオオカミとヤギが同じ岸にいる」または「農夫がいない時にヤギとキャベツが同じ岸にいる」となります。
この条件は「農夫とヤギが異なる岸にいる かつ (オオカミとヤギが同じ岸にいる または ヤギとキャベツが同じ岸にいる)」と言い換えることができます。

Public Function IsFailure() As Boolean
    Return Farmer <> Goat AndAlso (Wolf = Goat OrElse Goat = Cabbage)
End Function

次に、可能な渡り方を作っていきます。渡り方は農夫のみ、農夫とオオカミ、農夫とヤギ、農夫とキャベツの4パターンです。渡り方のパターンは限られているのでEnumを使って表現します。

Enum Operate    '可能な操作
    Solo        '農夫のみ
    WithWolf    '農夫とオオカミ
    WithGoat    '農夫とヤギ
    WithCabbage '農夫とキャベツ
End Enum

この時点で自分は、ただ探索してゴールにたどり着いただけではゴールまでの過程がわからないということに気付きました。そこで最初に作ったクラスStateに「1つ前の状態」「1つ前に行った操作」「現在のステップ数」を新たに追加することにしました。Stateクラスに以下のメンバー変数を追加します。

 Private Parent As State     '1つ前の状態
 Private Operation As Operate '実行した操作
 Private Steps As Integer    '移動回数

初期状態には実行した操作が存在しないため、初期状態限定のOperateを追加して以下のようにしました。

Enum Operate    '可能な操作
    Start       '初回
    Solo        '農夫のみ
    WithWolf    '農夫とオオカミ
    WithGoat    '農夫とヤギ
    WithCabbage '農夫とキャベツ
End Enum

次にStateにコンストラクタを作って、初期状態を作ったりOperateを渡された時に次の状態を生み出せるようにしていきます。
コンストラクタは次のように実装しました。

Public Sub New(ByVal Farmer As Boolean,
               ByVal Wolf As Boolean,
               ByVal Goat As Boolean,
               ByVal Cabbage As Boolean,
               ByVal Parent As State,
               ByVal Operation As Operate)
    Me.Farmer = Farmer
    Me.Wolf = Wolf
    Me.Goat = Goat
    Me.Cabbage = Cabbage
    Me.Parent = Parent
    Me.Operation = Operation
    If Parent Is Nothing Then
        Me.Steps = 0
    Else
        Me.Steps = Parent.Steps + 1
    End If
End Sub

続いて初期状態を得る関数を作ります。

Public Shared Function StartState() As State
    Return New State(False, False, False, False, Nothing, Operate.Start)
End Function

与えられたOperateによって次の状態を作る関数も作ります。
農夫、オオカミ、ヤギ、キャベツはBoolean型なのでボートによる移動はNotを使って表現できます。
指定された移動ができない時はNothingを返すようにします。

Public Function MakeNext(ByVal Operation As Operate) As State
    Select Case Operation
    
        Case Operate.Solo
            Return New State(Not Me.Farmer, Me.Wolf, Me.Goat, Me.Cabbage, Me, Operation)
            
        Case Operate.WithWolf
            If Me.Farmer = Me.Wolf Then
                Return New State(Not Me.Farmer, Not Me.Wolf, Me.Goat, Me.Cabbage, Me, Operation)
            End If
            
        Case Operate.WithGoat
            If Me.Farmer = Me.Goat Then
                Return New State(Not Me.Farmer, Me.Wolf, Not Me.Goat, Me.Cabbage, Me, Operation)
            End If
            
        Case Operate.WithCabbage
            If Me.Farmer = Me.Cabbage Then
                Return New State(Not Me.Farmer, Me.Wolf, Me.Goat, Not Me.Cabbage, Me, Operation)
            End If
            
    End Select

    Return Nothing

End Function

長くなってきたので続きは次回書きます。

2019年8月7日水曜日

RotateFlipでGDI+汎用エラーが出た

rotateflipでGDI+汎用エラー

背景

画像を読み込み回転させる処理を作成していました。
このとき訳あって読み込んだ画像を即座に削除する必要がありました。
Dim stream As New FileStream(FileName, FileMode.Open, FileAccess.Read)
Dim img As Bitmap = Image.FromStream(stream)
stream.Close()
File.Delete(FileName)
img.RotateFilp(RotateFlipType.Rotate180FlipNone)
このようなコードを実行すると5行目で「GDI+ で汎用エラーが発生しました。」という例外が発生しました。

原因

「GDI+ で汎用エラーが発生しました。」は原因が掴みにくいのですが、どうやら3行目でFileStreamを閉じてしまうと画像の保存などが出来なくなるらしく、今回の画像が回転できないのもこれが原因でした。
参考:Image.Save()でのエラー

実施

自分の場合はFileStreamにこだわる必要は無かったため、以下のように書き換えて対処しました。
Dim img As Bitmap
Using tmp As New Bitmap(FileName)
 img = tmp.Clone()
End Using
File.Delete(FileName)
img.RotateFilp(RotateFlipType.Rotate180FlipNone)