Embeddable Python」タグアーカイブ

埋め込み用のPython。Pythonのバージョンアップを気にせずに自身のプログラム内部にPython環境を埋め込んで実行可能にする。

Mark Sheet Reader (Basic version)

「マークシートリーダーをつくる(基礎編)」

DelphiでGUIを作成、マークシート画像はPythonにインストールしたOpenCVとNumpyで読み取り&計算処理して、結果をMemoに表示するマークシートリーダーの練習プログラム。

0.準備
1.使用するプログラムとマークシート画像について
2.マークシート画像を読み込む
3.マークシート読み取り処理のアルゴリズム
4.マークシート読み取り処理の実際(Object Pascalのコード)
5.さらに進化
6.著作権表示の記載方法
7.お願いとお断り

ここで紹介している練習用プログラムを、実際の採点業務で使用できるようにした拙作マークシートリーダーです。

0.準備

マークシートリーダー作成にあたって、以下の事前準備が必要です。

・PythonForDelphiのインストール
・Embeddable Pythonのダウンロードと必要なライブラリのインストール
(作業後、このプログラムへの埋め込み用にフォルダ名を「Python39-32」に変えて、このプログラム(マークシートリーダー)のexeがある場所へコピーする)
・アプリケーションの表示画面のリサイズ対応(縦編)

(いずれも、当Blogの記事で過去に紹介)

重要 上の記事の手順で、OpenCVとNumpyをインストールしたEmbeddable Pythonが入ったフォルダを「Python39-32」という名前で、以下のフォルダ内にコピーする。

C:\Users\ xxx \ Project1.dprojファイルのあるフォルダ \Win32\Debug\

1.使用するプログラムとマークシート画像について

当Blogの過去記事『~主として「高さ」の変更に関する覚書~』で作成したDelphiのGUIをそのまま使用します。

必要なVCLとその構造(親子関係)

画面サイズの変更に対応できるよう、以下のコードを記述。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.ExtCtrls, Vcl.Grids, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Splitter1: TSplitter;
    ScrollBox1: TScrollBox;
    Image1: TImage;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Splitter1Moved(Sender: TObject);
  private
    { Private 宣言 }
    //Panel1の幅とFormの高さを記憶する変数
    intPH, intFH:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TForm1 }

procedure TForm1.CMShowingChanged(var Msg: TMessage);
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    //Formの表示終了時に以下を実行
    Panel1.Height:=intPH;
    intPH:=Panel1.Height;
    intFH:=Form1.Height;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  //比率を維持してPanel1の高さを変更
  Panel1.Height:=Trunc(Form1.Height * intPH/intFH);
end;

procedure TForm1.Splitter1Moved(Sender: TObject);
begin
  //Panel1とFormの高さを取得
  intPH:=Panel1.Height;
  intFH:=Form1.Height;
end;

end.

マークシート画像は、以下の画像を使用。

「ms01.Jpg」

マークシート画像は、以下の場所に「MarkSheet」という名前のフォルダを作成して、その中に保存。

C:\Users\ xxx \ Project1.dprojファイルのあるフォルダ \Win32\Debug\Marksheet

2.マークシート画像を読み込む

Delphiを起動して、Project1.dproj(マークシート読み取り用GUIの保存してあるフォルダ内のDelphiのプロジェクトファイル)を開き、Panel3をクリックして選択しておいて、Panel3上にButton1を作成。Button1のNameプロパティはButton1のまま、Captionプロパティを「画像を表示」に変更。Button1の位置は下図を参照。

Captionプロパティを「画像を表示」に変更
Button1の位置は画面下・Panel3の左に寄せる

OpenDialog1をForm上に置く。

OpenDialogをダブルクリック
Form上のOpenDialog1

次に、Form上のButton1をダブルクリックして、procedure TForm1.Button1Click(Sender: TObject);を作成。

procedure TForm1.Button1Click(Sender: TObject);
begin

end;

作成した手続きではJpeg画像を扱うので、画面を上にスクロールして、implementation部の下に Vcl.Imaging.Jpeg を uses する。

implementation

uses
  Vcl.Imaging.Jpeg; //Jpeg画像を読み込む

{$R *.dfm}

Button1Clickプロシージャにvar宣言を追加して、Jpeg画像読み込み用の変数jpgを宣言。

procedure TForm1.Button1Click(Sender: TObject);
var
  jpg: TJPEGImage;
begin

end;

beginとend;の間に、以下のコードを記述。

  //OpenDialogのプロパティはExecuteする前に設定
  With OpenDialog1 do begin
    //表示するファイルの種類を設定
    Filter:='JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'MarkSheet';
  end;

  if not OpenDialog1.Execute then Exit;  //キャンセルに対応
  //オブジェクトを生成
  jpg := TJPEGImage.Create;
  try
    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);
  finally
    //オブジェクトを破棄
    jpg.Free;
  end;

上書き保存(Ctrl+S)して、実行(F9)。データの読み込み先を指定しておくと、目的のフォルダが一発で開くので便利。

マークシート画像が表示される。が、ごく一部しか見えない。

これはImage1のAutoSizeプロパティがデフォルトFalseに設定されているため。 Image1 のAutoSizeプロパティをTrueにするコードを追加(オブジェクトインスペクタで Image1 のAutoSizeプロパティを 直接指定してもOK)。

  try

    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);

    //追加
    Image1.AutoSize:=True;

  finally

上書き保存(Ctrl+S)して、実行(F9) 。画像の表示を確認する。

うまくいったように見える。Formを最大化してSplitterを下げて、さらに確認。
画像の表示位置を修正する必要がありそうだ

画像が表示される位置を、画面の左側へ移動するコードを手続きの先頭に追加する。

begin

  //Imageの表示位置を指定
  Image1.Top := 25;
  Image1.Left := 40;

  //OpenDialogのプロパティはExecuteする前に設定しておくこと
  With OpenDialog1 do begin

上書き保存(Ctrl+S)して、実行(F9) 。画像の表示を再度確認する。

ほぼイメージに近い出来栄え?

参考:画像読み込みのコード(全体)

implementation

uses
  Vcl.Imaging.Jpeg; //Jpeg画像を読み込む

{$R *.dfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  jpg: TJPEGImage;
begin

  //Imageの表示位置を指定
  Image1.Top := 25;
  Image1.Left := 40;

  //OpenDialogのプロパティはExecuteする前に設定しておく
  With OpenDialog1 do begin
    //表示するファイルの種類を設定
    Filter:='JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'MarkSheet';
  end;

  if not OpenDialog1.Execute then Exit;  //キャンセルに対応
  //オブジェクトを生成
  jpg := TJPEGImage.Create;
  try

    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);

    //追加
    Image1.AutoSize:=True;

  finally
    //オブジェクトを破棄
    jpg.Free;
  end;

end;

3.マークシート読み取り処理のアルゴリズム

