[Python] クラスを使ってソースコードを整理する【続き】

Python
この記事は約9分で読めます。

こんにちは、つちもぐらです。先日の記事では説明しきれなかった、RGB.pyファイルに定義したRGBクラスのメソッドについて解説します。RGB.pyファイルの全体については、次をご参照ください。

このRGBクラスは3つのメソッド

  1. __init__() # 初期化(コンストラクタ)メソッド
  2. gain() # 画像処理
  3. SaveBMP() #BMPファイル形式で保存

を持っています。

特に1.__init__() の初期化メソッドでは、

  • ビットマップ(BMP)ファイルのヘッダから、必要な情報の取り出し
  • BMP画像データ本体を、list型のインスタンス変数self.R, self.G, self.Bへ代入

をしています。

では実際に、メソッドを1つずつ見ていきましょう。

補足:クラスの変数

クラスに定義される変数をアトリビュートと呼びます。そしてアトリビュートには、クラス変数とインスタンス変数の2種類が存在します。メソッド内部で定義した場合はインスタンス変数、メソッド外で定義した場合は、クラス変数となります。

クラス変数がlist型のようなイミュータブルな変数で会った場合、クラスから生成したインスタンスへアクセスしてクラス変数の値を変更したとしても、同じクラスから生成した別インスタンスのクラス変数も、同様に変更される事となります。

上記はPythonに慣れていないと?な感じと思いますので、慣れないうちはクラス変数は使わない、またはクラス定義の中でしか値を代入しないというルールとした方が、トラブルが発生しにくいコードとなるでしょう。

スポンサーリンク

Pythonクラスの初期化メソッド(コンストラクタ)

__init__() で始めるメソッドは初期化メソッド、またはコンストラクタと呼ばれます。 コンストラクタといった方が、他のプログラム言語をやっている人とも話が通じ易いでしょう。コンストラクタはクラスからインスタンスを生成した時に必ず実行されるメソッドとなります。具体的には、下記のmain.pyファイル中のソース

 rgb = RGB(directory_path+”/”+input_filename)
行を実行する時に、RGBクラスのコンストラクタが動作します。

RGBクラスの中でコンストラクタには、
def __init__(self, file):
としてファイルパスの引数を持たせ、RGBクラスからインスタンスを生成するには必ずビットマップファイルを用意しなければならない設計としています。

ちなみに第1引数のselfは、Pythonクラスメソッドの第1引数には必ずselfを書くと覚えておいてください。最初は違和感ですが、直に慣れます。

当然ここの設計は自由なのですが、ビットマップファイルを読み込まなければ画像オブジェクトを生成するのに最低限必要な画像の縦・横サイズと、画像本体の画素値の情報が揃わないのでこの設計としました。

BMPファイルのヘッダ読み込み部に関しては先日に紹介した記事とほとんど同じなので、本記事では異なる部分を中心に説明をさせて頂きます。

インスタンス変数の定義

   self.xsize     = int.from_bytes(f.read(4), "little")
   self.ysize     = int.from_bytes(f.read(4), "little")

画像データは縦横のサイズと画像データの中身で定義されますが、その縦横サイズをインスタンス変数self.xsize, self.ysize としてBMPファイルのヘッダ部から値を取得しています。このようにメソッド内部で頭に”self.” を付けた変数(インスタンス変数)は、同じクラス内の他メソッドからアクセスする事が可能となります。

画像データ本体の格納場所を用意

    self.R =  [[0 for y in range(self.ysize )] for x in range(self.xsize )]
    self.G =  [[0 for y in range(self.ysize )] for x in range(self.xsize )]
    self.B =  [[0 for y in range(self.ysize )] for x in range(self.xsize )]

これはPython固有の内包表記を用いて、画像縦・横サイズの2次元のリストを成分毎に生成しています。分かりにくい場合は、次のように書き直してもOKです。この場合、リスト中の初期値は異なりますが、後でBMPファイルの画像データ本体に上書きされるので、問題ありません。

    self.R = []  
    self.G = []
    self.B = []
    for x in range(self.xsize ):
      self.R.append(list(range(self.ysize)))
      self.G.append(list(range(self.ysize)))
      self.B.append(list(range(self.ysize)))

内包表記を用いた方がよりPythonらしいコードとなりますが、ここは自分がやりやすい方を選べばよいでしょう。

