2023年2月7日火曜日

Python+Kivy 備忘録 BMPファイルを読んでみる編

はい、前回テキストを読みましたが、今回はBMPファイルです。
といっても処理を簡単にしたかったので、24ビットBMPファイルしか読めません。

## load_bmp ##

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.graphics import Color, Ellipse, Line, Rectangle
from kivy.core.window import Window

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput

from kivy.uix.floatlayout import FloatLayout
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.uix.popup import Popup

import os
from os.path import join,isdir
import uuid
from datetime import datetime

from kivy.uix.filechooser import FileChooserIconView
from kivy.uix.filechooser import FileChooserListView

from kivy.clock import Clock

Window.clearcolor = (0,0,0,0)

class BtnChangeSelectFile(Button):
    def on_release(self):
        self.parent.DrawSelectFile()
        
class BtnSelectFile(Button):
    def on_release(self):
        self.parent.parent.lFile.text='\n'.join(self.parent.fc.selection)
        self.parent.DrawBase()
        
class BtnChangeBase(Button):
    def on_release(self):
        self.parent.DrawBase()

class RGB():
    def __init__(self,r,g,b):
        self.r = r
        self.g = g
        self.b = b
        
class Output(BoxLayout):
    def __init__(self,path,**kwargs):
        super(Output,self).__init__(**kwargs)

        outputtext = path
        header = []
        
        self.map_width = 0
        self.map_height = 0
        self.bitmap:RGB = []

        self.list_la =[]
        self.list_rc =[]

        while True :
            if not os.path.exists(path):
                break
            outputtext = "exists"
            
            size = os.path.getsize(path)
            if size < 54:
                outputtext = "header not found"
                break
                
                
            with open(path,'rb') as f: 
                while True :
                    header = f.read(54)
                    
                    if not ((header[0]==66)&(header[1]==77)):
                        outputtext = "this file is not bmp"
                        break
                    
                    filesize = int(header[2]+(header[3]<<8)+(header[4]<<16)+(header[5]<<24))
                    headersize = int(header[10]+(header[11]<<8)+(header[12]<<16)+(header[13]<<24))
                    colorbit = int(header[28]+(header[29]<<8))
                    w = int(header[18]+(header[19]<<8)+(header[20]<<16)+(header[21]<<24))
                    h = int(header[22]+(header[23]<<8)+(header[24]<<16)+(header[25]<<24))
                    
                    if headersize != 54:
                        outputtext = "this format not surpported"
                        break
                        
                    if colorbit != 24:
                        outputtext = "24 bit color only"
                        break
                        
                    ### read bitmap ###
                    hsize = int((filesize - headersize)/h)
                    d = f.read(filesize - headersize)
                    
                    for y in range(h):
                        for x in range(w):
                            idx = hsize * y + x * 3
                            self.bitmap.append(RGB(b = int(d[idx+0]),g = int(d[idx+1]),r = int(d[idx+2])))
                            
                    outputtext ="colorbit:"+str(colorbit)+"width:"+str(w)+"/height:"+str(h)
                    self.map_width = w
                    self.map_height = h

                    break
            break
        print(outputtext)
        self.text = outputtext

    def Draw(self):
        if not self.bitmap:
            return
            
        with self.canvas.before:
            # clear all rectangle
            for old in self.list_rc:
                self.canvas.before.children.remove(old)
            self.list_rc.clear()
            
            # clear all label
            for old in self.list_la:
                self.canvas.before.children.remove(old.canvas)
            self.list_la.clear()
            
            w = self.size[0]/self.map_width
            h = self.size[1]/self.map_height
            idx = 0
            for y in range(int(self.map_height)):
                for x in range(int(self.map_width)):
                    Color(self.bitmap[idx].r/255,self.bitmap[idx].g/255,self.bitmap[idx].b/255)
                    idx = idx+1
                    self.list_rc.append(Rectangle(pos=(self.right-self.size[0]+x*w,self.top-self.size[1]+y*h),size=(w,h)))

class SubDisplay(BoxLayout):
    def __init__(self,**kwargs):
        super(SubDisplay,self).__init__(**kwargs)
        self.fc=FileChooserListView()
        #self.fc=FileChooserIconView()
        self.fc.size_hint_y=7
        self.fc.filters=['*.bmp']
        self.mode = -1 # setup

        Clock.schedule_interval(self.update, 1.0/3.0)

    def update(self,dt):
        if self.mode == 0: # Base Mode
            self.main_disp.Draw()
            return
        
    def DrawBase(self):
        self.clear_widgets()
        self.orientation='vertical'
        
        self.main_disp =Output(path=self.parent.lFile.text,size_hint_y=4)
        b1 =BtnChangeSelectFile(text='SelectFile',size_hint_y=1)
        self.add_widget(self.main_disp)
        self.add_widget(b1)
        
        self.mode = 0 # Base
        
    def DrawSelectFile(self):
        self.clear_widgets()
        self.orientation='vertical'

        b2=BtnSelectFile(text='SelectFile',size_hint_y=1)
        b3=BtnChangeBase(text='QUIT',size_hint_y=1)
        
        self.add_widget(self.fc)
        self.add_widget(b2)
        self.add_widget(b3)
        
        self.mode = 1 # Select

class Display(BoxLayout):
    orientation='vertical'
    def __init__(self,**kwargs):
        super(Display,self).__init__(**kwargs)
        self.lFile = Label(text='No Selected',size_hint_y=1)
        self.boxSub = SubDisplay(size_hint_y=8)
        self.add_widget(self.lFile)
        self.add_widget(self.boxSub)
        self.boxSub.DrawBase()
        
class LoadBmpApp(App):
    def build(self):
        layout = Display()
        return layout

if __name__=='__main__':
    LoadBmpApp().run()

※毎度の事ですが、一部字を小さくしているので、コードはコピペして読む事を推奨します

  • キモは BoxLayout 派生クラスの Output クラス
解説するとしたらイニシャライズで行っている、BMPファイルの読み込みと Draw 関数でしょうか。
そこまで怪しい処理は組んでいないと思いますが、いくつか注意点を連ねます。

  •  hsize という変数はビットマップの横一列分のデータサイズです
ビットマップは横一列のデータサイズが4バイト区切りで無いといけないというルールがあります。しかし、24ビット=3バイトで1ドットを表現している都合上、画像サイズによっては4で割り切れない場合があります。
この場合、末端に穴埋め用のデータを詰めて4バイトで割れるように整えます(パディングとか言います)。
が、この計算が面倒だったので、単純に画像データサイズ(ファイルサイズ-ヘッダーサイズ)をビットマップの縦のドット数で割って求めています。

  • 各ドットの色の順
一般的に光の三原色はRGBと並べますが、ファイルにはBGRの順に並んで格納されているので取り出す際に注意が必要です。

  • 描画は1秒間に3回行っている
Output クラスの Draw メソッドは親の SubDisplay クラスから1/3秒ごとに呼ばれる事になっています。
これは add_widget 直後ではサイズが定かではない為、正しく描画ができなかった為です。

…で、動かしてみたのですが…
…動く…けど、無茶苦茶重い。
32x32ドットの画を1024個の Rectangle で秒間3回描画しているだけなのに異常に重い。
コレはなにかしらやらかしている予感がしますね。
本当は次はテクスチャフォーマットの読み込みをやりたかったのですが、改良編になりそうです。

0 件のコメント:

コメントを投稿