まず最初にマークシートの左上にある特徴点(マーカー)画像: ■■■(トリプルドット)をOpenCVのテンプレートマッチングで探す。

特徴点(マーカー)画像が見つかったら、 特徴点(マーカー)画像左上位置を基準にして、「マークシートの周囲の枠部分のみ」を矩形選択して切り出し。

参考①:あらかじめ測定しておいた特徴点(マーカー)画像の位置(単位はピクセル)
左上のX座標=65
左上のY座標=28
右下のX座標=121(マークシート矩形の座標計算には使用しない)
右下のY座標=43(マークシート矩形の座標計算には使用しない)

参考②:あらかじめ測定しておいたマークシート矩形の座標 (単位はピクセル)
左上の X座標=65
左上の Y座標=61
右下の X 座標=419
右下の Y 座標=497

参考 上記の各座標をマークシート画像から計測し、テンプレートとして用意したマークシートごとに登録(座標値を保存)するプログラムを別途作成した。なお、座標原点(0,0)は画像の左上である(使い慣れた数学の座標系とちょっと違うことに注意!)。

赤が左上、青が右下の座標で、緑がマークシート枠の矩形

この座標を元にして、 特徴点(マーカー)画像からの距離で、マークシート矩形を切り出す。

マークシート矩形において、(W1、H1)が左上位置を、(W2、H2)が右下位置を示す座標となる。

上の例では、マークシートの列数は「1」、行数は「10」と数えることにする。列数が「1」の場合、W1は「ほぼ0(ゼロ)」になり、値としての意味がないように思われるが、このプログラムを実用化した場合は、下の例のように、複数の列があるマークシートを用いることになるので、2列めのマークシート矩形の座標は、左上が(W3,H3)、右下が(W4,H4)、3列めのマークシート矩形の座標は左上が (W5,H5)、右下が(W6,H6)のように指定でき、W値が0ではない場合が生じる。

マークシート用紙の作成に、私はWordを用いたが、Wordのバージョンによっては、あろうことか、上書き保存時に、マーカー画像(■■■)の位置が数ミリ程度、勝手に左へ移動するという予期しないトラブル(Wordの仕様?)が発生。このような点も考慮して、W1の座標は敢えて(0として)定数化していない。

マークシートの作成例(実験用に使用)
列数3、1列あたりの行数25、1行あたりの選択肢の数は16
この用紙の場合、総マーク数は3×25×16=1200個/枚となる
つまり用紙1枚につき、1200回マークの有無の判定が必要

実際の作業では、マークシート画像をスキャナーで読み取って、グレースケールのJpeg画像としてデータ化するので、マークシート(用紙)に「しわ」があったり、状況によっては「折られ」ていたりする関係上、読み取り画像を1枚ずつ比較すると、その上下・左右にどうしても微妙なブレ・ズレが生じてしまう。しかし、同じ印刷機で、同時に印刷したマークシートであれば、特徴点(マーカー)画像とマークシートの行列位置の関係は絶対であり、これが1枚ごとに変化することはありえない。つまり、スキャンした画像が余程大きく傾きでもしていない限り、テンプレートマッチングで、特徴点(マーカー)画像さえ発見できれば、予め測定・記録しておいた座標の相対的位置関係からマークシート矩形は容易に切り出せる。

次の画像は、別データとして保存してある特徴点(マーカー)画像を元に、OpenCVのテンプレートマッチングをマークシート画像に対して行ったもの。類似度の高い部分を赤枠で囲んで示すようプログラミングしている。

マーカー
テンプレートマッチングを行った画像

次に、上に述べた方法で計算したマークシート矩形を列単位で切り出す。切り出した画像は、マークの(=列)数・行数の整数倍のサイズになるようリサイズする(これは、このあと画像を細かく分割して処理するので、切り出す行や列の計算を簡単にするための工夫 → 整数倍にリサイズすれば、列数分&行数分廻すLoop処理の中で処理しやすい)。

列単位で切り出したマークシート矩形

マークシート用紙は、一般的なマークシート用紙のような厚みのある(高級感あふれる)専用紙でなく、ホームセンターでも「売ってない!」ような見た目が灰色の再生紙を用いている。このためか、あちらこちらにゴミのような黒い点や、細いすじが入っていることがある。これらの黒点やすじを判定プログラムが「マークあり」と誤認しないようにするため、次に「平滑化(ボカシ)処理」を行う。

平滑化(ボカシ)処理には「ガウシアンフィルタ」を用いた。これは、正規(ガウス)分布を利用して「注目画素からの距離に応じて近傍の画素値に重みをかける」という処理を行うもので、自然な平滑化が実現できるとのこと。次の画像は、上の切り出したマークシート矩形に対して、この平滑化処理を行ったもの。

img = cv2.GaussianBlur(img,(35,35),0) ※引数は奇数を指定する必要がある

引数の値が大きいほど正規分布のピークが低く、広がりは広くなる(=より均一に、より全体にボカシがかかる)。ここでは引数をかなり大きめにとり「35」としている。こうすることで、ゴミやシミを画像からほぼ完全に除去できる。

ガウシアンフィルタ処理を行い、ゴミやシミを除去する

さらに、この画像を「ある閾値」を元に白と黒に二値化処理する。この処理で枠線やマークされていないマーク部分が「すべて白」になり、鉛筆で濃くマークされている部分だけが「黒」になった白黒画像が得られる。当初は、以下のように引数を指定して二値化画像を作成した。

ret, img = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY)

現在は、次のように閾値の設定を自動で行う「大津の二値化」を利用している。

ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

式中の第2引数は閾値だが、大津の二値化では自動計算させるので0(ゼロ)を指定。第3引数は0-255の256段階でグレースケール化しているから、最大値の255を指定する。これによって、次の画像が得られる。

大津の二値化で作成した白黒画像

さらに、これを白黒反転させた画像を作成する。式は以下の通り。

img = 255 - img

これにより、次の画像が得られる。

マーク部分を「白」に変換した画像

次に、この画像を「行」単位に分割して切り出す。

1行目を切り出した画像

次に、選択肢の数で、均等に分割する。ここでは選択肢の数が「8」なので、上の画像を等幅で8個に分割する。下は、その1個目の切り出し画像である。

このように細かく分割して切り出した画像1つ1つについて、画素が白なら値を255・黒なら0として面積あたりの合計値を計算し、マークされている部分の面積の中央値を算出、これを閾値として、下の式では、マークされている(白い部分の)面積が他より3倍以上あるものを「マークあり!」と判定している。この数値が大きいほど、判定はきびしくなる。

result.append(area_sum > np.median(area_sum) * 3)

このマークシート読み取り処理のアルゴリズムの主要部分は全て、GitHubの次の記事に紹介されていたものです。素晴らしい記事を投稿してくださった作成者の方に、心から感謝申し上げます。

PythonとOpenCVで簡易OMR(マークシートリーダ)を作る