クラスの内部変数にBMP本体の画像データを格納

    ### RGBデータ本体読み込み ###
    dummy_size=0
    mod = (self.xsize*3)%4
    if (mod!=0) : dummy_size = 4-mod
    for y in range(self.ysize):
      for x in range(self.xsize):
        self.R[x][y] = int.from_bytes(f.read(1), "little")
        self.G[x][y] = int.from_bytes(f.read(1), "little")
        self.B[x][y] = int.from_bytes(f.read(1), "little")
      f.read(dummy_size)

動作はコードのままなので説明は不要かと思いますが、分かりにくいのは2~4行目と10行目に現れる変数dummy_sizeかと思います。

BMPファイル固有の決まりとして、画像の横1ラインを表すバイト数は4のバイト数でなければならないという決まりがあります。

今回対応したBMPファイルは1画素で3バイト(R,G,Bがそれぞれ1バイト)なので、仮に横サイズ445画素のBMPの場合、画像の横1ラインは、445×3=1335バイト、4で割ると 3余る事が分かります。4で割り切れるようにするためには、横右端ラインの終端に、1バイトのダミーデータを足した画像データと作成する必要があります。

BMPファイル読み出し時には、このダミーデータを無視する必要があります。

まずは水平1ラインの総バイト数(self.xsize*3)を4で割った時の余りmodを求め、次にmodが0でない場合は、必要なダミーデータのサイズdummy_sizeを計算します。

そして10行目で計算したダミーサイズの分だけ画像のライン右端でシーク位置を移動させる事により、ダミーデータを無視します。

画像処理を行うメソッド

  def gain(self, gain=1.0):
    for y in range(self.ysize):
      for x in range(self.xsize):
       ### ゲイン処理と整数化 ###
       tmpR = int(self.R[x][y] * gain)
       tmpG = int(self.G[x][y] * gain)
       tmpB = int(self.B[x][y] * gain)
       ### MAX 255でクリップ ###
       self.R[x][y] = min(tmpR, 255)
       self.G[x][y] = min(tmpG, 255)
       self.B[x][y] = min(tmpB, 255)

画像のサイズを示すインスタンス変数 self.xsize, self.ysize と、画像データ本体を格納するリスト型の変数self.R[x][y],self.G[x][y],self.B[x][y]を用いて、画像本体のRGBデータをゲイン&クリップ処理を行ったものへと書き換えています。

今回紹介したソースではこのgainメソッドもRGBクラスのメソッドとしましたが、 RGBクラスとは別にクラスを用意して、そのクラスのメソッドとしても全く問題ありません。その場合、引数には
 def gain(self, rgb, gain=1.0):
とRGBクラスのインスタンスを1つ増やす事となります。

例えばHDRのような複雑な画像処理を行う場合、コードの見通しを良くするために多くの関数に分けてソースを実装したくなりますが、そのような場合は別クラスとしてHDRクラスを用意して、そのクラスの中でHDR処理に必要な関数をメソッドとして用意して、それらメソッドの束ねたTOPのメソッドをPythonスクリプトの最上位で呼び出す実装とした方が、コードの管理がしやすいでしょう。

ビットマップ(BMP)ファイルを保存するメソッド

SaveBMPメソッド()で分かりにくいと思われる個所を抜粋して説明します。具体的にはBMP固有のダミーデータ対応()となります。

 def SaveBMP(self,file):
    ### メソッドで使用する変数 ###   
    xsize = self.xsize
    ysize = self.ysize
    dummy_size=0
    mod = (self.xsize*3)%4
    if (mod!=0) : dummy_size = 4 - mod
    bfSize = (xsize*3+dummy_size)*ysize

インスタンス変数 self.xsize, self.ysize から、ダミーデータのサイズdummy_sizeを計算します。この dummy_size はコンストラクタ __init__ メソッドでも出てきて、計算結果も同じとなるのですが、BMPファイル読み込み~BMPファイルの保存の間で画像サイズに変更が入った場合、 dummy_size の値も変わる可能性が高くなるので、汎用的なコードとするためにも、独立してダミーデータのサイズを計算する必要があります。

### RGBデータ本体へ書き込み ###
for y in range(ysize):
  for x in range(xsize):
    f.write(self.R[x][y].to_bytes(1,"little",signed=False))
    f.write(self.G[x][y].to_bytes(1,"little",signed=False))
    f.write(self.B[x][y].to_bytes(1,"little",signed=False))

  ### 画像の横ラインデータサイズを4の倍数にそろえる ###
  for i in range(dummy_size): 
    f.write((255).to_bytes(1,"little",signed=False))

9~10行目でdummy_sizeのバイト数だけ、ダミーデータとして整数255を書き込んでいます。書き込む値は1Byteに収まる整数(unsigned 0~255, signed -128~127) )であれば何でもOKです。

タイトルとURLをコピーしました