Python独習!

習得したPython知識をペイフォワード

Pythonで自動化!複数画像から輝度値を取得してテキストに出力する

画像から輝度値を抽出したいが、大量にあるので自動化したい。
filedialog.askopenfilenamesを使ってファイルダイアログで複数画像を一括で選択できるようにする。

解説

filedialog.askopenfilenamesの使い方

  • filetypesで開くファイルの種類を指定、initialdirでデフォルトパスを指定するだけ。ちなみに...namesのsを抜くとファイルが1個しか選択できない。複数選択したい場合はsを忘れないこと。
  • filesはタプル型になり、選択したファイルのパスが格納される。なので、このタプルをfor文でアンパックしてあげれば一括処理ができる。
files = filedialog.askopenfilenames(filetypes = ***, initialdir = ***)


サンプルプログラム

デスクトップに保存されている赤、青、緑の3枚の画像から一括で輝度値を取得してテキストに出力するプログラムを作成した。

  • 下の画像はファイルダイアログで画像を選択するところ。



  • 下の画像は出力結果。※見やすいように後から編集で適当にスペースを入れてある。


ソースコード

# 35_GetIntensity_001.py
# python 3.8.1
# opencv-python 4.1.2.30 
# coding: utf-8

import os
import datetime
import cv2
import tkinter as tk
from tkinter import filedialog

def getBGR(path):
    imgBGR = cv2.imread(path, cv2.IMREAD_COLOR)

    #開いた画像からRGBを抽出する。
    b = imgBGR.T[0].flatten().mean()
    g = imgBGR.T[1].flatten().mean()
    r = imgBGR.T[2].flatten().mean()

    return b, g, r

def main():
    root = tk.Tk()
    root.withdraw()

    #出力するファイルの名前を定義する。タイムスタンプ付き。
    now = datetime.datetime.now()
    fo = 'ImageIntensityList_{:%Y%m%d%H%M%S}.txt'.format(now)
    
    #ファイルダイアログのデフォルトパスをデスクトップにする。
    desktop_path = os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + "\\Desktop"

    #対象ファイルのタイプをTiffに限定する。
    target_format = [('TIF image', '*.tif')]
    
    #ファイルダイアログオブジェクトの作成。複数ファイル選択できる。
    files = filedialog.askopenfilenames(filetypes = target_format, initialdir = desktop_path)
    
    #ファイルダイアログで選択したすべてのファイルを処理する。
    with open(os.path.join(desktop_path, fo), mode='a') as f:
        f.write('File path,B,G,R,Intensity\n')
        for file in files:
            b, g, r = getBGR(file)
            i = 0.298912 * r + 0.586611 * g + 0.114478 * b  #RGBから輝度値を算出
            line = '{0},{1:.2f},{2:.2f},{3:.2f},{4:.2f}\n'.format(file, b, g, r, i)
            f.write(line)

if __name__ == '__main__':
    main()


Pythonのlist.sort()とsorted(list)の違いは新たにリストが作られるかどうか

list.sort()とsorted(list)の使い分けは?
対象となるリストを作り替えてもいい場合は list.sort() を、対象となるリストを残しておきたい場合は sorted(list) を使う

解説

list.sort()

sortメソッドは対象リストに直接変更を加えるソートを行う。これを「インプレイスでソートする」と言う。以下コード内の#1と#2が示すように、ソート前後でオブジェクトIDが変わっていないことがわかる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
print("Object ID:", id(data))   #1
data.sort()
print("Result   :", data)
print("Object ID:", id(data))   #2
Object ID: 86722536   #1
Result   : ['Black', 'Blue', 'Brown', 'Green', 'Orange', 'Red', 'Yellow']
Object ID: 86722536   #2


sorted(list)

sorted関数は対象リストとは別の新しいリストを作り、ソートした要素を入れる。以下コード内の#3と#4が示すように、sort関数から返ってくるとオブジェクトIDが変わっていることからも分かる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
print("Object ID:", id(data))             #3
print("Result   :", sorted(data))
print("Object ID:", id(sorted(data)))   #4
Object ID: 86722504   #3
Result   : ['Black', 'Blue', 'Brown', 'Green', 'Orange', 'Red', 'Yellow']
Object ID: 86721800   #4