URL:https://qiita.com/sbtseiji/items/6438ec2bf970d63817b8

参考 列が複数あるマークシートの読み取り処理について

上記記事では、特徴点(マーカー)画像をマークシートの上下に複数個用意し、テンプレートマッチングを行っています。確かに、マークシートの左上と右下に特徴点(マーカー)画像を用意すれば、より簡単にマークシート矩形の切り出しが可能でした。これは素晴らしいアイデアです。

私も当初は特徴点(マーカー)画像を複数個用意してマークシートを作成していたのですが、列数を2列、3列と増やすと、さまざまな問題が生じることに気が付きました。

第一に、特徴点(マーカー)画像を変えないと、列ごとの切り出しが困難だということです。つまり、3列あるマークシートでは、最も左の列用の特徴点を■■■、真ん中の列用の特徴点を■□■、最も右側の列用の特徴点を■□□として、Loop処理の中でテンプレートマッチングに使用する特徴点(マーカー)画像を切り替えて、目的とするマークシート矩形を切り出せるようにしてみた(□□■や□□□も含めればさらに多くの列が作成可能)のですが、この方法では、うまく特徴点(マーカー)画像を認識してくれないことがあり、安定感に欠ける気がしました。

第二に、万一、回答者が特徴点(マーカー)画像に意図的に変更を加える(例: ■□□ → ■■□)等の暴挙に出た場合、対応が難しいこと。

第三に、マーカー画像が多いと、マークシートの見た目もなんだか騒がしくて、個人的にはマーカー画像を複数個用意する方法はなるべく避けたいと考えたこと。

これらの理由から、「なんとか特徴点(マーカー)画像が1個で済まないか」と、私なりに工夫して、当ブログで紹介した方法を考えました。

創意工夫の過程で一時は、回答者が意図的に変更できるようなマーカー(例: □ )がなければOKかとも思い、別の特徴点(マーカー)画像も使ってみたのですが、それはそれでまた別の問題を起こすことがわかりました。

例えば、下のように、ヒトなら簡単に両者の違いを判別できる画像を用意します。

用意した特徴点(マーカー)画像

これに対して、左側の画像でテンプレートマッチングを行うと・・・

機械はヒトと違うモノの見方をしていることが、大変良くわかりました。

4.マークシート読み取り処理の実際(Object Pascalのコード)

Form上に、Buttonを1つ、PythonForDelphi関連のVCLコンポーネントを3つ配置する。Button2は、Panel3の中央付近に置き、Nameプロパティはそのまま、Captionプロパティを「読み取り」に変更する。PythonForDelphi関連のVCLコンポーネントは、すべて非ビジュアルコンポーネントなので、位置はどこでもよく、Nameプロパティもデフォルトのままとする。 PythonForDelphi関連で配置するコンポーネントは以下の通り。

以下のように、PythonForDelphi関連のコンポーネントのプロパティとイベントを設定

・PythonEngine1のAutoLoadプロパティはFalseに設定。

・PythonEngine1のDllNameプロパティはpython39.dllを指定(埋め込みPythonのバージョンに合わせて設定する)。ここでは3.9.9以下のバージョンのPythonでないとNumpyが非対応(2021年12月現在)であり、用意した埋め込みPythonのバージョンは3.9.9なのでpython39.dllに変更する。

・PythonEngine1のIOにはPythonGUIInputOutput1を指定。

・PythonGUIInputOutput1は他で利用するならプロパティのOutPutに「Memo1」などとするところだけれど、ここでは何も設定しない。

・PythonDelphiVar1のVarNameはプログラムコードの記述に合わせて「var1」とする。var1と入力後、Enterで確定すること!(青く反転表示されるのを確認する)

Formが生成される時、PythonEngine1を初期化する。Formのタイトルバーの上をクリックして選択し、オブジェクトインスペクタのイベントタブをクリックしてOnCreateイベントの右に表示されている「FormCreate」をダブルクリックして、コードの入力に切り替える。

参考:エラー対応方法(20220724追加)

P4D使用時にImageコントロールの bsClear を使うとエラーが発生します。

[dcc32 エラー] Unit02_MSReader.pas(1199): E2010 'TBrushStyle' と 'Enumeration' には互換性がありません

これはPythonEngine.pasの中で bsClear が定義(使用)されているためです。次に示す例のように、Image1の方のbsClearを明示的に Vcl.Graphics.bsClear として対応します。

  //矩形を描画
  with Image1 do
  begin
    //Canvas.Brush.Style:=bsClear;
    Canvas.Brush.Style:=Vcl.Graphics.bsClear;
  end;

以上、エラー対応でした。解説を続けます。

表示は次のようになっている(はず)。ここにコードを追加する。

procedure TForm1.FormCreate(Sender: TObject);
begin

  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;

end;

追加するコード

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath(追加)
  AppDataDir:string;
begin

  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;

  //以下のコードを追加
  //embPythonの存在の有無を調査
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';

  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理
    MessageDlg('Embeddable Pythonが利用可能です。',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=True;
    PythonEngine1.IO:=PythonGUIInputOutput1;
    PythonEngine1.DllPath:=AppDataDir;
    PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
    PythonEngine1.LoadDll;
    //PythonDelphiVar1のOnSeDataイベントを利用する
    PythonDelphiVar1.Engine:=PythonEngine1;
    PythonDelphiVar1.VarName:=AnsiString('var1');  //プロパティで直接指定済み
    //初期化
    PythonEngine1.Py_Initialize;
  end else begin
    MessageDlg('Embeddable Pythonが見つかりません!',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=False;
  end;

end;

ここでMessageDlgを使用しているので、以下のように System.UITypes を uses に追加する。

implementation

uses
  Vcl.Imaging.Jpeg, System.UITypes;  // <-追加

  //Jpeg:Jpeg画像を読み込む
  //System.UITypesはMessageDlgの表示に必要

{$R *.dfm}

プライベートメンバー変数 intCnt(カウンタとして利用する)と strAnsList(Pythonから返された計算結果を保存する) を2つ、Private宣言で新しく宣言する。

  private
    { Private 宣言 }

    //for Python(追加)
    //Counter
    intCnt:integer;
    //Pythonから送られたデータを保存
    strAnsList:TStringList;

    //Panel1の幅とFormの高さを記憶する変数
    intPH, intFH:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;

  public
    { Public 宣言 }
  end;

Form上のButton2(読み取りボタン)をダブルクリックして、手続きを作成し、以下の内容を入力する。

procedure TForm1.Button2Click(Sender: TObject);
var
  StrList:TStringList;
  strJCnt,strColCnt,strRowCnt,strSelCnt:String;
  TopLX, TopLY, TLX1, TLY1, BRX1, BRY1:integer;
  strPicName:string;
begin

  //初期化
  Memo1.Clear;
  intCnt:=1;

  //座標
  TopLX:=65;
  TopLY:=28;
  //BtmRX:=121;
  //BtmRY:=43;
  TLX1:=65;
  TLY1:=61;
  BRX1:=419;
  BRY1:=497;

  //マークシート数Check(+1することを忘れない)
  strJCnt:=IntToStr(2);

  //列数Check(+1することを忘れない)
  strColCnt:=IntToStr(2);

  //1列あたりの行数Check
  strRowCnt:=IntToStr(10);

  //選択肢数Check
  strSelCnt:=IntToStr(8);

  //マークシート名
  strPicName:='ms';

  //結果を保存するStringList
  strAnsList := TStringList.Create;

  //Scriptを入れるStringList
  StrList := TStringList.Create;

  try

    //Python Script
    StrList.Add('import cv2');
    StrList.Add('import numpy as np');

    //for JPN(日本語に対応)
    StrList.Add('def imread(filename, flags=cv2.IMREAD_GRAYSCALE, dtype=np.uint8):');
    StrList.Add('    try:');
    StrList.Add('        n = np.fromfile(filename, dtype)');
    StrList.Add('        img = cv2.imdecode(n, flags)');
    StrList.Add('        return img');
    StrList.Add('    except Exception as e:');
    StrList.Add('        return None');

    //マーカー画像を読み込む
    StrList.Add('template = imread("marker.png", cv2.IMREAD_GRAYSCALE)');

    //マークシートの枚数
    StrList.Add('for j in range(1,'+strJCnt+'):');

    //列数
    StrList.Add('    for i in range(1,'+strColCnt+'):');

    //マークシートへのパスを取得
    StrList.Add('        if j < 10:');
    StrList.Add('            MS_Name = r".\Marksheet\'+ strPicName +'0"+ str(j) +".jpg"');
    StrList.Add('        else:');
    StrList.Add('            MS_Name = r".\Marksheet\'+ strPicName +'"+ str(j) +".jpg"');

    //画像を読み込む
    StrList.Add('        img = imread(MS_Name)');
    //画像をグレースケールで読み込む
    StrList.Add('        img_gray = imread(MS_Name, 0)');

    //テンプレートマッチングの実行(比較方法cv2.TM_CCORR_NORMED)
    StrList.Add('        result = cv2.matchTemplate(img, template, cv2.TM_CCORR_NORMED)');

    //類似度が最小,最大となる画素の類似度、位置を調べ代入する
    StrList.Add('        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)');
    //最も似ている領域の左上の座標を取得
    StrList.Add('        top_left = max_loc');
    StrList.Add('        if i == 1:');

    //補正値を取得(高さ)
    StrList.Add('            h1 = ' + IntToStr(TLY1 - TopLY));
    StrList.Add('            h2 = ' + IntToStr(BRY1 - TopLY));
    //補正値を取得(幅)
    StrList.Add('            w1 = ' + IntToStr(TLX1 - TopLX));
    StrList.Add('            w2 = ' + IntToStr(BRX1 - TopLX));

    //矩形の左上の座標を計算 [0]-> X, [1]-> Y
    StrList.Add('        TL = (top_left[0] + w1, top_left[1] + h1)');
    //矩形の右下の座標を計算
    StrList.Add('        BR = (top_left[0] + w2, top_left[1] + h2)');
    //画像を切り出し img[top_Y : bottom_Y, left_X : right_X]
    StrList.Add('        img = img_gray[TL[1] : BR[1], TL[0] : BR[0]]');

    //選択肢数
    StrList.Add('        n_col = '+ strSelCnt);

    //解答欄1列あたりの行数
    StrList.Add('        n_row = '+ strRowCnt);
    StrList.Add('        margin_top = 0');
    StrList.Add('        margin_bottom = 0');
    StrList.Add('        n_row = n_row + margin_top + margin_bottom');

    //マークの列数・行数の整数倍のサイズになるようリサイズ
    StrList.Add('        img = cv2.resize(img, (n_col*100, n_row*100))');

    //保存して確認
    //StrList.Add('        cv2.imwrite("01_ReSize.png", img)');

    //平滑化の度合い
    StrList.Add('        img = cv2.GaussianBlur(img,(35,35),0)');

    //保存して確認
    //StrList.Add('        cv2.imwrite("02_GaussianBlur.png", img)');

    //二値化の閾値
    //50を閾値として2値化
    //imgはグレースケール画像でなければならない
    //第2引数はしきい値で,
    //画素値を識別するために使用(指定)
    //第3引数は最大値でしきい値以上
    //(指定するフラグ次第では以下)の値を持つ
    //画素に対して割り当てられる値
    //StrList.Add('        ret, img = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY)');

    //大津の二値化で閾値の設定を自動化
    //第1引数には画像データを設定
    //(グレースケール画像でなければならない)
    //第2引数はしきいだが自動計算させるので0(ゼロ)を指定
    //第3引数は0-255の256段階でグレースケール化しているから
    //最大値の255を指定
    StrList.Add('        ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)');

    //保存して確認
    //StrList.Add('        cv2.imwrite("03_threshold.png", img)');

    //白黒を反転
    StrList.Add('        img = 255 - img');

    //保存して確認(追加)
    StrList.Add('        cv2.imwrite("04_threshold.png", img)');

    //全マークを判定
    StrList.Add('        result = []');
    StrList.Add('        for row in range(margin_top, n_row - margin_bottom):');
    StrList.Add('            tmp_img = img [row*100:(row+1)*100,]');
    StrList.Add('            area_sum = []');
    StrList.Add('            for col in range(n_col):');
    StrList.Add('                area_sum.append(np.sum(tmp_img[:,col*100:(col+1)*100]))');
    StrList.Add('            result.append(area_sum > np.median(area_sum) * 3)');

    //判定結果を出力
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = "99"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = "999"');

    //Execute
    PythonEngine1.ExecStrings(StrList);

    //結果を表示
    Memo1.Lines.Assign(strAnsList);

    //Userへ案内
    MessageDlg('読み取り完了!', mtInformation, [mbOk] , 0);

  finally
    //解放
    StrList.Free;
    strAnsList.Free;
  end;

end;

Pythonから返された計算結果を受け取るため、PythonDelphiVar1のOnSetDataイベントの手続きを作成する。Form上のPythonDelphiVar1をクリックして選択し、オブジェクトインスペクタのOnSetDataイベントの右側をダブルクリックして、コード入力画面で以下の内容を入力する。

procedure TForm1.PythonDelphiVar1SetData(Sender: TObject; Data: Variant);
begin
  //値がセットされたら動的配列に値を追加
  strAnsList.Add(Data);
  intCnt:=intCnt+1;
  Application.ProcessMessages;
end;
表示の「999」は空欄、「99」は複数マークであることを意味する。

上書き保存(Ctrl+S)して、実行(F9)。次の画像のように、マークシートが正しく読み取り処理されることを確認する。

複数マークを許可する場合には、判定結果を出力する部分のコードを次のように変更する。マークシートの読み取り結果をCSVファイルに出力したり、Excelに書き出したりして利用する場合には、複数回答は99、未回答は999のように処理した方が、後々の処理がラクになる(・・・と思う)。

    //判定結果を出力(複数回答は99、未回答は999で表示)
    {コメント化ここから
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = "99"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = "999"');
    ここまで}

    //判定結果を出力(複数回答の詳細を表示)
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = str(res)+ '+'"!複数回答!"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = " *未回答*"');

PythonEngineが正しく初期化され、Embeddable Pythonが利用できることが確認できたら、このメッセージは必要ないのでコメント化しておく。

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath
  AppDataDir:string;
begin
  ・・・
  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理(コメント化)
    //MessageDlg('Embeddable Pythonが利用可能です。',
    //  mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=True;

5.さらに進化

さまざまな機能を追加したマークシートリーダー
(ファイルの名称を連番で変更/画像の回転/グリッド指示位置と画像の連動/グリッド指示位置を画像上で矩形選択/閾値等各種パラメータの調整と保存機能/音声読み上げ関連機能の搭載/回答チェック機能(空欄&複数回答対応)/CSV形式でのデータ出力/ExcelBookへのデータ出力/様式の異なるマークシートをテンプレートとして登録して利用可能/抱き合わせ採点の実施機能/共通テスト(数学の様式)に対応等、考えつく限りの機能を搭載/さらに進化します!)

このプログラムでは、「マークシート画像の表示」と、「読み取り処理」の間に何も関連がないが、このプログラムをさらに発展させて、複数枚数の処理を可能にし、読み取り結果を画面上で確認するような機能を追加する際には、マークシート画像の表示はどうしても必要な機能になる。

さらに、画面の左側などに読み込んだマークシートがリスト形式で表示されるようにして、ここから任意のマークシート画像を選んで表示できるような機能も追加するとよいと思う。

読み取り結果も、ここではMemoに表示しているが、CSVやExcelへ出力して利用することを考えると、ここはGridコントロールに変更したい。

Gridコントロール上で選択したデータの該当回答欄に相当する画像が自動的に画面上に表示され、かつ、表示されたマークシート画像上の該当回答欄が矩形で選択され、ユーザーがチェックしやすいGUIにするとなお良いだろう。

また、チェック時にはユーザーがマークシート画像を見ながら確認作業が行えるよう、Gridコントロールの数字をアナウンスしてくれる音声読み上げ機能があると大変便利だ。それから、回答の必要がない、全マークシートが空欄となっている部分は、予め指定することで、チェックから除外できる機能も欲しい。

さらに、スキャナーから読み込んだ画像データを回転させたり、連番で扱いやすい名前に変更したり、様式の異なるマークシートをテンプレートとして登録できるような機能も搭載したい。

より一層ユーザーに優しい、夢に見たようなマークシートリーダーを開発したい。この希望の実現に向けて、日々努力する私でありたい。

Web上に貴重な資料を公開してくださった多くの皆さまに心より深く御礼申し上げます。ほんとうにありがとうございました。

6.著作権表示の記載方法

参考:Python4DelphiのLicenseについて

GitHubのPython4Delphiのダウンロードページには「The project is licensed under the MIT License.」とある。これは「改変・再配布・商用利用・有料販売すべてが自由かつ無料」であること、及び使用するにあたっての必須条件はPython4Delphiの「著作権を表示すること」と「MITライセンスの全文」or 「 MITライセンス全文へのLink」をソフトウェアに記載する、もしくは、別ファイルとして同梱しなさい・・・ということを意味する。

したがってPython4Delphiを利用したプログラムの配布にあたっては、ソフトウェアの中で、次のような著作権表示を行うか、もしくは P4DフォルダのルートにあるLicenseフォルダをプログラムに同梱して配布すればよいことになる。

Python4Delphiを利用した場合の著作権表示の記載例:

Copyright (c) 2018 Dietmar Budelsky, Morgan Martinet, Kiriakos Vlahos
Released under the MIT license
https://opensource.org/licenses/mit-license.php

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Download Embeddable Python and Install the library

「埋め込み用Pythonのダウンロードとライブラリのインストール方法」

1.始めに
2.Embeddable Python をダウンロード
3.必要なライブラリをインストールする準備
4.Numpyのインストール
5.OpenCVのインストール
6.単体で動作確認(検証)
7.まとめ
8.お願いとお断り

1.始めに

なぜ、Embeddable(埋め込み用)なのかというと、内部的なデータ処理にPythonのOpenCV & Numpyライブラリを使うと、アプリケーションをより一層高速化できることがわかったから。
それから、Python環境のアップデートとは関係なく、安定動作する実行環境を、PCの操作にあまり詳しくないユーザーに提供できるから。

重要

このような特殊な目的ではなく、学習用にPythonを導入したい場合は、埋め込み用途に配布されている Embeddable Python はお勧めできません! 普通にインストーラを使用して、普通のPython環境をPCにセットアップしてください。

もし、PC環境を変更せずに、(持ち運びも可能な)Pythonが実行できる環境を作りたい場合は、WinPythonが便利! WinPythonならUSBメモリやSDカードにセットアップして、PC環境に変更を加えずに利用可能。なお、この場合は・・・

スタートボタン → 設定 → アプリ → アプリと機能 → その他の設定 → アプリ実行エイリアス → アプリ インストーラー(項目のいちばん下)のPythonとPython3をオフ

・・・にしてから、外部メディアにセットアップしたWinPythonを実行。

WinPythonのDL先URL:https://winpython.github.io/

WinPythonを外部メディアに入れて利用する場合

2.Embeddable Python をダウンロード

Embeddable Python は https://www.python.org/downloads/windows/ からダウンロード可能。

上記のサイトに行くと、古い2.X.Xから最新版の3.11.0(テスト用)まで、これまでにリリースされた Embeddable Python すべてがある。どれを選んでよいか、困ってしまう(実際、困った)。だから、使用目的(& 条件)に合わせてダウンロードする Embeddable Python を選択しなければならない。

私の場合、まず Stable Release (安定動作版:様々な動作検証がそれなりに行われたバージョンってこと?)であること。さらに、数値演算用のNumpyライブラリと、コンピュータの眼として利用する画像処理用のOpenCVがインストールできること。最低限、この3つを満たしていればOK!だ。

それから、32 or 64bitバージョンのどちらを選択するか、ちょっと迷ったが、よく考えたら(私が)、Delphi11で設定しているVCLのターゲットプラットフォームは32bitアプリケーション。だから32bitバージョンを選択すべきだと気付く。

ターゲットプラットフォームの設定はWindows32ビット(私の場合)

あとは・・・新しいのか、ちょっと前のか、すごく古いのか、どれを選べばいいんだろー??? 2.X.Xはもう既にサポートがないから、3.X.X なのは絶対だけど。。。3.6.X? 3.7.X? 3.8.X? それとも3.9.X? 最新版は3.10.1があるけどー。

うー。うーー。うーーー。(悩む私)

※ 実はマイナーバージョンごとの違いすらまったくわかってない。

たぶん(根拠無し)、最新版でいいだろー☆(←完全な思い込み)

単純極まりない私は、Stable Release のいちばん上にある

「Win7より前のOSには使えません」・・・って注意書きしかないし、この時点での私はNumpyが3.10.1に非対応(2021年12月現在)だということを、誰も教えてくれないから当然知らない(調べろ!)し、なにより、普通の人(?)は、最新版が取り敢えず良さそうに思えちゃうものじゃないですか。

3.10.1のダウンロード&解凍作業完了! 続いてライブラリのインストール。

コマンドプロンプトを開いて・・・。解凍先フォルダへ行って・・・。ラッタッタッタ。

python -m pip install numpy で、ポチ!

ERROR: Could not build wheels for numpy, which is required to install pyproject.toml-based projects

・・・と、表示され、あっけなく阻止される。なんでー

エラーメッセージの内容をよく読んでみると・・・

setup.py:63: RuntimeWarning: NumPy 1.21.5 may not yet support Python 3.10.

確かに。たいへんよくわかりました。はい。

インストールするライブラリが、どのPythonのマイナーバージョンに対応しているか? なんて、対応状況をあらかじめ調査するなんてこと、まずやるわけない私のようなド素人が(無茶を承知で) Python3.10.1 にNumpyライブラリを強制インストールする凶行に及んでも、ちゃんと阻止してくれるんですね。

できればこういう大事なことは、N○Kの朝と晩の7時の全国放送で毎日しつこくアナウンスするとか、誰もがTopページにしているであろう某サイトのいちばん見やすい場所に広告として日々表示してほしい☆・・・と夜空の星に願いつつ、

「使いたいライブラリがどのバージョンに対応しているか、ダウンロード前にきちんと調べる」という貴重な教訓を得て、ここで初めて検索キーワード「numpy python 対応バージョン」でGoogle先生にお伺いをたてると、以下の情報がヒット!

Python向け科学計算パッケージNumPyの開発チームは、最新版となる「NumPy 1.20.0」を1月30日(現地時間)にリリースした。
「NumPy 1.20.0」はこれまでで最大となるアップデートで、Python 3.7~3.9をサポートし、Python 3.6のサポートは終了している。

1月30日とあるのは2021年のこと。この記事は https://codezine.jp/article/detail/13574 より引用

わかった☆OK これでバージョン3.10.1は除外。とりあえず3.9.Xのどれかにしよう。

もうひとつ、どうしても入れたいのがコンピュータの眼「OpenCV」ライブラリ。そこで、PythonとNumpyとOpenCVの関係について調べてみると・・・

opencv-python 4.5.1.48が最新です。
pythonのバージョンは3.6以上とされていますが、numpyについては特に指定はありません。
pipのバージョンは19.3以上

teratailのPythonに関する質問(https://teratail.com/questions/323063)より引用

わかった☆OK これを近所の3歳児でもわかるように言い換えてみよう。

OpenCVとNumpyは仲がイイ。

ダウンロードするPythonのバージョンは、この情報をもとに 3.9.X の中でいちばん新しい 3.9.9 に決定。

理由は次の通り。

Pythonのバージョンを意味する番号は前から順に、メジャー.マイナー.マイクロのそれぞれを意味するそうで、Pythonのメジャーバージョンは2or3。サポート状況から、これは当然「3」を選択。マイナーバージョンは、これもやはりサポート期限を考えるといちばん長いのは3.9.Xで「2025年10月」までだから、これを根拠に「3.9.X」に決定。で、さらにマイクロバージョンは「バグ修正リリース」に相当し、マイクロバージョン間については、互換性が保証されるとのこと。ならば最もバグが消えているのは「3.9.9」なのかなー。みたいな・・・

Pythonのバージョンによる違いについては、次のサイトの解説が詳しい。

Pythonの複数バージョンの扱い方(Windowsの場合)

URL:https://gammasoft.jp/python/python-version-management/

あらためて気合を入れなおし Embeddable Python3.9.9 のダウンロードを持てる全力を挙げて決行!

(正直 ポチ!するだけだけど)

控えめに言えば、Python3.9.9-32bitのEmbeddable Packageを選択してダウンロード。

3.必要なライブラリをインストールする準備

ダウンロードした Package を任意のフォルダに解凍し、ライブラリのインストールに pip が使えるよう、設定を変更( pythonNN._pthファイルを修正 )する。

デスクトップに新しいフォルダーを作成して、そこにDLしたPackageを保存(Zipファイルの大きさはたったの7.3MB!)。

これを解凍すると、

python-3.9.9-embed-win32ができる(大きさは14.0MBとかなり小さい)

python-3.9.9-embed-win32 フォルダを開き、pythonNN._pthファイルを見つけて修正を加える(NNはPythonのバージョンを示す数字)。その方法は下記の通り。

→ バージョン3.9.9をダウンロードしたから、修正するファイルは python39._pth。見つけたらテキストエディタで開いて、いちばん下の行・・・

このナンバーを削除する→ # import site

を、

import site

と コメント解除 する。(※ 正確には、削除するのは#とその後ろの半角スペース)

【補足】
3.9.10では「#import site」となっており、ナンバー#の後ろには「半角スペースがありません」でした!(20220822追記)

コメント解除したら、上書き保存(Ctrl+S)する。

※ 以前、こんな場面で「上書き保存」ではなく「名前を付けて保存」し、あろうことか、ファイル名が「例:XXXXX._pth.txt」になってしまったコトが・・・

次に、ライブラリのインストールに必要な pip を実行するためのScriptファイル get-pip.py を入手する。get-pip.py は次のリンクからダウンロードできる。ちなみにダウンロードした get-pip.py をテキストエディタで開いたら、内容が知らない言語(もしかして、コレが宇宙語?)で書かれており、驚愕。びっくり。もうあけない。

get-pip.py の入手先はこちら(https://bootstrap.pypa.io/get-pip.py

で、ダウンロードした get-pip.py を python-3.9.9-embed-win32 フォルダへコピー。これで get-pip.py が使えるので、次に説明する方法で、まずpipをインストール。

ここからはコマンドプロンプトで作業する(PowerShellでは、モジュールエラーとなり、実行出来ないようだ:情報のみ、未検証です)。

スタートボタンを右クリック→ファイル名を指定して実行→「cmd」と入力して「OK」をクリック→コマンドプロンプトが起動→「cd」+半角スペースを入力→エクスプローラーから「 python-3.9.9-embed-win32 フォルダ」をドラッグ&ドロップしてEnterキーを押す。

で、画面に表示されている > の後ろに「python get-pip.py」と入力してEnterキーを押す(下図赤のアンダーライン部分)。正しく操作が行われていれば、下の画面のようにpipのダウンロードとインストールが自動的に行われる。

pipをインストール(この時点でフォルダ全体の大きさは29.7MB)

Consider adding this directory to PATH(このディレクトリをPATHに追加することを検討してください)と警告されるが、これは気にしない。Embeddable Python を使う目的そのものが、PATHなんかどこにも通さずに

「好き勝手にPythonを使う」

ことだから。

参考:もし、ここで「’python’ は、内部コマンドまたは外部コマンド、操作可能なプログラムまたはバッチ ファイルとして認識されていません。」というエラーが出る場合は、コマンドプロンプトの現在位置(カレントディレクトリ)をよく確認すること。Python.exeがある(見える)フォルダじゃないと、>python ~ コマンドは使えない。

pipがきちんとインストールされたことを、ここで確認しておく。

python -m pip list と入力してEnter

問題がなければ、インストールされたpip他のバージョンが表示される。
「python -m pip list」で「python.exe: No module named pip」が返る場合は、 pythonNN._pthファイルの修正(# import siteの前にある記号#(ナンバー)とその後ろの半角スペースを削除して import site だけにするコメント化の解除手続き)が正しく行われていない可能性が高い。
また、複数のライブラリのインストールを行うと、 pythonNN._pthファイル が修正前の状態に戻されてしまうこともあるようだ。要確認。

4.Numpyのインストール

続いて「愛しのNumpy」をインストール。

>python -m pip install numpy と入力してEnter!

「生きていてよかった」と思える至福の一瞬がここに。

警告:Consider adding this directory to PATH (このディレクトリをPATHに追加することを検討してください) は、まったく気にしない。Numpyが入ればいいのだ。わはは*(^_^)*♪

5.OpenCVのインストール

さらに、視力0.01かつ老眼&緑内障の恐れありと診断(2万ン千円も払ったのにイタいことばかり言いやがって:チ○ショー!「我が愛と哀しみの人間ドック2021年の記録」より抜粋)された私の眼に代わるSecret Weapon、目にも止まらぬ 走召 高速!でマークシートを読んでくれる機械の眼という意味がほぼない長い前置きを乗り越え、今、怒涛のクライマックス。「OpenCV」ライブラリがいよいよ My PC へ!

サぁイレント ナァイ~ ホぉリィ ナァイ~(さらに意味なし)

>python -m pip install opencv-python と入力してEnter!

注意:「opencv」に続けて「-python」が必要。

念願のOpenCVのインストールについに成功した・・・その日、彼は狂喜乱舞して泣き崩れたという。彼の日記の末尾には「OpenCVよ。永遠なれー」の文字が。

ちなみに、この時点で「Numpy」と「OpenCV」を入れた「python-3.9.9-embed-win32」フォルダの内容は152MB!と他を圧する勢いで巨大化していた。最初は15MB程度しかなかったのに10倍に膨れ上がっている・・・。

なんということか。すでに語るべき言葉を私は持たない。大きな広い美しい心で、この変化をありのままに・・・、そうだ、謙虚に受け止めよう。さぁ深呼吸だ。おぉ空気がうまい。生きてるってことは素晴らしい。

そう言えば、私が書いたDelphiのプログラムをことごとく「ウイルス扱い」して「隔離」しやがる某有名ウイルス対策ソフトも、今日は静かにしてるじゃないか。人間、すべからく、受容することが肝心だ。別にPCの重さがいつの間にか10倍になって、持ち運び困難になったわけではないのだから。

6.単体で動作確認(検証)

作成したEmbeddable Pythonのフォルダ「python-3.9.9-embed-win32」は名前が長く、ちょっと扱いにくいので、フォルダ名をもう少し短く、わかりやすい名前に変更してから、動作検証を行う。

変更前: python-3.9.9-embed-win32 → 変更後:python39-32

フォルダ名の意味:前から順に「Pythonが入っているフォルダで、そのメジャーバージョンは「3」、マイナーバージョンは「9」で、ターゲットプラットフォームは32ビット版だよ」と、全世界のユーザーにやさしくPR(どこかのサイトでこの表記法を見て感動!)。

【動作検証の準備】

上で作成した「python39-32」フォルダと同じ階層に、新しく「psf」という名前のフォルダを作成する。ここにテスト用のScriptファイルや画像データを保存する。

【説明】psf:「P」ythonの「S」criptが入っている「F」older ・・・ という意味。

データ保存用の psf フォルダを作成

【動作検証用の環境変数設定バッチファイルを作成】

最終的にはDelphiから操作する予定のEmbeddable Pythonだが、ここでは動作検証用のバッチファイルを作成し、これを起動してテスト用のScriptを走らせる。

最初に環境変数をセットするバッチファイルを作成する(バッチファイルの作成に関しては、下記参考リンク先:「Windowsでpythonを使う/配布する時に便利!Python embeddable package使い方」に大変詳しい解説があります。作成した方に心から感謝 m(__)m )。

以下の3行をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「setmyenv.bat」という名前を付けて、上の図の「新しいフォルダー」に保存する。

SET DP0=%~dp0
SET PATH=%DP0%\python39-32;%PATH%
SET PYTHON_PATH=%PYTHON_PATH%;%PYTHON_PATH%\Scripts

1行目で、バッチファイルのあるフォルダをカレントディレクトリに指定
2行目で、PATHにEmbeddable Pythonを入れたフォルダへのパスを設定
3行目で、Python.exeとpip.exeへのパスを設定

【動作検証用のスクリプト実行バッチファイルを作成】

続いてScriptを実行するためのバッチファイルを作成する。 以下の5行をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「python_script.bat」という名前を付けて「新しいフォルダー」に保存する。

@echo off
cd /D %~dp0
call setmyenv.bat
cd psf
cmd

1行目は、コマンドプロンプトの画面表示を抑制して見やすくする
2行目は、 バッチファイルのあるフォルダをカレントディレクトリに指定
3行目は、環境変数設定用バッチファイルを内部的に呼び出して実行
4行目で、画面に表示するディレクトリへ移動
5行目は、コマンドプロンプトを表示する

フォルダとファイル構成

【検証用スクリプトを作成】

Embeddable PythonにインストールしたNumpyとOpenCVをインポートして動作する検証用のScriptを作成する。 以下の内容をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「test.py」という名前を付けて「psf」フォルダーに保存する。

import numpy as np
import cv2

img = cv2.imread("test.jpg")
print(type(img))   # Numpy配列に画像データが読み込まれたことを確認
print(img.shape)   # OpenCVが読んだ画像情報(縦横画素数他)を表示

【検証用画像を用意】

任意のJpeg形式の画像を「test.jpg」という名前で「psf」フォルダーに用意する。画像ファイル名に日本語は使えないことに注意する(OpenCVの読み書きコマンドは日本語に対応していないため、日本語が混じっているとエラーになる)。この問題への対応方法は下記参考リンクをご参照ください。

psf フォルダの内容

【検証】

(1)「python_script.bat」 をダブルクリックしてコマンドプロンプトを起動。

コマンドプロンプトを起動したところ

(2)赤で示した下線部に「python test.py」と入力してEnterキーを押す。

黄色の枠内に結果より正しく動作したことがわかる。
<class ‘numpy.ndarray’>:データ形式はNumpyの配列、
(284, 283, 3)は、縦・横の画素数とチャンネル数を示す。

【参考URL】

Windowsでpythonを使う/配布する時に便利!Python embeddable package使い方

URL:https://hituji-ws.com/code/python/python-emb-usage/

Python OpenCV の cv2.imread 及び cv2.imwrite で日本語を含むファイルパスを取り扱う際の問題への対処について

URL:https://qiita.com/SKYS/items/cbde3775e2143cad7455

WindowsでPython3.7の実行環境を手早く作る方法

URL:https://qiita.com/hirohiro77/items/377dfc0a264acb3db222

7.まとめ

(1)使用目的や使用条件、必要なライブラリのインストール上の制約(どのバージョンのPythonに対応しているか)、何bitのアプリケーションに埋め込むのか等、事前に必要事項を十分調査した上でダウンロードするEmbeddable Pythonのバージョンを決める。

(2)ライブラリのインストールは必ず「Python -m」を付ける。→ 付けないとモジュール参照パスの指定等に問題が発生(構成を壊してしまうとの情報あり:参考リンク「WindowsでPython3.7の実行環境を手早く作る方法」を参照)するようだ。

Python -m pip install (ライブラリ名)

(3)必要なライブラリをインストール後、実際にそれらをimportして動くPython Script をEmbeddable Pythonで動かし、確実に動作することを確認する。Delphiに埋め込んでから余計なトラブルに悩まされないよう、ここで必ず単体で動作することを確かめておく。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Delphi & Embeddable Python

「なぜ Delphi & Embeddable Python なのか?」

自分ひとりで使うにはPythonはとても便利だ。カプセル化してある高機能なライブラリのおかげで、わずか数行Scriptを書くだけで、とんでもない処理が誰にでも簡単に実現できる。

必要な大抵の処理は、自分で書かなくても、どこかの優秀な方が作ったサンプルが、Web上のあちこちで公開されているから、ほとんどすべてそれで間に合ってしまう。だから、Pythonに関する限り、自分で書くというよりは、誰かが書いたものを探している時間の方が多い・・・というのは、私だけではないだろう。

それらを写経して、切ったり、貼ったりして業務をこなす。便利であること、この上ない。ラクをしたその分だけ、プログラミングする楽しさや喜びが失われたような、そんな気がすることもあるが・・・。

ただ、他人様に使っていただくモノについては、これが当てはまらない。

「マニュアルを読まなければ使えないようなプログラムは、ダメなプログラムだ。」・・・という、もはや信念と化した、狂気に近い思い込みが私にはある。

「マニュアルを読まなくても使えるプログラム」

それを実現するのがGUIなのだが、簡単・高速に、そのインターフェイスを作る機能は残念ながらPythonにはない。tkinterやPyQtを試したこともあったけど、Delphiのようにはいかなかった。直感的な操作という点で、どうしてもPythonで使えるGUI環境作成ツールはどれもこれもDelphiのそれに見劣りする(・・・と私は思う)。

唯一、2018年から開発が始まったというPySimpleGUIだけは、ちょっと違ったが。

さらに、実行形式のexeファイルにする作業もPythonだと困ることが多い。以前、業務で使用するプログラムをPythonで書き、exe化したら何と300MBを超える巨大なexeができちゃった・・・ことがある。ちゃんと動いたけど。必要なライブラリを全部!詰め込んだから、おなかいっぱいになっちゃった・・・んだろう。たぶん。

ところで逆に、Delphiで業務で使用するマークシートリーダーを開発した際、Delphiから利用できるOpenCVライブラリを使ったのだが、100枚読み取るのに4~5分を要した。読み取るA4横のマークシートは1枚が「1行あたりマーク数16個×25行×3列」という仕様(これは必須)なので、1枚あたり判定必要数はなんと1200! で、これが100枚あるとすると合計12万!

PCは、マークされている場所だけ読み取る・・・なんてヒト並みの芸当は絶対にできないから、白紙のマークシートであっても地道に1個1個・・・1枚についてきちんと1200回、白・黒の判定を繰り返す(実際の処理は、スキャナーで読み取ったマークシート画像にゴミ取り用のガウシアンぼかしをかけてから、ある閾値で二値化して、白黒反転させ、1行ずつ元画像から切り出して、さらにその画像を1行あたりのマーク数で細かく均等に分割して、1枚について1200個生成される画像1つ1つについて画素が白の部分の面積を計算し、白面積が最も大きい画像をマークありぃ!と判定している)。

私なら、1枚でやめます。・・・ってか、1行分でも多分無理です。

読み取りに「5分」かかったとすると、5分は300秒。12万個のマークを300秒で読むから、1秒あたりの読み取りマーク数は400個。1枚に3列(1200個)あるから1列1秒、1枚3秒で読んでおり、ヒトがそれをやるのに比べれば、これでも十分に高速なのだが・・・。

ところがPythonで同じ処理を書いてみたら、速いのだ。コレが・・・。

1枚250ms以下で読み取ってしまう。処理の流れはどちらも同じ(どちらも書いたのは私)だから、Python環境での処理速度は、Delphi環境のそれの12倍も速いことになる・・・。100枚を30秒未満で処理できる実力。これをどうにかして生かしたい。

そんな時、Embeddable Python というモノが存在することを、私は知ってしまったのだ。

Python Embeddableとは、超軽量なPythonの実行環境でファイルサイズがとても小さく、Windowsのシステムを汚さずに環境構築ができ、配布するのも簡単という特徴があります。

Webエンジニアの仕事見聞録(https://engineer-milione.com/programming/python-embeddable.html)より引用

Delphiで創ったコレが・・・

拙作Delphi製マークシートリーダー(テスト用サンプルを読み込んだところ)
拙作マークシートリーダーは上記リンク先ページからダウンロードできます。

PythonのOpenCVという視力を得たなら・・・どういうコトになるか?と思うと・・・

年甲斐もなく、ドキドキしてくるじゃありませんか! 皆さん

まとめ

(1)DelphiはGUI環境を簡単・高速に作成できる。

(2)Pythonには強力無比の数値演算ライブラリがある。

(3)DelphiでGUIを作成し、内部的な演算処理はPythonで実行。

(4)それを可能にするのがEmbeddable Python

(5)誰が言ったか知らんけど、

為せば成る!

俺はやるぞ!

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】