Tips ~Pythonのソートは安定している~

『安定』とは、ソート基準が同等のデータはソート前の順番が保持される、ということを意味する。細かいことだが、これを知らないとkeyパラメータに複数条件を入れる…なんて無駄なことをやってしまいかねない。と思ったら、過去に挙げた記事でまんまそれをやってしまっていた。
greenhornprofessional.hatenablog.com

文字数でソートする例

Green, Brown, Blackは5文字なので、同等データとなるがちゃんとソート前の順序が保持されていることがわかる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
data.sort(key=len)
print(data)
['Red', 'Blue', 'Green', 'Brown', 'Black', 'Yellow', 'Orange']

Pythonでシリアル通信!Arduinoに文字列を送って、Arduinoから文字列を受け取る!

2021/01/24 送受信の所要時間に関する記述を追加
シリアル通信でLチカしたプログラムをアップデート。Arduinoに特定の文字列を送ると、それに対応した返答を返すというもの。
RS232Cを備える測定器をリモートコントロールするための練習プログラム。

greenhornprofessional.hatenablog.com

結果

Pyhtonから送りたい文字列を標準入力で入力すると、Arduinoから意図したとおりの返答を受け取れている。
送受信は30~40msで完了している。この数字が妥当なのか?などの深堀はまた今度。
また、送信する文字列の末尾にセミコロンをつけていない場合は時間が増える。これはArduinoSerial.readStringUnitl()セミコロンを待つようにしているためで、これのタイムアウト(恐らく1sec)が影響している。

<= abc;
計測開始
送信完了:0.008sec
=> Input is abc
=> Return is ABC
受信完了:0.037sec

<= abc
計測開始
送信完了:0.010sec
=> Input is abc
=> Return is ABC
受信完了:1.020sec

<= ddd;
計測開始
送信完了:0.009sec
=> Input is ddd
=> N/A
受信完了:0.033sec

プログラム

Arduino
void setup(){
  Serial.begin(115200);
}

void loop(){
  if(Serial.available()>0){
    String input = Serial.readStringUntil(';');
    Serial.print("Input is "); Serial.println(input);
      if(strcmp(input.c_str(), "abc") == 0){
        Serial.println("Return is ABC");
      }else if(strcmp(input.c_str(), "def") == 0){
        Serial.println("Return is DEF");
      }else{
        Serial.println("N/A");
      }
  }
}
Python
#SimpleSerial_003.py

import serial
from time import sleep
import time

def decoder(byte):
    str_array = []
    str_array = byte.decode().rstrip('\r\n').split('\r\n')
    for i in str_array:
        print("=>",i)
    
def main():
    ser = serial.Serial('COM3', 115200, timeout=0.1)
    sleep(2)
    
    command = input()
    print("<=", command)

    t0 = time.time()
    print("計測開始")
    
    ser.write(bytes(command, encoding='ascii'))
    ser.flush()

    t1 = time.time()
    print("送信完了:{:.3f}sec".format(t1 - t0))

    while True:
        if ser.in_waiting > 0:
            data = ser.read_all()
            break
        
    decoder(data)

    t2 = time.time()
    print("受信完了:{:.3f}sec".format(t2 - t1))
    
    ser.close()

if __name__ == '__main__':
    main()

コメント:今回のプログラムではser.flush()は時間に寄与せず。

Pythonでシリアル通信してArduinoをLチカさせる

とある測定器をRS232Cでリモートコントロールしたくて、Pythonのシリアル通信の仕方を学ぶ。まずは簡単なところからということで、Pythonから送られてきた数字(トリガー)をArduino側で識別してそれぞれに対してLチカの挙動を変える、というプログラムを作ってみた。

結果

0 : 消灯、1 : 点灯、2 : 5回点滅、その他 : 10回点滅、という条件分岐がちゃんとできている。
以下のPythonプログラムは"2"の動作のみ。

プログラム

Arduino
void setup(){
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop(){
  void flicker(int, int);
  int input = Serial.read();
  if(input != -1){
  Serial.println(input);
    switch(input){
      case '0':
        digitalWrite(LED_BUILTIN, LOW);
        break;
      case '1':
        digitalWrite(LED_BUILTIN, HIGH);
        break;
      case '2':
        flicker(5, 500);
        break;
      default:
        flicker(10, 100);
     }
  while(Serial.available())Serial.read();
  }
}

void flicker(int n, int intval){
  for(int i=0; i<n; i++){
    digitalWrite(LED_BUILTIN, HIGH);
    delay(intval);
    digitalWrite(LED_BUILTIN, LOW);
    delay(intval);
  }
}
Python
#SimpleSerial_001.py

import serial
from time import sleep 

ser = serial.Serial('COM3', 115200, timeout=0.1)
sleep(2)
ser.write(bytes('2', encoding='ascii'))
ser.close()

Pythonでフリー公開されているDLLを使ってみる

ctypesを使ってDLLの呼び出し方を学ぶ。今回はフリーで公開されているDLLを使ってみる。前回よりも難しかった。
greenhornprofessional.hatenablog.com


使用させてもらったDLLは以下。文字列として書かれた式と変数の値を与えると式通りに計算してくる、というもの。
文字列よりの計算の詳細情報 : Vector ソフトを探す!

結果

式 x+y+zと、変数 x=10, y=2, z=0.1 を与えると、12.1が返ってきた。ちゃんと動いている。
今回はたまたま出来たというのが正直なところ。C/C++を勉強しないとこれよりも複雑なDLLは使えない。
プロトタイプに標準データ型以外の引数が書かれていると絶望的になる…

プログラム

# 31_CallCalcDll_001.py
# python 3.8.1
# coding: utf-8

from ctypes import *

p_ans = POINTER(c_double)
p_siki = POINTER(c_char)

lib = cdll.LoadLibrary('CALC11')
lib.Num_Form_Calc.argtypes = (p_siki, c_double, c_double, c_double, p_ans)
lib.Num_Form_Calc.restype = c_int

siki = b"x+y+z"
x = 10
y = 2
z = 0.1
ans = c_double()
i = c_int()

i = lib.Num_Form_Calc(siki, x, y, z, ans)

print(i)
print(ans)
print(ans.value)
0
c_double(12.1)
12.1

Pythonでダミーファイルを大量生産する

ソフトウェアの動作確認のために大量の画像ファイルが必要になった。中身はどうでもよく、とにかく数が必要。
1つの元画像をコピーして連番をつける、というプログラムをちゃちゃっと作った。

プログラム

# 32_CopyAndRename_001.py
# python 3.8.1
# coding: utf-8

import os
import shutil

num = 10            #Specify how many copies do you want.
_dir =  'C:\Test'   #Work directory
src = 'lenna.jpg'   #Original file name
src_path = os.path.join(_dir, src)              #Make the path like C:\aaa\bbb\ccc.jpg

if os.path.isfile(src_path):                    #Check if the original file exists.
    for i in range(1, num+1):
        copy = str(i).zfill(3) + '.jpg'         #Define sequential name like 001.jpg, 002.jpg, ...
        copy_path = os.path.join(_dir, copy)    #Make a path for copy like C:\aaa\bbb\001.jpg.
        shutil.copyfile(src_path, copy_path)    #Execute copy.
    print("Complete!")
else:
    print("No file found.")

Pythonでdllを使う Windows API - MessageBox

マシンビジョンカメラをコントロールするソフトが作りたい。でもPython向けのAPIが公開されていない。調べてみたらctypesでdllを使うことができるらしい。
とりあえず、WindowsのMessage Boxの呼び出し方を紹介してくれているサイトがあったので、参考にさせてもらった。

結果

以下のメッセージボックスを呼び出すことができた。メッセージボックスを表示させるだけならTkinterよりもお手軽。
f:id:greenhornprofessional:20200719193538p:plain

プログラム

# 30_WinMessageBox_001.py
# python 3.8.1
# coding: utf-8

import ctypes

mbox = ctypes.windll.user32
i = mbox.MessageBoxW(
    0,
    "[テスト] 予期せぬエラーが発生しました",
    "Error Message",
    0x00000002 | 0x00000010)

if i == 3:
    print("中止を選択しました")
elif i == 4:
    print("再試行を選択しました")
else:    #i == 5
    print("無視を選択しました")
/* -----codeの行番号----- */