「Delphi」カテゴリーアーカイブ

プログラミング言語Delphi関連のコーディングTips

キャラクタースタンプ作成ツール(TalkStampStudio)のご紹介

授業プリントや連絡帳、掲示物に、キャラクターを登場させたい。そんなときに使える、Windows用の無料ソフト「TalkStampStudio」を作りました。

2026年10月20日(火)、女性キャラクター版(TalkStampStudio_FemaleEdition)を、フリーソフトとして公開しました。この記事では、こちらの版を利用したアプリの概要と使い方を紹介します。

※ 2026年10月21日(水)、男性キャラクター版(TalkStampStudio_MaleEdition)も追加しました。

アプリの見た目と、添付したキャラクターシートが異なるだけで、両Editionとも機能は同じです。

この記事でわかること

  • どんなソフトか(概要と特長)
  • ダウンロードと、はじめて起動するときのしかた
  • 使い方(Wordに貼る・ふきだし・スタンプ・印刷)

【もくじ】

1.Talk Stamp Studioとは
2.主な特長
3.同梱のキャラクター画像
4.動作環境とダウンロード
5.はじめて起動するとき
6.基本の使い方(4ステップ)
7.ふきだしをつける
8.スタンプをつくる
9.お気に入りとスタンプシート
  ★お気に入り
  ★スタンプシート
10.ちょっと便利な機能
11.まとめ
12.お願いとお断り

1.Talk Stamp Studioとは

1枚の画像に並んだキャラクターのポーズの中から、使いたいコマをクリックするだけでコピーし、Wordの文書に貼り付けられるツールです。貼り付けたキャラクターは最初から「前面」の絵として入るので、ページの好きな場所へ自由に動かせます。

さらに、キャラクターにふきだし(せりふ)をつけたり、「大変よくできました!」のようなスタンプにしたりして、そのままWordに入れることもできます。

TalkStampStudioのメイン画面。キャラクターのポーズが並んだ画像が、点線の枠で1コマずつに区切られている。
TalkStampStudio(女性キャラクター版)のメイン画面。画像を開くと、点線の枠でコマごとに分かれます。
TalkStampStudio(男性キャラクター版)のメイン画面。出来ることは女性キャラクター版と全く同じです。

2.主な特長

  • クリックだけでコピー:ポーズをクリックすると、すぐコピーされます。「コピー」ボタンは不要です。
  • Wordにワンボタンで貼り付け:「Wordに貼る」で、カーソルのある行に「前面」の絵として入ります。
  • ふきだしをつける:せりふを入力するだけ。場所や形、色を選べます。
  • スタンプをつくる:3つのかたちと6色から選べ、かざりも付けられます。
  • お気に入り:作ったふきだし・スタンプを取っておき、あとから呼び出せます。
  • スタンプシート:スタンプをA4用紙に並べて、印刷やPNG保存ができます(ラベルシールにも使えます)。
  • 左右反転・保存:右クリックメニューから、左右反転コピーや、PNG/JPEGでの保存ができます。PNGなら背景は透明です。
  • よく使うことばの追加:「3年A組のみなさん、おはようございます!」のような定型文を、設定ファイル(TalkStampStudio.ini)に追加できます。
  • 日本語/Englishの起動画面:起動時のごあいさつ画面(スプラッシュ画面)は、日本語表示/英語表示のいずれかの画面を選べます。なお、画面は複数あり、起動時に毎回異なる画面が表示されます。

3.同梱のキャラクター画像

女性キャラクター版には、女性キャラクターの画像シートを2種類同梱しています。どちらも1枚に15コマ(3行×5列)で、手を振る・指し棒・ガッツポーズ・考え中・黒板・ノートなど、教室でそのまま使えるポーズが並んでいます。

ファイル髪型
CharacterSheet_Ms001ポニーテール
CharacterSheet_Ms002ミディアムヘア(ボブ)
同梱されている、ポニーテールとミディアムヘア(ボブ)の2種類のキャラクター画像シート。
同梱の画像シート(左:CharacterSheet_Ms001 ポニーテール/右:CharacterSheet_Ms002 ボブ)

男性キャラクター版の画像シートは1種類ですが、同じ構図でカラー画像とグレースケール画像を用意しました。印刷用途に合わせてご利用ください。

アプリ内の「<」「>」ボタン(またはPageUp/PageDownキー)で、画像を切りかえられます。JPEGやPNGの画像なら、ご自身の画像シートも開けます。

上の画像の他に、同じ構図で「キラキラ感をやや抑えたカラー画像(女性キャラクター版のみ)」と、「モノクロ印刷用のグレースケール画像」も Portrait フォルダ内に入っています。もし、よかったらお試しください。

男性キャラクター版に添付したキャラクター画像(カラー画像:左、及びグレイスケール画像:右)です。

4.動作環境とダウンロード

  • インストール不要です。ZIPを展開し、フォルダ内の TalkStampStudio_FemaleEdition.exe or TalkStampStudio_MaleEdition.exe を起動します(Portrait フォルダは、exe と同じ場所に置いたままにしてください)。
  • Windows用アプリです(Delphi 12のVCLで作成した32bitアプリ)。
  • 対応OS:Windows 11で動作確認済みです(Windows 10は動作未確認です)。
  • Wordへの貼り付けには、パソコンにインストールされたデスクトップ版のWord(Office 2019など)を使います。
  • 起動すると exe と同じ場所に各種設定状況を記録するイニシャライズファイルが作られます。

【ダウンロード】

ここで紹介している女性キャラクター版(TalkStampStudio_FemaleEdition)のダウンロードリンクです。

男性キャラクター版(TalkStampStudio_MaleEdition)のダウンロードリンクです。

ちなみに、男性キャラクター版(TalkStampStudio_MaleEdition)の実行時のイメージは次の通りです。

機能は、女性キャラクター版(TalkStampStudio_FemaleEdition)と全く同じです。

5.はじめて起動するとき

ZIPで受け取った場合は、右クリックから「すべて展開」してから使ってください。

はじめて起動したとき、Windowsの青い画面に「Windows によって PC が保護されました」と表示されることがあります。

これは、Windowsが、まだ広く配布されていないアプリをいったん止めて確認しているもので、ウイルスが見つかったという意味ではありません。次の手順で起動できます。

  1. TalkStampStudio_FemaleEdition.exe をダブルクリックします。
  2. 青い画面の「詳細情報」をクリックします。
  3. 出てきた「実行」ボタンを押します。

この確認は、ふつう最初の一回だけです。なお、「実行」を押してよいのは、出どころの確かなファイルだけにしてください。

TalkStampStudioを起動したときに出る、ピンクのごあいさつの画面。
起動すると、はじめに「ごあいさつの画面」が出ます(日本語/Englishを選べます。また、種類は複数あります。)

6.基本の使い方(4ステップ)

  1. 画像を開く:「ファイルを開く」を押して、キャラクターの画像(例:CharacterSheet_Ms001)を選びます。
    • 画像をアプリの画面にドラッグ&ドロップしても開けます。
    • 点線の枠で1コマずつに区切られ、「3行 × 5列 に分けました」と表示されれば成功です。
  2. ポーズを選ぶ:使いたいポーズを1回クリックします。
    • 選んだコマに枠が付き、下の帯に「コピーしました!」と表示されます。
    • 矢印キーで、となりのコマに移ることもできます。
  3. Wordに貼る:Wordで入れたい行にカーソルを置き、アプリの「Wordに貼る」ボタンを押します。
    • キャラクターが「前面」の絵として入ります。
  4. 動かす:Word上で、好きな場所へドラッグします。
    • 大きさは四すみの○、向きは上の丸い矢印で変えられます。

ポーズをクリックすると、下の画面のように枠が付いて、コピーされます。

使いたいポーズをクリックすると、枠が濃いピンクになり、下の帯に「コピーしました!」と表示される。
ポーズを1回クリックするだけでコピーされます(下の帯に「コピーしました!」と出ます)

もしWordへの貼り付けがうまくいかないときは、コピーは済んでいるので、Wordで Ctrl + V を押して貼ることもできます。その場合は、絵を選んで「レイアウトオプション」から「前面」にすると、自由に動かせるようになります。

7.ふきだしをつける

キャラクターに、せりふをしゃべらせたいときの機能です。次の画面で設定します。

ふきだしをつける画面。左にプレビュー、右にせりふ・場所・しっぽの先・形・色などの設定がある。
「ふきだしをつける」の画面。せりふを入力して、場所や形、ふちの色(ピンク・黒・グレー・青)を選びます
  1. キャラクターのコマをクリックして選びます。
  2. 「ふきだしをつける」を押します。
  3. せりふを入力します。
    • 200文字まで、Enterで改行できます。
    • 「よく使うことば」から選ぶこともできます。
  4. ふきだしの場所・しっぽの先・形・文字の大きさ・ふちの色(ピンク・黒・グレー・青)を選びます。
    • 場所は8方向(左上・上・右上・左・右・左下・下・右下)から選べます。
    • プレビューを見ながら調整でき、ふきだしやしっぽの先はドラッグで動かせます。
  5. 「Wordに貼る」または「コピーだけ」を押します。

ふきだしは絵として入るため、Wordでは文字を直せません。直したいときはWordで Ctrl + Z で取り消し、アプリで作りなおします(前回のせりふが残っているので、直すだけで済みます)。

8.スタンプをつくる

「よくできました!」「提出日」「静かに」など、プリントや連絡帳にひとこと添えたいときの機能です。

スタンプをつくる画面。左にプレビュー、右にことば・かたち・位置・色などの設定がある。
「スタンプをつくる」の画面。色はピンク・あお・みどり・オレンジ・くろ・グレーの6色です
  1. キャラクターのコマをクリックして選び、「スタンプをつくる」を押します。
  2. ことばを入力します。
    • 60文字までです。
    • 「よく使うことば」には、はじめから19個入っています。
  3. かたち(リボン・大きな文字・カード)、ことばの位置、色(ピンク・あお・みどり・オレンジ・くろ・グレー)、文字の大きさ、かざり、シール風の白いふちなどを選びます。
  4. Wordに入れるときの大きさ(長いほうの辺が3cm・5cm・7cm)を選びます。
  5. 「Wordに貼る」または「コピーだけ」を押します。

できあがりは、次のようなスタンプです。

リボン、大きな文字、カードの3つのかたちで作った、スタンプの見本。
スタンプの見本(リボン・大きな文字・カード)

9.お気に入りとスタンプシート

★お気に入り

ふきだしやスタンプの画面の下にある「★お気に入り」を押すと、作品を取っておけます。

  • アプリ上部の「★ お気に入り」から一覧を開くと、画像を開いていなくても呼び出して、すぐにWordへ貼れます。
  • 「作りなおす」を使うと、同じ設定でふきだし・スタンプの画面が開きます。ことばだけ直したいときに便利です。
  • 直近で作ったものは、自動で30件まで残ります。

お気に入りの一覧は、次のような画面です。

お気に入りの一覧画面。作ったふきだしやスタンプが、小さな絵で並んでいる。
「★お気に入り」の一覧。作ったふきだしやスタンプを、あとから呼び出せます

★スタンプシート

一覧の画面にある「スタンプシートを つくる…」で、お気に入りや、直近で作ったスタンプを、A4用紙にまとめて並べられます。

  • 印刷も、PNG画像(300dpi)での保存もできます。
  • 列数・行数・余白・すきまを調整でき、切り取り線も入れられます。
  • ラベルシールへの印刷にも使えます。
スタンプシートをつくる画面。左でスタンプを選び、右でA4用紙の並び方の見本を確認できる。
「スタンプシートをつくる」の画面。選んだスタンプをA4用紙に並べて、印刷やPNG保存ができます。

印刷するときの注意

  • 用紙は、A4にしてください。
  • 大きさは、100%(実際のまま)にしてください。
  • 市販のラベルシールに印刷する際には、切り取り線を入れずに印刷してください。
    ⇨ 「□切り取り線を入れる」チェックボックスのチェックを OFF にします。

10.ちょっと便利な機能

  • 枠を手で直す:自動で分けた枠がキャラクターの手や髪にかかるときは、「設定」の「枠を手で直す」で、枠の位置や大きさをドラッグで直せます。直した枠は画像ごとに覚えてくれます。
  • ぜんぶ保存:シートの全コマを、番号つきのPNG画像として一括保存できます。ほかのソフトでキャラクターを1枚ずつ使いたいときに便利です。
  • キーボード操作:Ctrl + O(画像を開く)、矢印キー(となりのコマへ)、Enter(もう一度コピー)、Ctrl + S(保存)、Esc(選択をやめる)、PageUp/PageDown(前後の画像へ)が使えます。

「枠を手で直す」は、次のような画面で使います。

枠を手で直すモードの画面。マウスの下のコマのまわりに、小さな四角のつかむところが出ている。
「枠を手で直す」では、コマの枠のふちやかどをドラッグして位置や大きさを直せます

Wordの文書に入れると、次のようなイメージになります(ふきだしは絵として入ります)。

キャラクターとふきだしを入れた、プリントのイメージ。
Wordの文書に入れたイメージ(ふきだしは絵として入ります)

11.まとめ

TalkStampStudioは、「ポーズをクリック → Wordに貼る → ふきだしやスタンプを足す」という流れで、プリントや掲示物にキャラクターを手軽に登場させられるツールです。まずは、ダウンロードして、同梱の画像で、好きなポーズをクリックしてみてください。先生方の教材づくりのお役に立てればうれしいです。

12.お願いとお断り

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

採点準備作業を大幅に自動化しました!

マークシートや手書き答案のデジタル採点プログラム MS_Reader V3 の採点準備作業のうち、自動化できる部分を全自動化し、実際の採点に入れるまでの時間を短縮しました。

【もくじ】

1.手書き答案を処理する際は、最後の1枚に白紙答案を準備
2.白紙答案を準備した場合の処理の流れ
3.解答欄矩形の検出プログラムも改良
4.まとめ
5.お願いとお断り

1.手書き答案を処理する際は、最後の1枚に白紙答案を準備

新しい MS_Reader V3 ではプログラム起動直後、スプラッシュ画面が表示された後に、次のメッセージが表示されるよう仕様を変更しました。

初めて採点を行う場合、採点専用画像はまだ作成していませんから、上のメッセージに対しては「いいえ」を選択することになります。ここでは「いいえ」が選択されたものとして、操作方法を説明します。続けて、次のメッセージが表示されます。

試験の形式をオプションボタンで選び、OKをクリックしてください。

ここでは「手書き答案のみを利用した試験の採点」を行うこととして説明を続けます。

OK をクリックすると、次のメッセージが表示されます。

最後の1枚は白紙答案をスキャンすることを推奨仕様としました

【最後の1枚を白紙答案とすることを推奨仕様とした理由】

手書き答案を採点する場合、解答欄矩形の検出処理にこれまでは、採点専用画像として用意した画像の先頭1枚目の画像を使用してきましたが、解答欄に解答が記入されていると、どうしても解答欄矩形を誤検出(1つの解答欄を2分割して検出する等)してしまうことがあり、出来れば解答欄矩形の検出作業には、解答の書かれていない白紙の解答用紙を使いたいとずっと考えてきました。

試験実施前に予め白紙の解答用紙をスキャンして解答欄矩形の座標を検出し、座標情報をデータ化して保存しておくことは不可能ではありませんが、それなりに手間がかかり、この採点準備作業を予め行っておくことは、一般ユーザーには難しく感じられるに違いないと考えました。

また、スキャンした画像から採点専用画像を作成する際、画像サイズを縮小した場合は、この縮小率を忘れると、これが意図しない事故の発生原因になってしまうという問題もありました(実際に、作者である自分自身が縮小率を忘れ、困ったことも・・・)。

上記メッセージに対して「はい」を選択した場合、プログラムは採点専用画像作成時に最後の1枚を別フォルダに移動し、これを解答欄矩形の座標取得用答案画像として使用するよう、プログラムを修正。同時に採点専用画像作成作業そのものを全自動化して、採点準備にかかる手間を大幅に削減しました。

【新しく生まれた構造的な問題点】

プログラムを起動して、初めて「解答用紙をスキャンする際、白紙の答案1枚を最後に追加しておく必要がある」ことがわかる点が、構造的な問題として残ったというか、今回、新しく生まれてしまいました。

2.白紙答案を準備した場合の処理の流れ

「はい」をクリックします。

「はい」を選択した場合、次のメッセージが表示されます。今回、採点専用画像の作成を自動化する新機能を追加しました。

スキャンした答案画像を保存した「フォルダ」をクリックして選択します。

「ファイル」ではなく、「フォルダ」を選択します。

画像の回転が必要な場合に、次のようなダイアログが表示されることがあります。
※ プログラム側で回転の必要性の有無を確認できた場合、このダイアログは表示されません。

指定したフォルダ内にある全画像に同じ設定が適用されます。
(解答用紙をスキャンする際、全解答用紙の向きを必ず確認してください)

変換作業が始まると、作業の進行状況は画面下部のプログレスバーに表示されます。変換先のProcDataフォルダ内に変換元と同じ名前のフォルダが作成され、変換元の画像がA3やB4サイズだった場合には、A4サイズ相当に自動的に縮小された画像が、採点用画像として連番のファイル名が付いた状態で保存されます。

無事、採点専用画像が作成されると、プログラムは解答欄矩形の座標を自動的に取得して表示します。出来れば、この部分も全自動化してしまいたい部分なのですが、現在の私の技術では、どうしてもそれが出来ません。次の図に明らかなように、現在採用している矩形検出アルゴリズムは、解答用紙の解答欄だけでなく、解答用紙に印刷された文字に外接する矩形を勝手に作成し、その座標を取得し表示してしまいます。

【検出アルゴリズムの改良前】

▶ ボタンをクリックして、必要な座標のみ解答欄座標側に移します。

ここから先は、これまで通りの使い方になります。

3.解答欄矩形の検出プログラムも改良

・・・と、ここまで来て、上の画像を眺め、あまりにも解答欄矩形の誤検出?が多いことに気がつきました。矩形検出プログラムは、解答欄矩形どころか、本来、というか、元々「枠がない」文字列の外接矩形を自分で勝手に作成し、その座標まで検出しています。

ちなみに枠の検出は、しきい値の決定には「大津の二値化(Otsu 法)」、解答欄群(ブロック)の検出にはモルフォロジー演算(膨張)+輪郭抽出(二値化後に3×3で2回膨張させ、最外周の輪郭だけを取って外接矩形にする。幅150px超などで絞り、解答欄が並ぶブロックを見つける)を用いています。さらにこうして発見した解答欄群(ブロック)毎に、レイアウト判定(解答欄群をを左右の列に振り分け、上下分割・左右連結等を判定)を行い、解答欄矩形を抽出(各ブロック内で再び二値化し、1回だけ膨張。枠の外周・内周を含む全輪郭を取り、条件で絞る)し、後処理(枠線の外周と内周のような位置の近い重複を除外し、Y座標の許容幅20pxで行にまとめ、左から右、上から下へ並べる)を行って、最終的に得られた値を解答欄矩形の座標として表示していましたが、これに以下の処理を追加し、文字列を誤って解答欄矩形として検出してしまわないようにプログラムを改良しました。

【今回追加した「真枠」の発見方法】

  • インク率判定:矩形を枠線の太さ分だけ内側に縮め、内側の黒画素の割合(0.15未満なら解答欄)で解答欄矩形であるかどうかを判定(白紙の解答用紙を用いることが大前提)。
  • 印刷インクのマスク:同じフォルダの複数の答案を二値化し、論理積(AND)で全答案に共通するインクだけを残します。これで手書き文字があっても、その部分を除外して判定できます。
  • 位置合わせ:画像の行・列方向の黒画素量の分布(射影プロファイル)の相互相関で、答案間の平行移動を推定します(±20px)。さらに膨張でわずかなずれを吸収します。

【検出アルゴリズムの改良後】

誤検出しなくなりました!

4.まとめ

これまで手動で採点用画像を準備していたことを思うと、今回の自動化処理の搭載で、必要な工数が相当数削減され、ユーザー体験の向上が図れたのではないか? と思います。ScanDataフォルダ内の任意の名称を付けたフォルダ内にスキャンした解答用紙の画像を保存して、採点用画像の自動作成機能を利用すれば、データの保存場所の知識等は一切不要で、最も重要な「デジタル採点」作業そのものに専念できます。作者である私が動作確認を行った範囲では、意図した通りに動作し、不具合は発見できませんでした。万一、バグがありましたら発見次第修正して、このブログで報告し、修正版をアップロードします。

採点専用画像の自動作成機能を追加した MS_Reader V3 Ver,3.1.0 は次のリンク先記事にあるダウンロードリンクからダウンロードできます。

5.お願いとお断り

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

MS_Reader V3 の使い方(手書き答案のAI自動採点)

ONNX Runtime+PP-OCRv5 と専用分類器による AI自動採点システムの使い方(Delphi 12)

画像に alt 属性が指定されていません。ファイル名: Screenshot_20260921_030942.png

MS_Reader V3 に追加した AI自動採点ボタン

【もくじ】

1.MS_Reader V3 を起動
2.AI自動採点ボタンをクリック
3.模範解答を入力
4.AI自動採点を実行
5.次の設問に移動
6.選択肢を制約(範囲を限定)する場合の設定方法
7.ダウンロード先のご案内
8.お願いとお断り

1.MS_Reader V3 を起動

MS_Reader V3 を起動して、採点可能な状態にした後、採点する設問を選択します。下図は設問1が選択されています。

No,5 の be は、私自身の手書き文字です。その他の文字も、機械学習の学習データとして使用すること、及び、Web上で公開する場合があることを説明の上、提供者の同意を得て収集したものですが、意図しない二次使用を防止するため、加工して掲載しています。

2.AI自動採点ボタンをクリック

マウスポインタが乗ってるボタンがAI自動採点ボタンです。

Shift キーを押しながら、AI自動採点ボタンをクリックすると、「自動 × の付け方」と「AI自動採点で要確認に回された解答を別窓の確認画面でチェック(=目視確認)する機能の利用の有無」を設定する画面を表示できます。

積極的に自動 × にするには、選択肢制約の設定が必須です。

3.模範解答を入力

(1) 模範解答が未入力の場合、次のメッセージが表示されます。

「はい」をクリックします。

(2) 入力画面が表示されます。設問1の模範解答を入力します。

模範解答を入力したら、画面右下にある「保存して閉じる」ボタンをクリックします。

4.AI自動採点の実行

(1) 採点を実行する場合は「はい」をクリックします。AI自動採点の進行状況は、プログレスバーに表示されます。

「はい」をクリックしてAI自動採点を実行
「いいえ」をクリックして、模範解答の再入力

(2) AI自動採点が実行され、初回実行時のみ、採点ログの保存方法を問うメッセージが表示されます。

AI採点ログの新規作成・追記を問われます。
新規作成の場合は「はい」、既存のログに追記する場合は「いいえ」をクリックします。

※ 2回目以降は、AI採点ログに追記するかどうかだけ問われます。

(3) 採点結果が表示されます。

要確認となった解答がある場合、AI採点後に別窓で解答をチェックする設定がONであれば、次のように別窓が開いて、番号順に拡大表示されます。目視による採点を行ってください。

AI採点ログの内容は、次の通りです。

ちなみに推論の進行状況はプログレスバーで表示されます。

(4) 採点結果を確認します。採点結果を修正する場合は、手動採点する画像をクリックして選択し、正解なら「1」キー、不正解なら「2」キー、部分点ありなら「3」キーを押し下げします。採点結果は、デフォルト設定では自動保存されます。「4」キー押し下げで採点結果を取り消すこともできます。

5.次の設問に移動

グリッドコントロールの設問2のセルをクリックするか、データの移動ボタンをクリックして、次に採点するデータを表示します。

次のデータを表示します。

6.選択肢を制約(範囲を限定)する場合の設定方法

選択肢が「例:アイウエオ」の場合、推論する範囲を予め、この5文字に限定することで、推論の成功率を(何もしないよりは)高めることが出来ます。

「いちいち設定するのは面倒だ」というユーザーの声が聞こえたように思いましたので、設定が必要な箇所を自動で判定し、一括で選択肢制約を設定出来るようにしました。

模範解答を一通り入力した後、ダイアログ右上にある「選択肢の一括入力」ボタンをクリックすることで、選択肢制約を一括設定できます(ただし、この機能は使い方を誤ると、設定済みの選択肢制約を自動的に書き換えてしまうことになりますので、使用の際は細心の注意が必要です!)。

設問3~6に選択肢制約を設定する前の状態です。
設定時に表示される確認メッセージです。
設問3~6にも選択肢制約が設定されました。

7.ダウンロード先のご案内

AI自動採点機能を搭載した MS_Reader V3 Ver,3.1.0 は次のリンク先記事にあるダウンロードリンクからダウンロードできます。

8.お願いとお断り

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

Cursor 事始め

画像加工用の自作アプリケーションで使用していた TImage コントロールを TPaintBox コントロールに入れ替える作業を Cursor で行いました。自分でやったら、「少なくとも数日は要したであろう」作業が「わずか数分で、しかも、エラーなく、完了!」しました。Cursor 凄すぎます!

【もくじ】

1.コントロール入れ替えのための準備作業
2.用意したプロンプト
3.実行
4.まとめ
5.お願いとお断り

1.コントロール入れ替えのための準備作業

今回の主役 Cursor をまずインストール。※ 以下の解説は Windows の場合です。

https://cursor.com/ja/download

“Download for Windows” をクリックしたら自動的に CursorUserSetup-x64-3.4.XX.exe がダウンロードされました(XX 部分の2桁の数値は頻繁に更新されて増加するようです。ちなみに 2026 年 4 月 28 日は 11、2026 年 5 月 16 日は 20 でした)。この exe を起動して Cursor をインストールします。

インストールが完了したら、日本語化します。

Cursor を日本語化するには、日本語表記用の言語パッケージをインストールする必要があるとのことで、Web 上にあった情報を参照しながら作業しました。

検索バーを表示

赤い枠囲み部分をクリック(ショートカットを利用するなら Ctrl + Shift + X を実行)して、検索バーを表示し、Japanese Language pack と入力するだけで自動的に検索が実行され、表示された検索結果の上のほうにある Japanese Language Pack for VS Code をクリックすると、その右側に Install ボタンが表示されるので、こちらをクリックしてインストールを実行します。

言語パッケージのインストールが完了したら、表示言語の設定を変更します。

メニューの View からコマンドパレットを開き(ショートカットなら、Ctrl + Shift + P)、次のように入力します。

Configure Display Language

すると、下に Configure Display Language が表示されるのでそれをクリックして選択します。

クリックして選択

使用できる言語の一覧が表示されるので、日本語を選びます。

(参考:日本語に設定した後の画面のスクリーンショットです)

設定を反映させるには、Cursor の再起動が必要なようです。再起動したら、Cursor 上部のメニューが日本語になっていました。これでとりあえず設定は完了したことにします。

Cursor の使い方をまったく知りませんので、とりあえず、この状態で動かし、何か問題があればエラーメッセージ等が表示されると思いますので、それを見て対応することにしました。

【重要】

次は、今回コンポーネントの入れ替えに使用する Delphi のプロジェクトの内容を丸ごと別のフォルダ内にコピーします。コントロールの入れ替え作業は、このコピーしたプロジェクトに対して実行します(オリジナルはバックアップとして必ず残しておきます)。

Cursor を起動して、ファイルメニューから「フォルダーを開く」を選択し、オリジナルプログラムを全部コピペした作業用フォルダを選択すると、左ペインにフォルダ内のファイルが表示されるので、書き換え対象の pas ファイルをクリックして選択します。

中央のエディター部分に pas ファイルの内容が表示されますが、文字化けしていました。

画面右下にある文字セットの表示をクリックします。

次のように表示されるので、「エンコード付きで再度開く」をクリックし、

表示される一覧から「 Japanese (Shift JIS) 」をクリックして選択したら、

宇宙語みたいな文字は、無事、日本語に変わりました!

Cursor 本体のインストールと日本語化と文字化け対応以外に、ここまでで行ったことは、Delphi のプロジェクトをコピペしたフォルダを指定して開き、コードを記述した Unit1.pas をクリックして中央のエディターペインに表示しただけ!です。走召!カンタン!!

Unit1.pas をクリックしただけ!

これで、準備作業は、半分完了?(です)。

2.用意したプロンプト

あとはプロンプトの入力(準備)だけですが、その前にそもそも、なぜ、TImage を TPaintBox に入れ替えようと思ったのか、その部分をザックリ説明します。このブログの過去記事でご紹介した Capity Plus(既存の製品ではどうしても満足できず、自分の欲しい機能だけ全部詰め込んで作成したオリジナルの画像加工アプリ)で、ただ一点、どうしても気になることがあって、それを解決したかったからです。

それは何かというと…

TImage は TGraphicControl でウィンドウハンドルを持たないため、これで作成したラバーバンドをドラッグすると、そのドラッグ中に画面の再描画が過剰に発生して、ラバーバンドが激しく揺れるフリッカーと呼ばれる現象が起きてしまうのです。これは Delphi の VCL の構造上避けることができない問題で、根本的な解決方法は TImage を他のコントロールと入れ替えるしかありません。

実は、当該画像加工アプリを作成した時、僕は TPaintBox でラバーバンドを作る方法を知らなかったのです。そこで、当時、唯一知っていた TImage を利用してラバーバンドを作る方法でこれを実装するしかありませんでした。

もちろん、開発当初からラバーバンドのドラッグ時に、フリッカーが発生するのは気になっていましたが、それ以外の部分では(自分的には)期待通りに動作するアプリが作れたので、もう日常使いはコレ一択で、日々たいへん喜んで使っていました。が、その後、偶然、TPaintBox でもラバーバンドが作れることを知ってからは、当該アプリ使用時にフリッカーが発生するのを目の当たりにするたびに(直さなきゃ…)って思いが、日に日に強くなって…

しかし、コントロールの入れ替えに伴う(自分的には)膨大な手間と、その入れ替えに失敗する可能性(の高さ)を考えると(フリッカーが発生しても、これは Delphi の TImage の仕様みたいなもので、アプリ自体が落ちるわけじゃないし、取り敢えず使用できるので、それを究極の不具合とは思えなかったこともあり)、入れ替え作業を実行することにためらいを感じていたことも事実でした。

そんな時、職場の友人から Cursor のことを聞き(それは凄い!と、かなりワクワク・ドキドキしてしまい)、Cursor の使い方を、ぜひ学びたい気持ちも相まって、これは絶好の機会だと感じ、Cursor に何もかも全部丸投げするかたちで VCL コントロールの入れ替えを行ってもらおうと思ったのです。

プロンプトは、どんなものを用意すればいいのか、さっぱりわからなかったので、単刀直入に次のプロンプトもどき?と、TPaintBox でラバーバンドを実装した際に書いたコード一式を指定。

このDelphiのコードで、TplResizeImage クラス(コンポーネント)を使ってラバーバンド(選択矩形)を描画している部分を、以下のUnit1内のTPaintBoxを使った実装に書き換えてください。

unit Unit1;

interface

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

(以下、延々と続く TPaintBox で作るラバーバンドの実装コード)

このプロンプトもどき?以下のラバーバンドの実装コードは、このブログの次の過去記事で紹介させていただいた Unit1.pas です。これを、そのまま、全部、プロンプト入力欄にコピペしました(記事に掲載した内容そのままです!)。

・・・ということで、次のようにプロンプトが完成(?)しました。

入力したプロンプトはこれだけ!

はたして、これで、OK なのでしょうか?

3.実行

他にすることは思いつかないし、 Cursor の詳しい設定は知らないので、あとは実行するだけです。プロンプト入力欄の右下の ↑ マークをクリック後、Cursor が作業を完了するまで約9分かかりました。

作業終了後、修正結果を上書き保存して、Delphi で当該プロジェクトを開き、コンパイルしたところ、エラーは発生せず、アプリのフリッカーは見事に消え、その他の機能は「コンポーネントの入れ替えなどなかった」かのように、完全に動作しました!

結果として、新たな問題は一切起こることなく、VCL コンポーネントの入れ替えに成功しました。あまりにも簡単に出来てしまったので、なんだか信じられない気持ちでしたが、巷で騒いでいる通り、まさに「 そういう時代になったのだ 」と、それだけを、ただ、ただ、ただ・・・ 実感しました!!

正直、今までの(コーディングに関する)苦労 は、いったい なんだった のでしょうか…

【参考】

使用できる AI は、Cursor Settings の Models で設定できるとのことでした(今回はここの設定は変更せず、デフォルトの設定のまま、コンポーネントの入れ替え作業を実行しました)。

状況によっては、指定した方がよさそうな気がしました。

4.まとめ

Cursor すごい です!!!

【ご注意願います】

ただ、今回得られた結果を AI に確認したところでは、たまたま(今回に限っては)うまく行った!というのが本当のところで、同じプログラムコードに対して、同じプロンプトを適用しても、生成されるコードは毎回、同じにはならない(似て非なるものになる)ということでした。

今回のプロジェクトに対して、試しに実験してみましたが、確かに2回目はコンパイル時にエラーが発生して、コードの訂正作業が必要になりました(実際に訂正作業は行いませんでしたが・・・)。1回目で期待した通りの結果が得られたのは、ただ単に、たまたま運がよかっただけなのですねー☆

ドラッグ時のフリッカーが消えた画像加工アプリは、次のリンク先記事内でダウンロードできます。もし、よかったらお試しください。同一範囲の連続キャプチャ(連続してスクリーンショットを撮る場合)など、それなりに便利に使えると思います。

5.お願いとお断り

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

MS_Reader V3 の使い方(マークシート&手書き併用型試験編)

この Blog で先にご紹介した完全無料でお使いいただける、「手書き答案のデジタル採点機能付き」マークシートリーダー MS_Reader V3 の使い方のご案内です。今回は、マークシート&手書き併用型試験の採点方法です。

プログラムの簡単な説明とダウンロードは、当 Blog の次の記事をご参照ください。

マークシートを利用した試験の実施と採点方法は、次の記事をご参照ください。

MS_Reader V3 の付属の機能を利用した、手書き答案のデジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

【もくじ】

1.併用型試験を選択
2.併用型試験の成績処理方法
3.まとめ
4.お願いとお断り

【追記】答案返却後のマークシート採点結果他の修正方法について

マークシートと手書きの併用型方式で試験を実施した場合、まず最初にマークシートの採点を実施・確定後、手書き答案の採点を行い、合計点の計算を実行してください。

以下の問題は、2026 年 6 月 9 日午前 6 時以降にダウンロードしたバージョンでは修正されています。

プログラムが上記の日付以前のもので、採点結果の修正が期待した通りに実行されない場合は、以下の手順で修正できます。

答案返却後等に、マークシートの採点結果を修正した場合は、併用型の場合、手書き答案の成績一覧表作成時にマークシートと手書きの合計得点が計算されますので、マークシート採点結果を修正した再配布用の採点結果通知シート(返却用答案)は次の手順で作成する必要があります。

(1)マークシートの採点結果を修正・専用CSVファイルに上書き保存
(2)手書き答案採点側で、マークシート採点結果と合算して成績一覧表を作成
(3)マークシート採点側に戻り、採点結果通知シート(返却用答案)を作成・印刷

重要なお知らせ

併用型の試験において、マークシート採点側で採点結果を修正・保存し、上記手順によらず、そのまま採点結果通知シートを作成した場合、マークシート採点の修正結果が合計点の計算に反映されません! くれぐれもご注意願います。

これはバグというより、設計上の見通しが不十分であったことに起因する不具合です。改善案をまとめ、プログラムを修正し、期待通りに動作する状態に修正できましたら、追記報告いたします。プログラムの修正が完了するまで、上記手順で再配布用の採点結果通知シート(返却用答案)を作成していただけますよう、お願い申し上げます。

【追記】プログラムの改良と不具合修正の報告です。

次の改良と修正を行いました。(20260603)

手書き答案の採点モードで、解答欄を一覧表示している際、スクロールが固まる(完全に固まるわけではなく、上下にわずかしか動かなくなる)現象が発生する場合があることを確認し、この問題を修正しました。

解答欄座標の取得時に、保存の処理を行わないで終了すると、解答欄座標の取得ボタンの Enabled が False のままになり、再度、解答欄座標の取得作業が出来なくなっておりましたが、解答欄座標を取得後、保存の処理を行わないで終了した場合でも、再度、連続して解答欄座標の取得処理ができるようにプログラムを修正しました。

次の改良を行いました。(20260607)

配点等入力モードから採点モードに戻る際、データの変更の有無にかかわらず、配点等のデータをファイルに上書き保存する必要がありましたが、これをデータの変更があった場合のみデータを上書き保存する設定に修正しました。

【追記】プログラムの改良と不具合修正の報告です。

次の修正を行いました。(20260530)

マークシート採点と手書き答案採点の併用型を指定した場合に出力される採点結果通知シート(返却用答案)の順位がマークシート採点の合計得点のみから計算した順位となっていました。ほんとうにごめんなさい。ユーザーさんからの指摘を受け、マークシート側の得点合計と手書き答案側の得点合計を合算した総得点で計算した順位に修正しました。

さらに、採点結果通知シート(返却用答案)の作成がより滞りなく実行できるよう、「配点」をチェックして採点結果を読み込まなくても、プログラム起動時に採点結果を保存したファイルの存在を確認し、それがある場合は直ちに採点結果通知シート(返却用答案)が作成できるよう、コードを修正しました。

マークシート採点と手書き答案採点を併用するモードで動作している場合、合計点表示位置を指定後、直ちに採点結果を保存したファイルの存在の有無を確認し、それがある場合はマークシートの採点結果及び手書き答案の採点結果を同時に表示(描画)するよう、コードを修正しました。

また、どの採点形式が選択されていても、合計点等が整数で表示されるようにしました。

さらに、成績一覧表や正答率一覧表のヘッダー部に処理対象フォルダの名称と出力時の西暦年月日と時刻を表示できるようにコードを修正しました。

その他、Edit コントロールに表示した設定値表示位置の調整・修正を行いました。

【追記】返却用答案の作成方法です!

下記の不具合は、5月29日現在、修正し、解消されています。マークシート側で「配点」を1回クリックして Check を ON にして採点データを読み込んでいただければ、マークシートの採点結果と手書き答案の採点結果の両方を表示した採点結果通知シートが印刷できると思います。ご迷惑をおかけした皆さま、誠に申し訳ありませんでした。

<採点結果通知シート(返却用答案)が作成できない場合は?>

マークシートの採点と、手書き答案の採点の両方が完了したら、一度プログラムを再起動し、返却用答案を作成したいクラスを選んで開き直し、次の操作を行ってください。

(1)手書きをCheckして成績を計算する。
   手書き答案の採点側の成績一覧表出力で、マークシートと手書き両方の合計点を計算・出力。
(2)マークシート側の成績一覧表出力で「配点」をチェックしてマークシート採点結果を読み込む。
(3)マークシート側の調整をCheckして、次の設定を実施。
   ・得点の自動をCheck
   ・正解の自動をCheck
   ・マークシート用フォントサイズの得点を12に設定
   ・合計点印刷設定の枠を描画にCheck、サイズを14に設定。×ボタンで閉じる。
(4)マークシート側の成績一覧表出力で再度「配点」をチェックして手書き採点結果を読み込む。
(5)返却用答案の印刷を実行する。

この流れで作業すれば、確実に受験者への返却用答案を印刷できます。返却用答案の作成がうまく行かない場合は、ぜひ、上記手順をお試しください。この流れで作業すれば、返却用答案がきちんと作成・出力できることを実際の採点現場で確認済みです。

マークシートと手書きを併用した試験の採点結果処理部分につきましては、その手順の簡略化を早急に図り、プログラムをより使いやすいものにアップデートする予定です。

1.併用型試験を選択

マークシートと手書き答案を併用した試験を実施する場合、最初にマークシートの採点を実施してください。採点準備手続きの冒頭で「併用型試験を選択」すること以外、マークシートの読み取りとマークシート採点の各手順は、マークシートと手書きの併用型試験でも同じです。

マークシートの読み取りと採点の詳細なご案内は当 Blog の「 MS_Reader V3 の使い方(マークシートリーダー編)」の記事をご参照ください。

手書き答案の採点の詳細なご案内は当 Blog の「 MS_Reader V3 の使い方(手書き答案の採点編)」の記事をご参照ください。

併用型試験を実施する上で、ただ1点、ご注意いただきたい点が「併用型試験を選択」する点です。ですので、ここでは「併用型試験を選択する部分のみ」解説します。

マークシートの読み取り準備設定作業の冒頭で、特徴点 ■■■ の検出閾値の自動設定が行われた後、次のメッセージが表示されます。

必ず「はい」を選択(クリック)してください。

続けて「併用型試験の実施の有無」を問うメッセージが表示されます。

マークと手書き併用型試験を実施する場合は、
必ず「はい」を選択(クリック)してください。

これで exe と同じ位置にある settings.ini に手書き答案併用フラグが True で書き込まれ、マークシートと手書きを併用した試験の実施が可能となります。

この後、最初にマークシート方式部分の、マーク読み取りと採点処理を実行し、次に手書き答案の採点処理を実行してください。

2.併用型試験の成績処理方法

ここでは両方の採点処理が終了した後の「併用型試験としての成績処理」を行う手順を解説します。

まず最初にマークと手書き併用型試験の成績処理は、マーク部分の採点を先に、手書き部分の採点を次に、それぞれ行っていただいた後、プログラムをいったん終了し、再起動した状態で行っていただきたく、心からお願い申し上げます次第です。理由は様々にありますが、最も不具合が発生する可能性が低い方法がプログラムを再起動して併用型試験の成績処理を行う方法です。

(1)MS_Reader V3 の再起動

マークシート採点と、手書き答案の採点をそれぞれ行った後、プログラムをいったん終了し、再起動してください。

併用型試験の答案画像を入れたフォルダを選択後、画面は次の状態になります。

「実行」をクリックして選択状態にしてください。

マークシートの読み取りと採点が可能な状態になります(ここでは既にマークシートの採点処理は終了しているという前提です)。

次に「配点」チェックボックスをチェックします。すると次のメッセージが表示されます。

OK をクリックしてください。

案内を表示するモードでは、次のメッセージも表示されますが、配点や観点別評価の区分は既に入力済みなので OK をクリックしてメッセージを消去してください。

併用型試験方式の指定がある場合、これでマークシートと手書き、両方の採点データが読み込まれ、最終的な成績処理の準備が完了した状態になります。

※ 採点データが読み込まれていないまま、返却用答案を作成すると合計点等の表示が0点になります。ご注意ください。

(2)成績一覧表及び正答率一覧表の作成と印刷

・「配点」チェックボックスをチェックしたままの状態で、マークシートのみの成績一覧表や正答率一覧表の作成・印刷がそのまま実行できます。

・マークシートと手書き両方の成績一覧表や正答率一覧表の作成と印刷は、手書き答案採点側で実行可能です。

(3)返却用答案の作成と印刷

「配点」チェックボックスのチェックを外し、返却用答案の作成ボタンをクリックしてください。

ヒントは案内を表示する設定が ON の場合に表示されます。

合計点の表示位置等の設定が調整済みであれば、次のように返却用答案が作成されます。ちなみに返却用答案の印刷手続きはマークシートのみの場合の印刷手続きと全く同じです。

必要に応じて、合計点等の表示位置や、フォントサイズの調整を行ってください。

設定の保存も忘れずに実行してください。

3.まとめ

(1)試験の形式は併用型を必ず指定する。
(2)採点は、マークシート → 手書き答案採点の順に実行する。
(3)MS_Reader V3 はいったん終了し、再起動する。
(4)配点チェックボックスをチェックすると採点データが準備される。
(5)併用型の成績一覧表等は手書き採点モード側で作成&印刷する。
(6)併用型の返却用答案はマークシート採点モード側で作成&印刷する。

4.お願いとお断り

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

MS_Reader V3 の使い方(手書き答案の採点編)

この Blog で先にご紹介した完全無料でお使いいただける、「手書き答案のデジタル採点機能付き」マークシートリーダー MS_Reader V3 の使い方のご案内です。今回は、手書き答案の採点方法です。

マークシートを利用した試験の実施と採点方法は、次の記事をご参照ください。

MS_Reader V3 の付属の機能を利用した、マークシートと手書き答案の併用型デジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

【追記】プログラムの改良と不具合修正の報告です。

次の改良と修正を行いました。(20260603)

手書き答案の採点モードで、解答欄を一覧表示している際、スクロールが固まる(完全に固まるわけではなく、上下にわずかしか動かなくなる)現象が発生する場合があることを確認し、この問題を修正しました。

解答欄座標の取得時に、保存の処理を行わないで終了すると、解答欄座標の取得ボタンの Enabled が False のままになり、再度、解答欄座標の取得作業が出来なくなっておりましたが、解答欄座標を取得後、保存の処理を行わないで終了した場合でも、再度、連続して解答欄座標の取得処理ができるようにプログラムを修正しました。

次の改良を行いました。(20260607)

配点等入力モードから採点モードに戻る際、データの変更の有無にかかわらず、配点等のデータをファイルに上書き保存する必要がありましたが、これをデータの変更があった場合のみデータを上書き保存する設定に修正しました。

【もくじ】

1.解答用紙の準備
2.解答用紙読み取り前の確認
3.解答用紙をスキャナーでJpeg画像化
4.スキャンした解答用紙画像の保存先
5.解答用紙読み取りと採点のための準備
6.処理に必要なメモリー容量の確認
7.採点対象の読取専用画像が入ったフォルダを選択
8.特徴点を検出
9.解答用紙情報の設定
10.配点及び観点別評価の区分を設定
11.採点
12.成績一覧表及び正答率一覧表の作成と印刷
13.返却用答案の作成と印刷
14.手書き答案採点設定の保存
15.まとめ
16.お願いとお断り

1.解答用紙の準備

採点する手書き答案の解答用紙を準備(印刷)します。MS_Reader V3 で使用する解答用紙には特徴点が必要です。解答用紙の左上隅の任意の位置に ■■■(ドットを3つ)ご用意ください。

プログラム自体は、特徴点がなくても、スキャンした画像の左上を座標原点 (0, 0) として動作するように組んでありますが、動作テスト時に一部データの読み込みに問題が発生することがありました。ですので、誤動作を極力防止するためにも特徴点は必ずご用意いただけますよう、お願い申し上げます。

【ご注意願います】

特徴点をご用意いただいた場合でも、MS_Reader V3 が常に意図した通り、完全に動作することは保証できません。MS_Reader V3 を使用される方の完全自己責任の下、十分な動作テストを行っていただいた上で、解答用紙画像の読み込み性能に納得していただける場合のみ、MS_Reader V3 はお使いいただけます。悪しからず、ご了承ください。

MS_Reader V3 で手書き答案の採点に使用する解答用紙ですが、次の解答欄作成例ようにお作りいただけますと採点の準備が手間をかけずに行えると思います。

国語で使用する縦書き答案も同様です。

文字数の指定(制限)がある解答欄については、解答欄矩形の内部には点線を使って解答欄を作成していただけると採点準備がよりスムースに進行すると思います。

解答欄矩形を認識・取得する際、点線部分は検出されません。

点線の間隔は、スキャナーでスキャンした画像において、明瞭に点線であると判別できる間隔としてください。

上の良い例では、点線部分の上にさらに図形の四角形を置き、その内部の塗りつぶしと枠線を「白」に指定しています。このひと手間が確実に効きます。

サンプルの解答欄は Word で作成しています。

解答欄を識別するための記号・番号の直後も点線でなければいけません。ご注意ください。

また、当初から「複数の解答を抱き合わせて採点」する予定の場合も、抱き合わせて採点する範囲を自動認識できるよう、予め次の例のように解答欄矩形を作成してください。

ア、イ、ウ の解答を抱き合わせて一括採点する場合の解答欄設定(点線はなくてもよい)
(配点が1点以上であれば、部分点も設定できます)

読み取りを想定した解答欄群の形式は次の3種類です。

(1)縦方向に解答欄群(ブロック)数は1以上

上の(1)ような形式の場合、プログラムは解答欄矩形の座標をブロック1の行方向については左から右へ、列方向については上から下へ、それぞれ検出します。ブロック1内の解答欄矩形を検出したらブロック2内の解答欄矩形の座標をブロック1の場合と同様に検出、以降同じ操作をブロック数分繰り返し実行します。

(2)横方向の解答欄群(ブロック)数は2つまでを想定

ブロック数が3以上の解答用紙は想定も、読み込み実験も行っておりません。

上の(2)ような形式の場合、プログラムは解答欄矩形の座標をまず左側のブロック1から検出します。ブロック1の行方向については左から右へ、列方向については上から下へ、それぞれ検出します。ブロック1内の解答欄矩形を検出したらブロック2内の解答欄矩形の座標をブロック1の場合と同様に検出、以降同じ操作をブロック数分繰り返し実行します。

ブロック1及びブロック2は、(1)のように縦方向にさらに複数のブロックに分かれていても問題ありません。その場合は(1)で説明した通りの順番で解答欄矩形が検出されます。

(3)国語で使用される縦書き答案の場合

国語の試験で使用される縦書き解答用紙の場合、プログラムは解答用紙の右側から、まず縦方向上から下へ(青色い矢印方向)、次に横方向は右から左へ(オレンジ色の矢印方向)、解答欄矩形を検出します。縦書き解答用紙は上の図で示したように解答欄群が横方向に1以上存在する場合のみを想定しており、解答欄が縦方向(上下方向)に2段になっているような形式は想定外です。

上記(1)、(2)、(3)以外の形式の解答用紙につきましては、想定しておりませんし、従って動作確認等も一切行っておりません。現在のところ、今後、上記(1)、(2)、(3)以外の形式の解答用紙への対応を行う予定はありません。

マークシートの場合と異なり、手書き形式試験の解答用紙の印刷は、インクジェットプリンター以外の方法(例:輪転機による印刷)で行っていただいて構いませんが、試験実施後、スキャナーで解答用紙を読み取って画像化する都合上、印刷後、解答用紙の変形が生じるような印刷方法(熱が加わるような印刷方法)は避けていただいた方が賢明です。

< 戻る >

2.解答用紙読み取り前の確認

スキャナーで解答用紙を読み取る前に、次の事項について確認し、必要な作業を行ってください。

(1)欠席者の位置に白紙の解答用紙を挿入

(2)解答用紙に付着した消しゴムの屑を除去

塵も積もれば・・・ナントカで、消しゴムの屑が付いたままの解答用紙をスキャナーで何百枚も処理すると、スキャナーの光学読取装置部分が大変なコトになります。ですので、解答用紙に付着した消しゴムの屑は出来る限り除去してからスキャン作業を行ってください。

(3)解答用紙が出席番号番号「昇順」に並んでいることを確認

解答用紙が出席番号(もしくは受験番号)の昇順(=スキャナーで読み込む順)に並んでいることを最低2回は確認してください。また、その際、上下の逆転がないことも入念にご確認ください。

< 戻る >

3.解答用紙をスキャナーでJpeg画像化

解答用紙は解像度 200 dpi、カラー、Jpeg 画像を指定してスキャンしてください。

必要以上に解像度を高くしても良いことは1つもありません。200 dpi で十分な精度が得られます。また、必ずカラー画像としてスキャンしてください(スキャナー側では二値化処理しないでください)。

一般的に使用されている複合機のスキャナーでスキャンした PDF ファイルは、Jpeg 画像のコンテナのような形式で作成されていることが多いと聞き、MS_Reader V3 には、そのような PDF ファイルであった場合、Jpeg 画像を抽出する機能も搭載しましたが、様々なスキャナーで作成されたPDF ファイルを入手し、必要十分な動作テストを行うことは私の環境では不可能でありますので、この PDF ファイルから Jpeg 画像を抽出しての処理は、その可否をご確認の上、可の場合でも必要十分な動作テストを実施後にご利用ください。

なお、筆者の職場にあります E 社製複合機のスキャナーで解像度 200 dpi、カラー、PDF を指定して読み込んだデータを用いた動作検証を行い、プログラムが意図した通りに動作することを確認しましたことを申し添えます。

< 戻る >

4.スキャンした解答用紙画像の保存先

キャンした画像は、MS_Reader_V3.exe と同じフォルダにある ScanData フォルダ内に任意の名称のフォルダを作成し、そこへ保存してください。各 Jpeg 画像ファイルの名称は、この後の読み取り処理専用画像作成の際に一括して変更しますので、どのような名称でもかまいませんが、ファイルの並びが出席番号(=受験番号)順となるような名称である必要があります。

Jpeg 画像を保存するフォルダの名称は、実際には「年度・考査名・教科科目名・クラス名」を組み合わせて作成すると良いと思います。

例:R8_考査①_公共_2A

【重要】フォルダ名には「 – 」や「 ー 」を用いないでください。

フォルダ名に「 – 」や「 ー 」が含まれていた場合、読み取り専用画像を保存するフォルダ名は、スキャナーで読み取った画像を保存したフォルダ名中の「 – 」や「 ー 」を自動的に「 _ 」に変換した名称で作成されます(文字列処理の中で「 – 」を分割に利用しているためです)。この点につきましては、どうか十分にご注意願います。

また、スキャナーで読み取った画像を入れるフォルダの配置については、次の例をご参照ください。

< 戻る >

5.解答用紙の読み取りと採点のための準備

MS_Reader V3 を起動し、解答用紙の読み取りと採点のための準備を行います。

MS_Reader_V3 フォルダ内にある上のアイコンをダブルクリックして起動します。

ダウンロード直後の初回起動時に、次のような Windows Defender SmartScreen による警告画面が表示されると思います。「詳細情報」をクリックし、次に表示される画面の「実行」をクリックしてプログラムを起動してください。

ダウンロードされたファイルに「 Zone.Identifier 」という「ゾーン識別子」が付加されていると、Windows はこの識別子を見て「インターネット経由で取得されたファイル」と判断し、SmartScreen が警告を表示します。

MS_Reader V3 が起動したら、まず、読み取り専用画像を作成します。次の図に示す「矢印がフォルダ内へ入っている」アイコンをクリックしてください。

?マークをクリックすると操作ガイド PDF が表示されます。

「選択」をクリックして、スキャナーで読み取った Jpeg 画像を保存したフォルダを選択します。

「選択」をクリック
Jpeg 画像を保存した「フォルダ」を選択し、Ok をクリックしてください。

次の図に示すように①→②→③の順で操作してください。

①で、必要があれば回転方向を指定します。
②で、画像のリサイズ指定が可能です(A4の解答用紙であれば「なし」か「自動」を指定)
③をクリックすると読取専用画像を保存するフォルダを指定するダイアログが表示されます。

【ご注意】

②でリサイズする際、サイズを指定した場合は必ずその数値をメモしてください。他のクラスも同じ設定で読み取りと採点を行うためには、全クラス同じサイズで画像を変換する必要があります。

ProcData フォルダ内に読取専用画像を保存するフォルダが自動作成されますので、このフォルダをクリックして指定し、その後 Ok をクリックしてください。

「変換実行」をクリックしてください。

「マークシート」と「手書き答案の採点」を併用する場合は「はい」、併用しない場合は「いいえ」をクリックしてください。

確認メッセージが表示された場合は、Ok をクリックしてください。

他のクラスも連続して処理する場合は「画面の初期化」を、読取専用画像の作成作業を終了する場合は「終了」をクリックしてください。

< 戻る >

6.処理に必要なメモリー容量の確認

図の「矢印がフォルダから出ている」アイコンをクリックしてください。

次の案内が表示されます。

上記案内に表示している画像1枚あたりのサイズは、「メモリ上に展開されたOpenCV IplImageの生ピクセルデータサイズ」で計算しています。こちらのメモリ検証を参考に、お使いの PC で快適に作業できる答案枚数をご確認ください。

プログラムは、画像をメモリ上に展開しなくても動作するように作成してありますが、都度画像を読み込んで処理する場合、動作速度(=ユーザー体験)は大変遅くなります。全画像をメモリー上に展開できる作業環境で MS_Reader V3 をお使いいただけますよう、伏してお願い申し上げます。

プログラム作成の流れで言うと、非メモリー展開モードが当初作成した「都度画像を読み込み、採点結果を都度書込みという(流れの)オリジナル」で、それではどうしても動作速度的に満足できないと(新規に手書き答案採点プログラムを書き直した意味がない・旧版の AC_Reader に比較して明らかに良くなっている・進化していることが自分自身の中での絶対的評価基準でした)、このユーザー体験の改善は「やるか・やらないか」の二択ですから「それはやるしかないだろ」ってコトで、もう一度イチから出直して新たに追加したのが「全画像をメモリ上に展開し、読み込みはそこから行い、採点結果は画像に書き込まず JSON ファイルに保存、それが必要な時は JSON ファイルから読み出して画像上に描画する」というメモリー展開モード(=自分的には、新しく採用したアルゴリズム:現実世界では、これが業界標準のようですが・・・)です。

上記の理由から、MS_Reader V3 のプログラムは、状況に応じて2つのモードを使い分けて動作するように出来ています。・・・ 出来ているはずなのですが、メモリー展開モードを追加してからは、ほとんど、この環境でしか動作テストを行っておりませんので、もしかしたら、非メモリー展開モードでの実行時に何らかの予期しない不具合が発生するかもしれません。

ただし、非メモリー展開モードは、私ではない善意の第三者が、高校現場において、定期考査の採点に実際に使用し、致命的な問題がないことをリアル採点現場で確認済みです(この際、判明した問題点はすべて修正・改良しました)。

< 戻る >

7.採点対象の読取専用画像が入ったフォルダを選択

解答用紙のサイズを選択し、受験者数を入力してメモリー展開モードでの作業の可否を確認後、Ok をクリックすると「フォルダーの選択」ダイアログが表示されます。

採点対象の読取専用画像が入ったフォルダを選択

ここではファイルではなく、「フォルダー」を選択することに注意してください。上の例で言えば「 03_HandWriting_Port 」フォルダをクリックして選択 ⇨ 「フォルダーの選択」ボタンをクリックという流れになります。

案内が表示された場合は、Ok をクリックします。

< 戻る >

8.特徴点を検出

今回の冒頭でお示ししたように、MS_Reader V3 では、解答用紙の解答欄を個別に切り出す際の座標原点として、解答用紙の左上隅に印刷した ■■■ を特徴点として利用します。これがなくても動作するように設計しましたが、特徴点があることをあくまでも大前提としておりますので、MS_Reader V3 で処理する解答用紙の左上隅には必ず ■■■ を設置していただけますようお願い申し上げます。

解答用紙は通常ワープロソフトで作成されることが多いと思いますが、10.5~14ポイントくらいの大きさで ■■■ を入力していただければよろしいかと思います。

前作と異なり、MS_Reader V3 では、特徴点を自動認識するよう、アルゴリズムを改善しました。以下、特徴点検出方法です。

画面右上の操作案内の表示(✅Info )がチェックされている場合

■■■ の下に Thresh とある表示の右側の閾値表示用 ComboBox が「空欄」の状態で、その閾値表示用 ComboBox の右にある特徴点自動検出ボタンをクリックしてください。

特徴点検出手続きが実行され、プログラムは画像の左上付近に捜索範囲を限定して ■■■ を探します。プログラムは検出閾値を次々に変え、最も安定して利用できると思われる最適な値を見い出します。最適な値が見つかったら場合、その値付近でさらに数回試行し、安定動作(検出)出来た場合は検出動作を停止して特徴点を赤枠で囲み、ユーザーに確認を求めます。

特徴点検出作業中、画面左下に進捗状況が表示されます。

【特徴点が検出された場合】

「はい」をクリックしてください。
(背景のマークシート画像がぼやけているのは検出手続き中でガウスぼかしを利用しているためです)

【特徴点が検出されなかった場合】

特徴点が検出されなかった場合は、次のメッセージが表示されます。

Ok をクリックしてください。

上記いずれの場合でも次の確認メッセージが表示されます。マークシートのみの採点作業及びマークシートと手書き併用型の採点作業を行う場合は「はい」を、手書き答案の採点作業のみを行う場合は「いいえ」をクリックしてください。

手書き答案の採点を行いますので「いいえ」をクリックします。
横書き形式の答案であれば「はい」をクリックします。
国語で使用される縦書きの場合は「いいえ」をクリックしてください。
OK をクリックしてください。

手書き答案の解答欄の形式を指定します。

< 戻る >

9.解答用紙情報の設定

続けて解答用紙の情報を取得・設定します。選んだ解答用紙の形式により、アクティブになるボタンは異なります。

横書き形式を選択した場合にアクティブになるボタン
縦書き形式を選択した場合にアクティブになるボタン

ここでは横書き形式を選んだ場合の操作方法を説明しますが、縦書きの場合も操作内容は同じです。

横書き形式の手書き答案の解答欄座標を自動取得して表示。

ボタンをクリックすると次のような別 Window が開き、そこに取得出来た解答欄矩形の座標の一覧が表示されます。

横書き形式の解答用紙の場合(その理由はよくわかりませんが)、一般的な横書き形式の解答用紙の採点方式はほぼ 100% 左から右へ、上から下へだと思いますので、そのような人の採点時の習性に合わせて解答欄矩形の座標を取得して表示できるよう、試行錯誤を繰り返し、解答用紙画像が、多少、傾いていても採点する順番に座標を取得するアルゴリズムとしました。

座標取得直後の解答用紙の画像です。
第7設問の解答欄矩形の座標が取得できていません。

プログラムは OpenCV の矩形検出で検出できた解答欄矩形を、明るい緑色の枠で囲んで表示します。青い枠で囲まれている解答欄が、一覧表示の中で現在カーソルがある解答欄(=TMemo のアクティブな行)です。

私が以前に書いた解答欄矩形の座標検出アルゴリズムでは、幅の狭い矩形も全て取得して表示してしまい、必要な矩形座標と、そうでないものを判別する必要がありました。今回書き直したプログラムでは、ある程度の幅がないと検出対象から外すようにしましたので、上の例で第7設問の解答欄矩形が検出されていないのは、その副作用であると思われます。基本的に点線部分は矩形として認識しないように設定しましたが、このあたりの線引きは非常に難しく、あまり厳しくしすぎるとほんのわずかに線の一部が欠けていても必要な座標が検出できなくなったりしますので、微妙な調整に試行錯誤を繰り返しました。同時に、ユーザー体験の向上のため、矩形検出に使用する閾値も、プログラムが自動的に決定するように旧プログラムを改良し、自分的にはこれがベストと思われる仕様で公開した次第です。今後、実際の試験で運用し、この辺りの微調整が必要と判断された場合は、マイナーバージョンアップで矩形検出アルゴリズムを修正したいと考えています。

別 Window の ▶ ボタンをクリックすると、解答欄矩形の座標が右の TMemo へ移動します。次の図はその作業の様子です。

上下の矢印キーを押し下げすることで、アクティブな矩形座標を変更できます。

作業の手順として、検出出来なかった解答欄があっても、とりあえず、それは無視して検出できた解答欄矩形の座標を右側の TMemo へ移してください。

検出できた矩形の座標から、解答欄矩形の座標のみを選ぶ作業が完了した状態の例

次に、自動検出できなかった解答欄の座標があれば手動取得します。第7設問の解答欄矩形の座標を手動取得する例でその方法を説明します。

別 Window の中央上部にある「編集」チェックボックスをチェックしてください。

いちばん最初の解答欄矩形がアクティブな状態になります。

下向きの矢印キーを何回か押し下げして、第6設問の解答欄をアクティブにしてください。

右向きの矢印キーを複数回押し下げて、マウスのカーソルを第6設問の解答欄矩形の座標データの末尾へ移動します。

カーソル位置を座標データの末尾へ移動させたところ
拡大図

Enter キーを1回押し下げて空行を入れてください。

別 Window の ▶ ボタンの下にある「新規」ボタンをクリックします。

第7設問の解答欄の左上隅から右下隅へ向かってマウスをドラッグしてください。青い枠がドラッグした範囲に描画されます。微調整が必要な場合はラバーバンド上のグラブハンドルをドラッグして調整してください(矢印キー押し下げによる微調整は出来ません!)。

第7設問の解答欄をラバーバンドで囲んだら「取得」ボタンをクリックして、その座標を取得してください。

他にも検出出来なかった解答欄がある場合は、この作業を繰り返して、必要な解答欄矩形の座標をすべて取得してください。

作業の最後に、解答欄矩形の座標がすべて取得出来た事を、実際に採点する順番に確認してください。

上向きの矢印キーを何回か押し下げするか、TMemo をスクロールして最も上にある座標をクリックするかして、最初の解答欄矩形の座標をアクティブにし、次に下向きの矢印キーを押し下げながら、座標の取得状況を確認してください。

下向きの矢印キーを1回押し下げます。

⇩

下向きの矢印キーを1回押し下げます。

⇩

同じ操作を最後の解答欄まで繰り返し、座標の取得状況を確認してください。

最後の解答欄まで座標の取得状況の確認が出来たら「保存」ボタンをクリックしてください。確認メッセージが表示された場合は、OK をクリックしてください。

終了ボタンがアクティブになりますので、クリックして座標の取得 Window を閉じます。

「終了」ボタンをクリックしてください。

10.配点及び観点別評価の区分を設定

解答欄矩形の座標の取得作業を終了すると、その直後に次のメッセージが表示されますので、「はい」をクリックしてください。

各設問の配点や、観点別評価の区分が未入力の場合は、次のメッセージが表示されます。

OK をクリックすると、各設問の配点及び観点別評価の区分を入力するグリッドコントロールが表示されます。

全設問に設定する配点を入力してください。後から設問毎に再設定できますので、最も多くの設問に設定する配点を選択して入力してください。ここでは5点で設定したものとして説明します。

「5」を入力して OK をクリックします。
(半角数字で入力してください)

全設問の配点に「5」、観点別評価の区分に「1」が自動設定され、合計点が計算されます。

設問毎の配点を修正する必要があれば、それに該当する設問の配点を修正してください。また、観点別評価の区分も必要箇所を「2」に変更してください。

合計点は入力された値に基づき、自動計算されます。

入力後、配点と観点別評価の区分の設定値が本当に正しいことを必ず確認してください。確認後、設定を保存します。次のアイコンをクリックしてください。

配点設定等の保存は上のアイコンをクリック

次の注意が表示されます。

既に採点されたデータがある場合、正解(○)と採点したデータの配点は自動的に最新の配点設定値に更新されますが、部分点あり(△)と採点したデータの採点は変更作業の対象外となります。採点後に配点を変更した場合は、どうか、ご注意ください。

続けて、次のメッセージが表示されます。

「はい」をクリックして、配点及び観点別評価の区分の入力を終了します。

画面は、次の状態になります。

これで採点する準備ができました!

< 戻る >

11.採点

採点は数字キーを押し下げして行います。

・正解の場合:半角数字の 1 を入力

解答には、採点記号と配点、観点別評価の区分が赤で描画され、
グリッドコントロールには、配点設定に基づいた得点がセットされ、
フォーカスは自動的に次の生徒の解答へ移動します。

【全員を正解とする場合】

上のアイコンをクリックすると全員正解で採点できます。

・不正解の場合:半角数字の 2 を入力

解答には、採点記号と観点別評価の区分が青で描画され、
グリッドコントロールには、0(ゼロ)がセットされ、
フォーカスは自動的に次の生徒の解答へ移動します。

【全員を不正解とする場合】

上のアイコンをクリックすると全員不正解で採点できます。

・部分点がある場合:半角数字の 3 を入力

部分点を入力するインプットクエリーが表示されますので、部分点を半角数字で入力してください。

入力後、OK をクリックしてください。
解答には、採点記号と与えた部分点及び観点別評価の区分がオレンジ色で描画され、
グリッドコントロールには、部分点が「負の値」としてセットされ、
フォーカスは自動的に次の生徒の解答へ移動します。

グリッドコントロールには、部分点が「負の値」としてセットされますが、マイナス記号は「部分点あり」のフラグとして利用しています(プログラムのバグではありませんのでご注意ください)。得点合計は絶対値で計算していますので、表示がマイナス記号付きであっても問題ありません。

・採点を取り消す場合:半角数字の 4 を入力

出席番号1番の生徒の採点を取り消したところ

消去チェックボックスをチェックすると、まとめて採点を取り消すことができるようになります。

・1行分(=1設問分)をまとめて取り消し

1設問分の採点を受験者全員について取り消します。

・全採点の取り消し

全設問の採点結果を取り消します。

採点結果をまとめて取り消した場合、元に戻すことは出来ません。

・フォーカスの移動

フォーカスは左右の矢印キーで移動させることもできます。上下の矢印キーは採点対象の設問を変更することになりますので、押し間違えないよう、十分にご注意ください。

・採点結果の保存

入力した内容はフォーカスが移動する時、自動保存のチェックボックスにチェックが入っていれば自動保存されます。ですので、特別の理由がない限り、自動保存のチェックは外さないでください。

手動で保存する場合は、次のアイコンをクリックしてください。

クリックすると、現在アクティブな行(採点中の設問)の採点データが一括して保存されます。

全てのデータを手動で保存する場合は、次のアイコンをクリックしてください。

メモリー上に全画像データを読み出して処理している場合は、JSONファイルに採点結果を書き込むだけなので、保存は一瞬で完了しますが、採点結果を直接画像に書き込む低速モードで運用している場合には、全画像のすべての採点結果を書き直すことになりますので、処理の実行が完了するまでに相当の時間が必要となります。設問数と受験者数により必要な時間は異なってきますが、設問数50問前後、受験者数40名前後の場合、数十分程度必要です。低速モードで運用される場合、全画像の再生成処理の実行はやむを得ない事情で、その処理が必要になった場合のみ、時間に余裕を持って実行してください。

< 戻る >

12.成績一覧表及び正答率一覧表の作成と印刷

採点が完了したら、まず、成績一覧表及び正答率一覧表を作成してください。この作業を行わないと返却用答案が作成できません。

【成績の計算が未実行の場合に表示されるメッセージ】

成績一覧表を作成するには、まず、配点チェックボックスをチェックします。

配点チェックボックスをチェックしたところ(グレーアウトします)

グリッドコントロールは配点及び観点別評価の区分の入力画面に変わります。この状態で、次のアイコンをクリックしてください。

・成績一覧表の作成

クリックすると成績一覧表を作成・表示します。

成績一覧表が表示されます。

受験者の氏名を表示する機能は、このバージョンにはありません。

既存の採点データがある場合、上書き確認のメッセージが表示されます。

・正答率一覧表の作成

クリックすると正答率一覧表を作成・表示します。

正答率一覧表が表示されます。

・2枚目がある場合

出力サイズはデフォルトA4・縦を設定してあります。なので、受験者数が多い場合は2枚以上になります。▶ アイコンをクリックすれば次ページを、◀ アイコンをクリックすれば前のページをそれぞれ表示できます。

・成績一覧表及び正答率一覧表の印刷

成績一覧表及び正答率一覧表を印刷するには、次のアイコンをクリックしてください。

クリックすると、成績一覧表 or 正答率一覧表を印刷できます。

出力は「A4・縦」がデフォルト設定です。

出力先のプリンタを選んで、OKをクリックしてください。

PDF24は「PDFのあらゆる作業を無料で簡単にできるツール」です。オンライン版(PDF24 Tools)と、PCにインストールして使うオフライン版(PDF24 Creator)の2種類があり、私はオフライン版をインストールしています。プリンタのドライバの不具合で直接印刷できない場合、PDF化したデータをPDF24の印刷機能を利用して印刷すると、MS_Reader V3 から印刷できなかったプリンタにも出力できました。

【追記_20260530】

各一覧表のヘッダー部に処理対象フォルダの名称と出力時の西暦年月日と時刻を表示できるようにコードを修正しました。

・採点モードに戻るには?

配点等の入力画面が表示されている場合、配点チェックボックスはグレーアウトしてクリックできない設定としてあります(事故を防止するためのプログラムの仕様でバグではありません)。

採点モードに戻るには、次のアイコンをクリックして配点設定を上書き保存してください。

クリックすると「配点と観点別評価の区分を保存して終了」するアイコン

クリックすると上書き確認のメッセージが表示されます。

「はい」をクリックしてください。

さらに確認のメッセージが表示されます。

採点モードに戻ります。

「はい」をクリックしてください。

< 戻る >

13.返却用答案の作成と印刷

成績一覧表の作成を実行すると、返却用答案の作成が可能になります。

まず、調整チェックボックスをチェックして、合計点表示用のフォントサイズを指定してください。

合計点等のフォントサイズを指定してください。

フォントサイズを変更した場合、次のメッセージが表示されます。

「はい」をクリックしてください。

× ボタンをクリックして設定 Window を閉じてください。

「閉じる」ボタンは Window 右下にあります。

「返却用答案の表示」ボタンをクリックしてください。

「返却用答案の表示」ボタン
OK をクリックしてください。

画像上の任意の位置をクリックしてください。合計点等の表示サンプルラベルが描画されます。

ラベル上をクリックしてドラッグすれば表示位置を微調整できます。
よろしければラベルの右横にある「この位置に決定」ボタンをクリックしてください。
「はい」をクリックしてください。

印刷プレビューが表示されます。

次のアイコンをクリックすると返却用答案を印刷できます。

「返却用答案の印刷」ボタン

確認のメッセージが表示されます。

適切なボタンを選択してクリックしてください。

プリンタの設定ダイアログが表示されます。出力先等を選択して OK をクリックしてください。

用紙サイズは、解答用紙サイズにかかわらず A4 がデフォルト設定です。

印刷(枚数)の確認メッセージが表示されます。

印刷内容を選び、適切なボタンをクリックしてください。

「はい」をクリックした場合、全員分の返却用答案が出力先に送られます。出力完了時に次のメッセージが表示されます。

「印刷設定の保存」確認メッセージが表示されます。

「はい」をクリックすると現在の印刷設定が保存されます。

さらに確認メッセージが表示されます。

OK をクリックしてください。

印刷(枚数)の確認メッセージで「いいえ」をクリックすれば、番号を指定して個別に返却用答案を出力できます。

個別に出力する場合は「いいえ」をクリックしてください。

出力したい返却用答案の出席番号を指定します。

この解説では「マークシート採点」を併用する方法は紹介しておりません。
マークシートと手書きを併用する試験の実施方法は次回の記事で解説します。

出力が完了すると、次のメッセージが表示されます。

OK をクリックしてください。

この後、全員分を印刷する場合と同様に、現在の印刷設定を保存するか・どうか、確認するメッセージが表示されます。

< 戻る >

14.手書き答案採点設定の保存

次のアイコンをクリックすると、現在、採点処理を行っている手書き答案の採点設定を保存できます。

「手書き答案の採点設定を保存」します。

設定の保存完了の確認メッセージが表示されます。

OK をクリックしてください。

< 戻る >

15.まとめ

今回の記事では、マークシートリーダー MS_Reader V3 に付属の「手書き答案のデジタル採点機能」について解説しました。

以下、MS_Reader V3 に付属の「手書き答案のデジタル採点機能」でできることのまとめです。

(1)横書き・縦書きいずれの形式の解答用紙のデジタル採点が可能
(2)成績一覧表及び正答率一覧表の作成・印刷が可能
(3)返却用答案の作成・印刷が可能

< 戻る >

16.お願いとお断り

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

< 戻る >

MS_Reader V3 の使い方(マークシートリーダー編)

この Blog で先にご紹介した完全無料でお使いいただける、「手書き答案のデジタル採点機能付き」マークシートリーダー MS_Reader V3 の使い方のご案内です。

MS_Reader V3 の付属の機能を利用した、手書き答案のデジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

MS_Reader V3 の付属の機能を利用した、マークシートと手書き答案の併用型デジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

【もくじ】

1.マークシートの準備
2.マークシートの印刷
3.マークシート読み取り前の確認
4.マークシートをスキャナーでJpeg画像化
5.スキャンしたマークシート画像の保存先
6.マークシート読み取りと採点のための準備
7.処理に必要なメモリー容量の確認
8.採点対象の読取専用画像が入ったフォルダを選択
9.特徴点の検出
10.マークシート情報の設定
11.マークシート設定の複写
12.マーク読み取りの実行と確認
13.音声読み上げ
14.正解・配点・観点別評価の区分の入力と保存
15.成績一覧表、正答率一覧表の作成と印刷
16.採点結果通知シートの作成と印刷
17.まとめ
18.お願いとお断り

1.マークシートの準備

試験で使用するマークシートを準備します。MS_Reader V3 で使用するマークシートには特徴点が必要です。マークシートの左上隅の任意の位置に ■■■(ドットを3つ)ご用意ください。

プログラム自体は、特徴点がなくても、スキャンした画像の左上を座標原点 (0, 0) として動作するように組んでありますが、動作テスト時に一部データの読み込みに問題が発生することがありました。ですので、誤動作を極力防止するためにも特徴点は必ずご用意いただけますよう、お願い申し上げます。

【ご注意願います】

特徴点をご用意いただいた場合でも、MS_Reader V3 が常に意図した通り、完全に動作することは保証できません。MS_Reader V3 を使用される方の完全自己責任の下、十分な動作テストを行っていただいた上で、マークシートの読み込み性能に納得していただける場合のみ、MS_Reader V3 はお使いいただけます。悪しからず、ご了承ください。

上図のような位置に、上図のような大きさで ■■■ をご用意ください。

MS_Reader V3 に同梱した Mark Sheet Maker をご使用いただければ、上図のようなマークシートをどなたでも簡単に作成できる(?)と思います。(もし、難しかったらごめんなさい)

< 戻る >

2.マークシートの印刷

マークシートの印刷には、輪転機ではなく、必ずインクジェットプリンターを使用してください。

高性能化が進む輪転機ですが、マークがどうしても濃く印刷されてしまい、マーク読み取りの際に問題が発生することがあります。タイトル及び特徴点以外の、マーク・罫線部分の印刷の濃さは、「実際の運用でご使用になるスキャナーで読み取ったマークシート画像のマーク内の数字がモニター画面上で読み取れ、かつ、その画像を実際にご使用になるプリンタで印刷した場合に、マーク内の数字が何であるか、ある程度判別可能な濃さ」としてください。

※ 上記マークシートメーカーの記事に、印刷濃度とスキャンした画像の比較サンプルがあります。

< 戻る >

3.マークシート読み取り前の確認

スキャナーでマークシートを読み取る前に、次の事項について確認し、必要な作業を行ってください。

(1)欠席者の位置に白紙のマークシートを挿入

(2)マークシートに付着した消しゴムの屑を除去

塵も積もれば・・・ナントカで、消しゴムの屑が付いたままのマークシートをスキャナーで何百枚も処理すると、スキャナーの光学読取装置部分が大変なコトになります。ですので、マークシートに付着した消しゴムの屑は出来る限り除去してからスキャン作業を行ってください。

(3)マークシートが出席番号番号「昇順」に並んでいることを確認

マークシートが出席番号(もしくは受験番号)の昇順(=スキャナーで読み込む順)に並んでいることを最低2回は確認してください。また、その際、上下の逆転がないことも入念にご確認ください。

< 戻る >

4.マークシートをスキャナーでJpeg画像化

マークシートは解像度 200 dpi、カラー、Jpeg 画像を指定してスキャンしてください。

必要以上に解像度を高くしても良いことは1つもありません。200 dpi で十分な精度が得られます。また、必ずカラー画像としてスキャンしてください(スキャナー側では二値化処理しないでください)。

一般的に使用されている複合機のスキャナーでスキャンした PDF ファイルは、Jpeg 画像のコンテナのような形式で作成されていることが多いと聞き、MS_Reader V3 には、そのような PDF ファイルであった場合、Jpeg 画像を抽出する機能も搭載しましたが、様々なスキャナーで作成されたPDF ファイルを入手し、必要十分な動作テストを行うことは私の環境では不可能でありますので、この PDF ファイルから Jpeg 画像を抽出しての処理は、その可否をご確認の上、可の場合でも必要十分な動作テストを実施後にご利用ください。

なお、筆者の職場にあります E 社製複合機のスキャナーで解像度 200 dpi、カラー、PDF を指定して読み込んだデータを用いた動作検証を行い、プログラムが意図した通りに動作することを確認しましたことを申し添えます。

< 戻る >

5.スキャンしたマークシート画像の保存先

スキャンした画像は、MS_Reader_V3.exe と同じフォルダにある ScanData フォルダ内に任意の名称のフォルダを作成し、そこへ保存してください。各 Jpeg 画像ファイルの名称は、この後の読み取り処理専用画像作成の際に一括して変更しますので、どのような名称でもかまいませんが、ファイルの並びが出席番号(=受験番号)順となるような名称である必要があります。

Jpeg 画像を保存するフォルダの名称は、実際には「年度・考査名・教科科目名・クラス名」を組み合わせて作成すると良いと思います。

例:R8_考査①_物理基礎_2A

【重要】フォルダ名には「 – 」や「 ー 」を用いないでください。

フォルダ名に「 – 」や「 ー 」が含まれていた場合、読み取り専用画像を保存するフォルダ名は、スキャナーで読み取った画像を保存したフォルダ名中の「 – 」や「 ー 」を自動的に「 _ 」に変換した名称で作成されます(文字列処理の中で「 – 」を分割に利用しているためです)。この点につきましては、どうか十分にご注意願います。

また、スキャナーで読み取った画像を入れるフォルダの配置については、次の例をご参照ください。

< 戻る >

6.マークシート読み取りと採点のための準備

MS_Reader V3 を起動し、マークシート読み取りと採点のための準備を行います。

MS_Reader_V3 フォルダ内にある上のアイコンをダブルクリックして起動します。

ダウンロード直後の初回起動時に、次のような Windows Defender SmartScreen による警告画面が表示されると思います。「詳細情報」をクリックし、次に表示される画面の「実行」をクリックしてプログラムを起動してください。

ダウンロードされたファイルに「 Zone.Identifier 」という「ゾーン識別子」が付加されていると、Windows はこの識別子を見て「インターネット経由で取得されたファイル」と判断し、SmartScreen が警告を表示します。

MS_Reader V3 が起動したら、まず、読み取り専用画像を作成します。次の図に示す「矢印がフォルダ内へ入っている」アイコンをクリックしてください。

?マークをクリックすると操作ガイド PDF が表示されます。

「選択」をクリックして、スキャナーで読み取った Jpeg 画像を保存したフォルダを選択します。

「選択」をクリック
Jpeg 画像を保存した「フォルダ」を選択し、Ok をクリックしてください。

次の図に示すように①→②→③の順で操作してください。

①で、必要があれば回転方向を指定します。
②で、画像のリサイズ指定が可能です(A4のマークシートであれば「なし」か「自動」を指定)
③をクリックすると読取専用画像を保存するフォルダを指定するダイアログが表示されます。

【ご注意】

②でリサイズする際、サイズを指定した場合は必ずその数値をメモしてください。他のクラスも同じ設定で読み取りと採点を行うためには、全クラス同じサイズで画像を変換する必要があります。

ProcData フォルダ内に読取専用画像を保存するフォルダが自動作成されますので、このフォルダをクリックして指定し、その後 Ok をクリックしてください。

「変換実行」をクリックしてください。

「マークシート」と「手書き答案の採点」を併用する場合は「はい」、併用しない場合は「いいえ」をクリックしてください。

確認メッセージが表示された場合は、Ok をクリックしてください。

他のクラスも連続して処理する場合は「画面の初期化」を、読取専用画像の作成作業を終了する場合は「終了」をクリックしてください。

< 戻る >

7.処理に必要なメモリー容量の確認

図の「矢印がフォルダから出ている」アイコンをクリックしてください。

次の案内が表示されます。

上記案内に表示している画像1枚あたりのサイズは、「メモリ上に展開されたOpenCV IplImageの生ピクセルデータサイズ」で計算しています。こちらのメモリ検証を参考に、お使いの PC で快適に作業できる答案枚数をご確認ください。

プログラムは、画像をメモリ上に展開しなくても動作するように作成してありますが、都度画像を読み込んで処理する場合、動作速度(=ユーザー体験)は大変遅くなります。全画像をメモリー上に展開できる作業環境で MS_Reader V3 をお使いいただけますよう、伏してお願い申し上げます。

プログラム作成の流れで言うと、非メモリー展開モードが当初作成した「都度画像を読み込み、採点結果を都度書込みという(流れの)オリジナル」で、それではどうしても動作速度的に満足できないと(新規に手書き答案採点プログラムを書き直した意味がない・旧版の AC_Reader に比較して明らかに良くなっている・進化していることが自分自身の中での絶対的評価基準でした)、このユーザー体験の改善は「やるか・やらないか」の二択ですから「それはやるしかないだろ」ってコトで、もう一度イチから出直して新たに追加したのが「全画像をメモリ上に展開し、読み込みはそこから行い、採点結果は画像に書き込まず JSON ファイルに保存、それが必要な時は JSON ファイルから読み出して画像上に描画する」というメモリー展開モード(=自分的には、新しく採用したアルゴリズム:現実世界では、これが業界標準のようですが・・・)です。

上記の理由から、MS_Reader V3 のプログラムは、状況に応じて2つのモードを使い分けて動作するように出来ています。・・・ 出来ているはずなのですが、メモリー展開モードを追加してからは、ほとんど、この環境でしか動作テストを行っておりませんので、もしかしたら、非メモリー展開モードでの実行時に何らかの予期しない不具合が発生するかもしれません。

ただし、非メモリー展開モードは、私ではない善意の第三者が、高校現場において、定期考査の採点に実際に使用し、致命的な問題がないことをリアル採点現場で確認済みです(この際、判明した問題点はすべて修正・改良しました)。

< 戻る >

8.採点対象の読取専用画像が入ったフォルダを選択

解答用紙のサイズを選択し、受験者数を入力してメモリー展開モードでの作業の可否を確認後、Ok をクリックすると「フォルダーの選択」ダイアログが表示されます。

採点対象の読取専用画像が入ったフォルダを選択

ここではファイルではなく、「フォルダー」を選択することに注意してください。上の例で言えば「 01_MarkSheet 」フォルダをクリックして選択 ⇨ 「フォルダーの選択」ボタンをクリックという流れになります。

案内が表示された場合は、Ok をクリックします。

< 戻る >

9.特徴点を検出

今回の冒頭でお示ししたように、MS_Reader V3 では、マークシートのマークを個別に切り出す際の座標原点として、マークシート(解答用紙)の左上隅に印刷した ■■■ を特徴点として利用します。これがなくても動作するように設計しましたが、マークシートだけでなく、手書き答案の採点プログラムでもこれがあることをあくまでも大前提としておりますので、MS_Reader V3 で処理する解答用紙の左上隅には必ず ■■■ を設置していただけますようお願い申し上げます。

解答用紙は通常ワープロソフトで作成されることが多いと思いますが、10.5~14ポイントくらいの大きさで ■■■ を入力していただければよろしいかと思います。

前作と異なり、MS_Reader V3 では、特徴点を自動認識するよう、アルゴリズムを改善しました。以下、特徴点検出方法です。

画面右上の操作案内の表示(✅Info )がチェックされている場合

■■■ の下に Thresh とある表示の右側の閾値表示用 ComboBox が「空欄」の状態で、その閾値表示用 ComboBox の右にある特徴点自動検出ボタンをクリックしてください。

特徴点検出手続きが実行され、プログラムは画像の左上付近に捜索範囲を限定して ■■■ を探します。プログラムは検出閾値を次々に変え、最も安定して利用できると思われる最適な値を見い出します。最適な値が見つかったら場合、その値付近でさらに数回試行し、安定動作(検出)出来た場合は検出動作を停止して特徴点を赤枠で囲み、ユーザーに確認を求めます。

特徴点検出作業中、画面左下に進捗状況が表示されます。

【特徴点が検出された場合】

「はい」をクリックしてください。
(背景のマークシート画像がぼやけているのは検出手続き中でガウスぼかしを利用しているためです)

【特徴点が検出されなかった場合】

特徴点が検出されなかった場合は、次のメッセージが表示されます。

Ok をクリックしてください。

上記いずれの場合でも次の確認メッセージが表示されます。マークシートのみの採点作業及びマークシートと手書き併用型の採点作業を行う場合は「はい」を、手書き答案の採点作業のみを行う場合は「いいえ」をクリックしてください。

続けて、手書き答案の採点作業を行うかどうかの確認メッセージが表示されます。

適切な方のボタンをクリックしてください。

< 戻る >

10.マークシート情報の設定

特徴点が検出できたら、次にマークシートの情報をイニシャライズファイルに登録します。

各ブロックの 幅と高さは同じ でなければなりません。
また、設問番号部分はブロックに入れません。

MS_Reader V3 では、上の図のようにマークシートのマーク欄を群(ブロック)に分け、さらにその群(ブロック)を行と列に分割してマーク単体を切り出し、その部分の濃度を計測してマークの有無を判定しています。前作までのマークシートリーダーでは白黒二値化後の塗り面積計算でマークの有無を判定していましたが、これを濃度を計測しての判定に変更することで、次のような点で判定精度を向上させることが出来たのではないかと考えます(比較実験は行っておりません)。

(1)薄いマークや濃淡のあるマークの検出精度の向上
(2)スキャナーの機種の違いに強くなる(採点現場では統一されていないことの方が多い)
(3)「消え残り」と「真のマーク」を区別しやすくなる
(4)紙の汚れ・印刷のムラに強くなる(マーク領域全体の平均をとることで、局所的なノイズの影響が統計的に薄まる)
(5)パラメータ調整が容易になる(白黒二値化では、閾値を1動かしただけで検出結果が逆転することがあり、感覚的に調整しにくい)

高校における実際の定期試験の採点現場では、スキャン環境も受験者の筆記具も統一されていないのが当然で、白黒二値化から濃度計測への判定アルゴリズムの変更は、判定精度と運用安定性の両面で大きな改善につながったと思います。

まず、群(ブロック)数を入力します。

次のメッセージが表示されます。

自動検出は「おまけ・お遊び」程度にお考えください。

スキャナーで読み込んだ画像の罫線の状態は粗めになっていることが多く、余程良好な状態でないと群(ブロック)ごとの罫線枠の自動検出は成功しないと思われます。

【参考:罫線枠の自動検出に成功した場合】

罫線の状態が良ければ、罫線枠の自動検出に成功します。

ちなみに上の場合のパラメータ設定は「10」です。こちらを先に設定してから左隣の「マーク欄の自動検出」ボタンをクリックしてください。群(ブロック)数設定時の操作ミスを防止する観点から、敢えてコントロールの位置を自動検出ボタンの「右」側にしてあります。

閾値は「10」程度でお試しください。

自動検出に成功した場合は、次のように操作してください。まず、検出できた座標を保存します。

▯1 ボタンをクリックしてください。

2つ目に自動検出された罫線枠が自動的に選択されます。2つ目に検出できた座標を保存します。

▯2 ボタンをクリックしてください。

設定した群(ブロック)数回、同様に操作します。これ以降の設定方法は自動検出機能を利用しなかった場合と同じです。

なお、罫線枠(マーク欄)の自動検出には「適応的二値化」を用いています。これにより紙の折り目跡などによる画像の濃淡の違いに強くなり、より安定して検出できるようにはなっていると思うのですが、私の行った試行では誠に残念ながら上手く検出できることの方が少なかったです(検出精度を数値的に評価するような実験は行っておりません)。・・・ですので、ここでは罫線枠(マーク欄)の自動検出機能は利用せず、手動でマーク欄の情報を設定する方法を説明致します。

罫線枠(マーク欄)の自動検出機能を使用しない場合は「いいえ」をクリックします。

次のメッセージが表示された場合は、よく読んでから Ok をクリック。

マウスのカーソルが大きな「+」になります。範囲の設定は必ず読み取りブロックと表現している罫線枠(マーク欄)の左上から右下へドラッグするかたちで行ってください。まず、+になったカーソルを読み取り範囲の左上位置にポイントします。

ポイントしたらカーソルを動かさずに、そこでクリックして右下方向へドラッグします。画面はマウスの動きに追従して自動的にスクロールするはずです。

実際には、右下隅でぴったり罫線枠に合わせるのは、なかなか難しいと思います。そこで、少し大きく範囲指定して微調整できるようにアルゴリズムを改善しました。

幅・高さともやや大きめに範囲指定したところ。この後、微調整します。

微調整は Shift キーを押したまま、上下左右の矢印キーを押し下げして行います。

上の状態を例に説明すると、

Shift + 左向きの矢印キー押し下げで、幅が左へ小さくなります。
Shift + 上向きの矢印キー押し下げで、高さが上へ小さくなります。

また、MS_Reader V3 では、赤い点線で範囲指定した枠そのものも移動できるようにしました。

上下左右の矢印キーを押し下げすると、1回押し下げにつき1ピクセルずつ、赤い点線で範囲指定した枠そのものが押し下げた矢印キーの方向へ移動します。

1つめの範囲を指定出来たら、その座標を保存します。

▯1 ボタンをクリックしてください。

次に2つ目の読み取り範囲の罫線枠(マーク欄)を指定しますが、ここにも改良を加えました。MS_Reader V3 で使用するマークシートにおいて各マーク欄ブロックの幅と高さは全て同じという大前提ですので、2つ目以降のブロックは特徴点からの距離が変化するのみで、その大きさは変わりません。そこで、1つ目で指定した範囲をそのまま2つ目のブロックへドラッグ&ドロップ出来るようにしました。作業内容は、次のようなイメージです。

選択した範囲の内部をクリックし、そのまま図の矢印方向へドラッグします。
選択範囲を2つ目のブロック位置へ移動します。

マウスはなるべく水平方向へ移動するよう操作してください。上や下方向へマウスを動かすと画面がスクロールしてしまいます。ご注意ください。

ドロップ位置での微調整は、上で説明した通りの方法で行うことができます。

2つ目の罫線枠(マーク欄)の座標を保存します。

▯2 ボタンをクリックしてください。

同じ操作を、ブロックの数だけ繰り返し、すべてのブロックの座標を取得・保存します。

次に1ブロックあたりの行数、1解答欄(=設問)あたりの選択肢数他を指定します。

1ブロックあたりの行数を設定します。
ComboBox の選択肢から適切な値を選択してください。

次に1解答欄(=設問)あたりの選択肢数を指定します。

選択肢数は8
ComboBox の選択肢から適切な値を選択してください。

用紙のサイズと向きも ComboBox の選択肢から選んで指定します。最後の項目は縮小率(%)です。

画像のリサイズが「なし」or「自動」の場合は縮小率は「100」を指定し、任意のサイズを指定した場合は指定したサイズ(%)の数値を指定します。(下の図を参照してください)

次に設問数を設定します。100設問設定可能なマークシートであっても実際に使用しているのが 74 設問分であれば設問数には 74 を設定してください。

設問数を設定

次に選択肢の始まりの番号を指定します(多くの場合、それは「1」だと思いますが、高等学校の教科「情報」のように選択肢の始まりの番号が「0(ゼロ)」である場合があります。ご注意ください)。

選択肢の始まりの番号が1なら1を指定

次に複数マークの扱いを指定します。

【チェックなしの場合】

複数マークがあった場合は「99」と表示します。
(99 は複数マークであることを示すフラグです)

【チェックありの場合】

複数マークがあった場合、そのまま読み取ってカンマ区切りで表示します。

【ご注意ください】

複数マークを許可した場合は、読み取り結果を CSV ファイルに出力して、採点等はこの CSV ファイルを利用した別アプリケーションで行っていただく必要があります。MS_Reader V3 付属の採点システムでは複数マークを許可した場合の採点を想定しておりません。ですので、複数マークを許可した場合の MS_Reader V3 の機能は、マークの読み取り& CSV 出力までとなります。

最後に、マークシート情報が正しく設定されていることを目視して確認します。

目視チェックの実行ボタンをクリックするとマークの切り出し枠が赤い線で描画されます。
画面をスクロールしてマークシート情報が正しく設定されているかどうか、確認してください。

最後に設定内容をイニシャライズファイルに保存します。

「マークシート設定の保存」ボタンをクリックしてください。

< 戻る >

11.マークシート設定の複写

【追記:実際の採点現場で生じた問題と対応方法】

同じプリンターで印刷し、同じスキャナーで読み取った解答用紙画像であっても、特徴点を認識する際の閾値がブレてしまい、複数クラスに同じマークシート設定を適用できないケースがあることを実際の採点現場での運用時に確認しました。

このような場合は、ProcData フォルダ内に複写した settings.ini を削除していただき、ご面倒でも特徴点の認識から作業を行っていただけますよう、お願い申し上げます。

以下、オリジナルの記事です。

複数クラスに同じマークシート設定を適用する場合は、作成したマークシート設定を複写できます。

複写する必要がなければ「いいえ」をクリックします。

【「はい」を選んだ場合】

例:1A のマークシート設定を 1B, 1C に複写する場合、Ctrl キーを押しながら選択します。

「フォルダの選択」ボタンをクリックすると、既存のマークシート設定が複写先のクラスのフォルダへコピーされます。

既存のファイルがある場合、上書き確認のメッセージが表示されます。

< 戻る >

12.マーク読み取りの実行と読み取り結果の確認

これでマークシートを読み取る準備が出来ました。たくさん準備を行ってきましたが、読み取りはあっけないくらい 走召 高速です。初めて実行した時、機械が壊れているんじゃないかと思ったくらいです。

「①実行」ラジオボタンをチェックし、「②マーク読み取りを実行」ボタンをクリックするだけです。

私の環境では、1設問あたり8選択肢、100 設問、40 Sheet分、すなわち 32000 マークを

1.011 秒で読んでいます。もちろん、読み取りミスはありません。

マークの有無の判定アルゴリズムは3種類用意してあります。

・Area:面積法
・Intensity:濃度法
・Hybrid:上記2つの方法の組合せ

何度も試行して確認しましたが、濃度法一択でよろしいのではないかと思われます。

続いて、読み取り結果の確認です。

例えば 50 設問用のマークシートを使用し、実際の設問数は 46 であった場合、47, 48, 49, 50 は間違えてマークした生徒を除き、受験者全員が空欄になっています。このような空欄は最初からチェックの対象としないように設定できます。また、0を指定すると、そのブロック内のすべての回答欄がチェック対象となります。

第2ブロックは、これ以降の設問をチェックから除外します。

チェックの設定は保存しておき、同じ試験を実施した他のクラスにも適用できます。

チェックを実行すると「空欄:999」と判定したマークと、「複数マーク:99」と判定したマーク位置でチェックが停止します。目視して該当マーク部分の読み取り判定結果をチェックしてください。

【空欄と判定した場合】※ 空欄のフラグは 999

【複数マークありと判定した場合】※ 複数マークのフラグは 99

複数マークを許可した場合は、1, 2 と表示され、チェック対象ではなくなります。

塗りつぶしが薄すぎて空欄判定になっていたり、消したマークの消し方が不十分で複数マークありの判定になっている場合は、読み取り結果を採点者の判断で正しく修正してください。

すべてのマークをチェックしたら、保存ボタンをクリックして、読み取り結果を保存します。

既存のファイルがある場合、上書き確認のメッセージが表示されます。

採点の「確定」を確認するメッセージです。採点結果を確定しないと合計点や採点結果通知シートの作成は行えません。また、採点結果を確定した場合は、次回当該クラスのフォルダを指定して開いた際、採点処理をしなくても合計点他の計算や各種資料の印刷が可能になります。

事故防止の観点から、どんな場合でも、処理は必ず「合計点を計算」⇨「採点結果通知シートの作成・印刷」の順で実行しなければならないように設定してあります。それが MS_Reader V3 の仕様であると御理解いただけますよう、お願い申し上げます。

< 戻る >

13.音声読み上げ

グリッドコントロールの任意の箇所をクリックしてアクティブにした位置から音声による読み取り結果の読み上げが可能です。マークシートの実際のマークと読み取り結果の照合や、入力した正解が正しいかどうか等の確認にご使用ください。読み上げ速度も設定可能です。

< 戻る >

14.正解・配点・観点別評価の区分の入力と保存

採点を実行する前に、採点に必要な情報を入力し、保存します。

「配点」をチェックすると入力が可能になります。

(マークシート用採点情報の入力実行時は)「編集」をチェックしないと値を入力できません(デフォルト設定で「チェックあり」になっています)。

合計点等は自動計算され、表の上に表示されます。

必要なすべての値を入力したら採点に必要な情報を保存します。上の図にある保存ボタンをクリックします(既存のファイルがある場合、上書き確認のメッセージが表示されます)。

【採点情報の複写方法】

マークシートの設定同様、採点情報も同一の試験を行った他のクラスに複写して利用できます。

複数クラスに複写する場合は、Ctrl キーを押しながらフォルダをクリックしてください。

1A の採点情報を 1B, 1C に複写する場合の例です。

< 戻る >

15.成績一覧表、正答率一覧表の作成と印刷

旧版では別プログラムだった成績一覧表の作成を MS_Reader 内できるようにしました。また、併せて旧版では出来なかった正答率一覧表の作成や印刷も可能としました。さらに、旧版と比較した場合、いずれの処理も高速に実行できます。

成績一覧表のサンプルです(成績はデタラメです)。

正答率一覧表のサンプルです(こちらも適当です)。

【追記_20260530】

各一覧表のヘッダー部に処理対象フォルダの名称と出力時の西暦年月日と時刻を表示できるようにコードを修正しました。

< 戻る >

16.採点結果通知シートの作成と印刷

受験者に配布する個別採点結果通知シートの作成と印刷が可能です。マークシート画像上に採点結果、配点、観点別評価の区分、正答・正解を描画し、さらに任意の位置に合計点他を描画、採点結果通知シートとして印刷できます。

「調整」をチェックして作業を開始します。

赤い枠内にチェックを入れるとよろしいのではないか?思います。

【印刷プレビューの表示】

印刷プレビュー

【印刷の実行】

○:正答・正解 △:部分点有り ×:誤答・不正解 K1, K2:観点別評価の区分

【合計点表示例】

合計点他は任意の位置に

【採点結果通知シートのサンプル】

不正解であった場合は、正解マークの上に正解の選択肢番号を描画

< 戻る >

17.まとめ

・名称:MS_Reader V3(完全無料のマークシートリーダー)
・超高速読取り:My PC では 約 32000 マーク/秒
・極軽量:本体わずか4MB
・手書き答案のデジタル採点機能付き(横書き&縦書き両形式答案に対応)
・観点別評価に対応
・成績及び正答率一覧表、採点・配点・正解マーク&得点合計&順位付き採点結果通知の作成&印刷
・CSV出力
・音声読み上げ
・答案画像の傾き補正
・マークシート作成ソフトも同梱
・動作保証一切なし、サポートなし、バグあるかも?
・完全自己責任のもとでのみ使用可能です。

< 戻る >

18.お願いとお断り

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

< 戻る >

MS_Reader V3(完全無料のマークシートリーダー・超高速読取り・極軽量本体わずか5MB・手書き答案のデジタル採点機能付き=横書き&縦書き両形式答案に対応・AI自動採点機能を搭載・観点別評価対応・成績一覧表、正答率一覧表、採点・配点・正解マーク&得点合計&順位付き返却用答案の作成と印刷・CSV出力・音声読み上げ・答案画像の傾き補正・マークシート作成ソフトも同梱・動作保証一切なし・サポートなし・バグあるかも?・完全自己責任のもとでのみご使用いただけます)

1クラス分のマークシート読み取りに5分を要したバージョン1、Python 用の OpenCV を使用してやや高速化できた反面、容量が激増、WindowsのDLLチェックのため起動が遅く、動作が重かったバージョン2、このままでは死んでも死にきれないと 2025 年秋から Windows 用 OpenCV を利用したバージョン3の制作に着手。世に誰一人その完成を待ってくださる方もいらっしゃいませんが、2026年、春、ついに出来ました☆ 僕の夢のカタチ MS_Reader V3 です。

【追記】(20260705)

答案画像を保存したフォルダまでの Path 文字列中に「-(ハイフン)」を使用しないでください。Path 文字列中に「-(ハイフン)」があった場合は、警告のメッセージを提示して処理を中止します。

ただし、ハイフン類似文字は検出しません(U+002D に加えて U+2011 非分離ハイフン、U+2010 ハイフン、U+FF0D 全角ハイフン、U+2212 マイナス記号 など)ので、ハイフンに類する文字は、Path 文字列中には使用しないようにご注意願います。

【もくじ】

1.推奨動作環境
2.サンプル画面
3.プログラムのダウンロード
4.お願いとお断り

1.推奨動作環境

長すぎるタイトルに書けなかったこともご承知おきください。以下、推奨動作環境です。

OS:開発したのは Windows11 25H2 です(Windows11以外では動作検証しておりません)。
CPU:はやければ、はやいほどイイです!
GPU:深層学習とか、興味ありますし、したいけど、出来ません!!!・・・ので不要です。
搭載メモリ:16GB以上を推奨(8GBの場合、B4サイズ以上の答案100枚対応が困難な見込み)
画面解像度:1366×768を推奨(Version 3.1.0 より超高解像度ディスプレイにも対応済み)
マウス:必須です。画面をタッチして使うことを一切想定しておりません。
特徴点:Version 3.1.0 より特徴点 ■■■ がない場合でも動作するよう、仕様を変更しました。

2.サンプル画面

マークシートリーダー実行時の画面(旧バージョンのスクリーンショットです)
手書き(横書き)答案採点時の画面(旧バージョンのスクリーンショットです)
手書き(縦書き)答案採点実行時の画面(旧バージョンのスクリーンショットです)

3.プログラムのダウンロード

今回の記事でご紹介した マークシートリーダー(マークシートメーカーも同梱)のプログラム一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の Readme.txt をご覧ください。なお、本ソフトウェアは OpenCV 2.4.13 (3-clause BSD License)及び Delphi-OpenCV を使用しています。OpenCV 2.4.13 のライセンスの詳細については、マークシートリーダーのフォルダに同梱した LICENSE 文書をお読みください。また、Delphi-OpenCV のライセンスの詳細については、こちらもマークシートリーダーのフォルダに同梱した MPL-1.1.txt をお読みください。

アプリケーション起動時に『このアプリケーションを実行するには、「Visual C++ v14 Redistributable (x86)」が必要です。Microsoft の公式サイトから最新版をインストールしてください。』というメッセージが表示され、アプリケーションを起動できない場合、アプリケーションが利用しているDLLファイルがお使いのシステムにインストールされていません。下記URLより、Microsoft公式の再頒布パッケージ(vcredist_x86.exe)の最新版をダウンロードし、お使いのシステムにインストールしてください。なお、マークシートリーダーの操作方法の詳細については、付属のご案内をご参照ください。

https://www.microsoft.com/ja-jp/download/details.aspx?id=53587

追記(20260405)

Windows の OpenCV は Visual C++ でビルドされており、そのビルドに使われた特定バージョンのVisual C++ ランタイム DLL がシステムフォルダに存在しない場合、OpenCV が起動できないため、vcredist でその DLL を所定の場所にインストールする必要があります。

MS_Reader V3 のダウンロードは、以下のリンク先から行ってください。

■ AI自動採点用 高精度モデルについて(任意・別配布)

MS_Reader_V3 のAI自動採点で使う高精度な文字認識モデル(PP-OCRv5_server_rec.onnx、約84MB)です。ファイルサイズが大きいため、本体には同梱していません。
入れなくてもAI自動採点は使えますが、入れると、読み違いによって正解の答案に × が付くことを減らせます(漢字を含む語句・数値・英単語の設問で働きます)。

※ 本ファイルは無保証です。ご利用は自己責任でお願いいたします。
※ 本体(MS_Reader_V3.zip)は別途ダウンロードしてください。

  • 対象:AI自動採点機能付きの MS_Reader_V3(2026年9月版以降)
  • ダウンロードするファイル:MS_Reader_V3_HighAccuracyModel.zip(約76MB/展開後 約84MB)
  • 使い方:zip を展開し、OnnxOCR フォルダ内の PP-OCRv5_server_rec.onnx を、MS_Reader_V3.exe と同じ場所にある OnnxOCR フォルダへコピーするだけです(ファイル名は変えないでください。設定の変更は不要です)。詳しくは同梱の Readme.txt をご覧ください。
  • SHA-256:d9dc333c9c7b042c6dffb8e33d72b6f65c9c1d463d0a3c2f78174fea55e94752
  • ライセンス:PaddleOCR(PaddlePaddle)が Apache License 2.0 で公開しているモデルを、無改変で同梱しています。

高精度モデルのダウンロードは、以下のリンク先から行ってください。

MS_Reader V3 のマークシート読み取り&デジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

MS_Reader V3 の付属の機能を利用した、手書き答案のデジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

MS_Reader V3 の付属の機能を利用した、マークシートと手書き答案の併用型デジタル採点の実施方法は、当 Blog の次の記事をご参照ください。

2026年9月27日、手書き答案のAI自動採点機能を搭載した新しいプログラムに更新しました。また、古いバージョンにあった細かな不具合も発見できた分についてはすべて修正しました。AI自動採点機能の使い方は、当 Blog の次の記事をご参照ください。

同梱のマークシートメーカーは PDF ファイルの出力に Skia を使用しています。Skia のライセンスは MIT ライセンスです。このライセンスの詳細につきましては、マークシートメーカーのフォルダに同梱した Skia4Delphi_and_Skia_LICENSE.txt をご覧ください。マークシートメーカーの使い方の詳細については、当 Blog の過去記事をご参照ください。

採点準備作業を大幅に自動化(採点専用画像を自動的に作成)しました。これにより、これまでと比較して、よりスムーズに実際の採点作業に入れるようにユーザー体験の向上が図れたと思います。

4.お願いとお断り

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

Code Editor あれこれ

Delphi のコードエディターを使っていて、個人的に(あれ !?)って思う機能が2つ。

1.驚愕のショートカット その1

1つは、カスタマイズ出来るのかもしれませんが、左向き矢印キーを押し下げ続けた際のカーソルの挙動です。コードエディターの行の左端までカーソルが移動すると、一般的なエディター(?)ならカーソルは現在の行の上の行へと移動すると思うのですが(そのエディタを開発したプログラマさんの考えによるものと拝察しますが)、Delphi のコードエディターでは、カーソルは画面の左端位置をまるで死守するかのように、そこで頑ななまでに停止し続けます。

もって瞑すべし。左向きの矢印キーの連打にも必死に抵抗、テコでも動かないカーソルの立派な態度は見習うべきものがあります。

個人的には、実はこの機能がとても気に入っていて、コードエディタの右側に表示される 縦のガイドラインである「右マージン」(Right Margin)をはるかに超えて、右へ、右へ、と 長~くコードを書いてしまったときなど、とにかく左向き矢印キーを押し下げ続けていれば、現在カーソルがある行の先頭まで戻れるので、いつの間にか、これを多用するクセがつき、他のエディタを使っている時につい、うっかり、左向きの矢印キーを押し下げ続けてしまい、意図しない遥か上の行へとカーソルが移動・・・みたいなことも。

ただ、アセっている時など、カーソルの移動速度がトロくてイラつくことも、多々あり・・・
(こんな時、コードエディター下のスクロールバーをマウスで操作するのは、手が3本あれば話は別だと思いますが、非常に面倒くさい)

実は、(コレが言いたかったことなのですが)これにはショートカットなる裏ワザがあって、左向きの矢印キーを押し下げ続けなくても、なんと、(私の PC では)Fn キー + Home キー押し下げで現在編集している行の左端へ、イッキに戻れることを、先日偶然、発見。

小学生の頃、宇宙戦艦ヤマトで「ワープ」という航法を知り、激しく感動した記憶は今も胸にあざやかですが、この事実にはそれ同等か、それ以上の驚きを感じました。

だって、20年以上、そんな機能の存在すら、知らなかったのですから・・・

チャリンコで風を切りながら、わーぷできんかなー♪ と考えていたあの頃がなつかしぃ

それとは別に海を見下ろす丘からの長いながい下り坂。マシンと一体化するマンガか、なにかの真似をしてチャリンコの後ろの方へ足を伸ばしつつ、わーぷをイメージして駆け下ったら、右足が後輪のスポークにはさまってわーぷどころか、大急ブレーキ。足の甲がチギれたかと思うほど痛い目にあったのも、今では懐かしい思い出です。改めて実感。バカは何も今に始まったことでなく・・・

もしかして、死んでも なおらない・・・?
この世に未練がある場所、いっぱいあるもんなー
あぁ ことごとく、あの日、あの場所に戻れたら。

なにはともあれ、左へ戻るなら Fn + Home キー

こんなイイコトがあったなんて、誰も教えてくれないんだもの。前回の記事にも書きましたが、Delphi を使い続けること20年・・・、こちらも初めて知る驚愕のテクでありました。

2.驚愕のショートカット その2

同じ驚きが、コメント化にも潜んでいました。

AI にコードを書いてもらって、それを試してみる時など、「絶対に動く」保証などどこにもありませんから「現在、不満を抱えつつも or 不備を内包しつつも、捨てるに捨てられない procedure / function はとりあえずコメント化」して、その上か下に(私は下ですが) AI が書いてくれたコードを貼り付けて試す・・・みたいなことを、どなた様も普通におこなっていらっしゃる今日この頃のではないかと思うのですが、その際問題になるのがコメント化の方法です。Delphi のコメント化の場合、もちろん使用するのは、それが1行、2行なら // で、それが複数行にわたる場合は皆さま { } だと思うのですが、

マウスでドラッグしてコメント化したい範囲を選択


んで、Ctrl + / を実行。・・・すると

んごっ!(みたいな衝撃はありませんが、感覚的にはありで)

最後の d; 部分に現れる「芸の細かさ」にも魂はふるえ・・・


さらに、あろうことか・・・ そのまま、もいちど Ctrl + / で

元に戻ります・・・

ひー

こんなイイコトがあったなんて、誰も教えてくれないんだもの。前回の記事にも書きましたが、Delphi を使い続けること20年・・・、こちらも初めて知る驚愕の一手でありました。・・・と、もう一度書くしかない厳粛な事実を、どう受け止めたらよいのでありましょう?

てか、こういう時代ですから、IDE 側でこちらの気持を忖度して、左向きのキーが長押しされたら、

「それなら Fn + Home が便利です」とか・・・

超絶、延々と範囲選択したら、

「コメント化なら Ctrl + / だよ」などと・・・

テレパシーで送信するのは無理だとしても、せめて StatusBar あたりに表示してもらえたら・・・

と、思ったのですが、そぉいうのは、無理かなー やっぱり?

いっしょに暮らしている方が、この記事を読んだら、きっと、

それがどしたん?
おまえが勉強しとらんだけだろ?

そうおっしゃるに相違なく。

でも、海より深く感動した事実とささやかな夢を、またまたインターネットの片隅に・・・

おまけ 驚愕のショートカット その3

ナニと、それがかち合うとその機能が停止するのか・・・ AI はLSPの解析タイミングとかなんとか言ってましたけど・・・ ほんとのところは未だにまったくわかりませんが、動いてくれると大変便利なんですが、最近、動かないショートカットの代表が、Ctrl + Shift + V です。

あの記事を書いた頃は、それなりに動いていたんだけど、なー


動いてくれれば、こんなに便利なショートカットは他にないと思うのですが、3日くらいまえかなー? 偶然のように動くのを見た記憶があるのですが、あれから1度も動きません。

メニューのリファクタリングから見ると、いつもグレーアウトしています。


これが、いつも期待通りに動くと、すごく、うれしいんだけど、なー☆

かみさま 僕の Delphi で Ctrl + Shift + V が今度動く日は、いつー?

お願いとお断り

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

case文と列挙型

新しく開発中のデジタル採点システムで、試験形式を分類するための公開列挙型を次のように interface 宣言部に宣言しました。

// 試験の形式
type
  TExamFormat = (efMarkOnly, efMixed, efHandOnly);

TExamFormat の列挙値の意味は次の通りです。

efMarkOnly:マークシート方式
efMixed:マークシートと手書きの併用方式
efHandOnly:手書き方式

で、驚いたのは、これを case 文で使用する時の挙動です。

case ExamFormat of

と、書きまして、Enter キーを叩いたら、

  case ExamFormat of
    efMarkOnly: ;
    efMixed: ;
    efHandOnly: ;
  end;

・・・と、全自動で入力されました!

こんな便利な機能が case 文にあったなんて!!

20年以上、Delphi に触れてきて、恥ずかしい限りですが、昨日、初めて知りました!!!

理由はわかりませんが、

めちゃくちゃ感動!

この感動と驚きをインターネットの片隅に。

CPU Window は見たくない

ずっと、そう思っていました。・・・ってか、新しい Delphi は玄人向け仕様に進化して、エラーが起きたら、デフォルト CPU Window が表示されるんだなー みたいな。うわー すごいなー!! みたいな。

(僕がそう思い込んでいただけで、上の記述は完全な誤りです)

いつか、この画面の意味がわかるようになりたいなー!


だから、エラーが発生すると僕みたいなド素人はもぉたいへん・・・。>_<

OutPutDebugString(デバッガが受信できるデバッグストリームに文字列を出力する API )を随所に入れて、どこまで無事に動いたか? みたいな方法で、膨大な手間暇をかけてエラーが起きた行を特定するしかなく・・・

s := 'Step1 Passed';
OutPutDebugString(PChar(s));

Hoge Hoge

s := 'Step2 Passed';
OutPutDebugString(PChar(s));

Hoge Hoge Hoge

s := 'Step3 Passed';
OutPutDebugString(PChar(s));

プログラム書いてるんだか、OutPutDebugString経を唱えてるんだか、わからない状態に・・・

もちろん、悩み相談できる人もなく・・・普通に考えて、Delphi 使って開発しているヒトが僕の周囲数十(ひょっとすると数百・・・)km以内にいるとは思えないし・・・、で、Object Pascal のことなら何でも答えてくれる AI に訊いても、Delphi の IDE の具体的な操作方法になると全然ダメで、ありもしない『オリジナル』メニュー項目を創作したり? それってもしかして Delphi 7 の時代のメニューじゃない? みたいな、僕の12.3 にはない操作方法をまことしやかに案内されたり・・・

だから、CPU ウィンドウは、なるべくなら見たくないものの代表でした。
Delphi 12.3 のエラー発生時のデフォルト設定は「 CPU ウィンドウの表示 」。
今、考えると、それは僕の悲しい思い込みだったようです。

【もくじ】

1.ある日 AI が言いました
2.CPU ウィンドウが消えた!
3.デフォルト CPU ウィンドウの表示じゃなかったの?
4.まとめ
5.お願いとお断り

1.ある日 AI が言いました

午前2時、太陽が出ている間は陽の当たらない場所を歩くことが多い僕にとって、いちばん輝いた時間です。ほんと、心安らかに Delphi との会話を楽しめる、他にたとえようもないくらい、大好きで大切なひと時です。

ふと思い立って AI に訊いてみました。

Q:Delphiで、エラーが発生した行を突き止めたいのですが CPU という Window が出てしまいます。エラーが発生した行を示すように設定を変更したいです。Delphi のバージョンは 12.3 です。このバージョンに合わせて正しい設定方法を出来る限り、わかりやすく、教えてください。

AI の回答を要約。

1. ビルド構成を「Debug」にする
画面右側の「プロジェクト マネージャ」で [ビルド構成] のツリーを展開し、[Debug] が太字(アクティブ)になっていることを確認。もし [Release] が太字になっていたら、[Debug] をダブルクリックして切り替える。

2. 「デバッグ用の .dcu を使用する」をオンにする
※ これは 12.3 では「デバッグ版 DCU の使用」という名称でした。
IDE上部のメニューから [プロジェクト] > [オプション] をクリックして、プロジェクトオプション画面を開き、左側のツリーメニューから、[ビルド] > [Delphi コンパイラ] > [コンパイル] を選択。画面一番上の「ターゲット」というドロップダウンが [Debug 構成] になっていることを確認。右側の設定リストの中から [デバッグ] というグループを探し、[デバッグ用の .dcu を使用する] という項目を [True] に変更する。

3. 「最適化」をオフにし、「デバッグ情報」をオンにする
[コンパイル] の画面で、[コード生成] グループの中にある [最適化] を [False] にする。続けて、左側のツリーメニューから、[ビルド] > [Delphi コンパイラ] > [リンク] を選択し、[デバッグ情報] という項目を [True] に設定。

設定が終わったら、画面右下の [保存] ボタンを押してオプション画面を閉じる。

4. プロジェクトの「クリーン」と「ビルド」を実行する
設定を変更した後は、古いコンパイル結果が残らないように全体を再構築する必要がある。この手順を忘れると設定が反映されない。方法は、メニューから [プロジェクト] > [(プロジェクト名)クリーン] をクリックする。続いて、メニューから [プロジェクト] > [(プロジェクト名)をビルド] をクリックして実行する。

おかしな部分も多少ありましたが、いつもより、かなりまともな回答をいただけました。
(実は今まで何度もこのような質問をして、そのたびにガッカリしてきた記憶が・・・)

【おかしな部分】

① 誤:「デバッグ用の .dcu を使用する」 → 正:「デバッグ版 DCU の使用」


② 完全な誤り:メニューから [プロジェクト] > [(プロジェクト名)クリーン] をクリックする。

そんなメニュー項目は Delphi 12.3 にはありません。でも、すぐにピンときました。キーワードは「クリーン」です。そう、IDE の画面右上にあるツールウィンドウの「プロジェクト」に表示されている exe を右クリックすると出るサブメニューの「クリーンアップ」のこと? みたいな・・・

exe を右クリックするとサブメニュー(下)が表示される
きっとコレだ!

ダメもとで上記設定1~4を早速 設定 & 実行 してみました☆ すると・・・

2.CPU ウィンドウが消えた!

長い間、見たことのない懐かしい画面が出現したのです。CPU ウィンドウは表示されず、画面に薄い赤い線引きで(あぁ、御懐かしい)表示されたのは System.SysUtils の内部エラーが発生している行☆

こんな感じ(画像はイメージで、実際の画面とは異なります)

今思えば、僕が書いたプログラムのコードじゃなくて、「 System.SysUtils の内部エラーが発生している行 」が表示されているところにも重大な意味があったのですが、もちろん、この時の僕はそんなことは露知らず・・・、もぉうれしくて!!!

CPU Window が消えたー☆☆☆

ヒトはなんて可笑しな生き物なのでしょう。(何にもわかってない僕は)さらに欲を出して・・・

Q:Delphiで、Debug実行時にエラーが発生した場合、System.SysUtils などのエラー発生行が示されますが、自分の書いたコードのエラーが発生する原因になった記述のある位置を表示する方法を教えてください。

AI の回答の要約

常にVCLのソースコードの中まで入ってしまうのが煩わしい場合は、設定を変更すれば「自分のコードに近い場所」で止まりやすくできます。

で、その方法は・・・

1. メニューの プロジェクト(P) > オプション(O) を開く。
2. ビルド > Delphi コンパイラ > コンパイル を選択。
3. 「デバッグ用 DCU を使用」 を False にする。

はぁ?

「デバッグ用 DCU を使用」 を False に?

さっきと逆じゃん☆

どーゆーコト?

みたいな・・・

3.デフォルト CPU ウィンドウの表示じゃなかったの?

この時点で、まだ僕は Debug 実行でエラーが発生した際に表示される画面を「デバッグ用 DCU を使用」の True / False が左右していることに気がついていません。

つまり・・・(わかってしまえばカンタンなのですが)

設定表示される画面メリット
TrueVCL(ライブラリ)のエラー発生個所VCLの内部の動きがわかる
False自分のコードのエラー発生個所自分のコードの誤りがわかる

最初、僕は「デバッグ用 DCU を使用」を True に設定していたので、エラー発生時に表示された画面は System.SysUtils のエラー発生行だったのです。

僕はどちらかと言えば、「自分で書いたコードのエラー発生位置(行)を知りたかった」ので素直に AI にそう訊ねたのですが、AI は僕がくるくるぱーで、実はなんにもわかってないことなんて知りませんから、ごく普通に、至極当たり前の表現で、

常にVCLのソースコードの中まで入ってしまうのが煩わしい場合は設定を Falseにした方がいいよ!

・・・と教えてくれたわけです。つまりこれは、False に設定すれば VCL 内部のエラーを引き起こす原因を作った僕の書いたコードの位置を表示して実行が中断されることを意味します。

なぜ、そのことに気がついたかと言うと、嘘をついても始まりませんから正直に言いますが・・・

Q:エラーの原因になった行を特定するためには、「デバッグ用 DCU を使用」 はTrueにしておくのが正しいのですか? それとも False にする方が正しいのですか? あなたに訊いたら、ある時はTrueにせよ、ある時はFalseにせよ、と一貫性がない答えをいただいて混乱しています。True/Falseどちらが良いのか、わかりやすく解説してください。

この問いに対し AI は『 状況によって正解が変わるんですよ 』と、まるで人間みたいな前置きをした上で、「デバッグ用 DCU を使用」の True / False による挙動の違いを説明してくれました(その極めて簡単にしすぎた感もある要約が上記の表です)。

そぉ だったのかー と、素直に感動☆

もちろん、僕の今後の「デバッグ用 DCU を使用」の設定は False の一択に決定です。僕の書いたコードが VCL にどんな迷惑をかけたかなんてことよりも、その迷惑をかける「きっかけ」となった記述(コード)のある行の方がド素人には重要なのです。

そこを直せば VCL に迷惑をかけずに済むのですから・・・

Delphi 作ったヒト、ほんとにすごいなー☆☆☆

湧き上がる新たな感動に心を震わせたまま、思ったことは(これは My Blog に残しておかなきゃ!)・・・みたいな・・・邪(よこしま)な思い。なんで邪と思うのか、その理由は言葉にできませんが。

Blogに残すにはナニはともあれ、エラー発生時に表示されていた CPU ウィンドウを記録しておかねば・・・と、まともな方向でプログラム書いてる時には思ったこともない「エラーの発生を心から願う」という理解し難い気持ちを抱えつつ、Debug 関連の設定を初期状態(?)に戻してエラーを再現・・・

(エラーが起きるのを、こんな楽しみにしたことも、なかったなー☆)

そう思いつつ、エラーを起こすプログラムを実行(F9)・・・

あ・れ?

CPU Window が出てこない!

どーゆーこと? みたいな・・・

これはもう AI に責任をとってもらうしかありません。

【こんなプロンプトでも AI は言いたいことを理解してくれました!】

Q:今までずっとCPUウィンドウしか出なかったのですが、設定を元に戻しても今度はCPUウィンドウが出なくなりました。なぜですか? 

【驚きの回答の要約】

結論:「一度デバッグ環境をクリーンにしてビルドし直した」ことによって、Delphiの内部管理(デバッグ情報の紐付け)が正常化された。今までCPUウィンドウしか出なかったのは「異常な状態」で、今ソースコードが出るようになったのが「正常な状態」。

そうなった理由は主に3つ。

1.「クリーンアップ」の効果
「プロジェクトをクリーンアップ」したことにより、古い設定(デバッグ情報が壊れていた、あるいはパスが通っていなかった時期)の残骸(.dcu や .exe)が完全に消去された。

2.デバッガが「ソースの場所」を学習(一度「デバッグ用DCU」をオンにしてビルドした際、Delphiのデバッガは「VCLのソースコードはここにある」というパスを正確に認識した。そのため、設定をオフに戻しても、デバッガが「前回の情報を手がかりにソースを見つけ出せる」ようになった。

3.エラー発生元 が「自分のコード」から呼ばれている
あるメソッドを実行してエラーが起きた際、デバッガはまずエラー地点(DLL内)を見るが、「そこはソースがないから、一歩手前の呼び出し元を表示しよう」と判断する。呼び出し元は僕の書いた Unit1.pas。今の環境は「自分のソースコードがどこにあるか」をデバッガが完璧に把握している状態なので、ソースコードを優先して表示し、CPUウィンドウを隠してしまう。

よーするに、今までの僕の環境が異常だったってコト?

つまり、僕の「エラー発生時にまず表示されるのは CPU Window である」というこれまでの認識は完全な誤りでした。

4.まとめ

(1)デバッグ情報不足(含むPath)だと、IDEは「ソースなし」と判断、CPUウィンドウを表示。
(2)デバッグ情報を設定し直すと、ソースコードが優先的に表示されるようになるカモ。
(3)デバッグ用DCUのTrue/Falseの使い分け
 ・自分のミスを探すなら False
 ・VCL 内部でのエラー発生の仕組み・位置を知りたいなら True
(4)今回の出来事を Blog に書いてもイイ?って AI に尋ねたら「全然いいよー」ってお返事でした。
(5)エラーが発生した際に CPU Window が表示されるのはデフォルト設定ではありません。

5.お願いとお断り

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

新型フローティングパネル

過去記事で書いたフローティングパネルより、さらにカンタンで気持ちイイ、パネルの浮かべ方です。

【もくじ】

1.学びのきっかけ
2.実装
3.まとめ
4.お願いとお断り

1.学びのきっかけ

作成中のマークシートリーダーに、手書き答案の採点機能も追加搭載したくなり、そうなるとどうしても必要になるのが、手書き答案の解答欄矩形の座標データ。

データそのものは、OpenCV の矩形検出を使えばなんとか取得できそう。ただ、どうしても不要な矩形まで検出してしまうことがあるので、検出できた座標を一覧形式で表示し、解答欄矩形ではないものをそこから削除する作業と場所が必要。

これに最適なのがフローティングパネル。解答欄矩形の座標データは、その取得作業が完了してしまえば、表示しておく必要はさらさらない。さっさと消して、より広い表示スペースを確保したい。

そう思って、コレを作りました☆

新型フローティングパネル


実行中の様子です。

実行中は上のようなイメージになります。


タイトルバーを D&D して任意の位置へ移動できます。

現在選択している座標が青い枠で囲まれています。
(画面は座標位置へ、自動スクロールします)

2.実装

【専用フォームを作成】

最初に、パネルを載せるための専用フォームを作成します(メイン Unit は既存のもの)。

メニューから 「ファイル」→「[新規作成」→「フォーム」 を選択します。


新しいフォームの名前は、例: frmFloatHolder とします。


次は、プロパティの設定ですが、上のように名前を付けたら、あと2つ設定します。

1つは、タイトルバー。細くして、フワフワ浮遊感が出る?・・・かな??
BorderStyle を bsSizeToolWin に設定。


もう1つは、最前面に表示されるように
FormStyle を fsStayOnTop に設定。


以上の設定を終えたら、例:UnitFloatHolder.pas という名前で保存します。

追記(20260319)

OS にレイアウトを操作されたくないので、後日、Scaled も False にしました。

【メインフォーム側の処理】

メインフォームから frmFloatHolder を操作できるように、Unit1 の implementation の下の(なければ作成)uses に UnitFloatHolder を追加します。


メインフォーム上に新しい Panel を置いて、あれこれ載せます。


Button1Click手続きにクリック時のフローティング処理を書きます。

procedure TForm1.Button1Click(Sender: TObject);
begin
  //フローティングフォームを作成
  if not Assigned(frmFloatHolder) then
    frmFloatHolder := TfrmFloatHolder.Create(Application);

  //パネルのサイズをフォームに合わせる
  frmFloatHolder.ClientWidth := Panel1.Width;
  frmFloatHolder.ClientHeight := Panel1.Height;

  //親をメインフォームからフローティングフォームに書き換える
  Panel1.Parent := frmFloatHolder;

  //パネルを移動用フォーム全体に広げる
  Panel1.Align := alClient;

  //フローティングフォームを表示
  frmFloatHolder.Show;
end;

実行して、Button1をクリックしてみました!

これは気持ちイイ!


ただ、フローティングパネルの閉じるボタン(右上)をクリックすると・・・

からっぽのフォームだけになってしまいます!(当然ですが)


それでは寂しいので、フローティングパネルの基地を作ります。

基地の名前は「 PanelAnchor 」

大きさは、とりあえず Panel1 と同じにしました。
(Visible := False で運用するなら、とくにこだわらなくてもいい部分ではあります)


基地に戻るためのボタンをフローティングパネルに追加します。


下にいるのもなんなので・・・フローティングパネル(Panel1)を右クリックして、表示されるサブメニューから「コントロール」→「前面に移動」を順にクリック。


フローティングパネルが上にきました!


で、基地に戻るためのコードを書きます。Button2 をダブルクリックして・・・

procedure TForm1.Button2Click(Sender: TObject);
begin
  //親を基地のパネル」に設定
  Panel1.Parent := PanelAnchor;
  Panel1.Align := alClient;

  //基地を表示
  PanelAnchor.Visible := True;

  //フローティングパネル用のフォームを隠す
  frmFloatHolder.Hide;
end;

実行して、基地に戻してみました!

帰るべき家があると安心感がありますね!


実際の運用では基地の Visible を False に設定して、表面上はフローティングパネルは消えたらもうそれまで・・・というようにしたり、あとは自由自在です。

3.まとめ

この方法の最大のメリットは「パネル内のコントロールのイベントハンドラは、全部 メインユニットにある」 という点です。メンテナンスが 走召 楽勝。これで WinAPI の知識は不要で、安全でメンテナンスしやすいフローティングパネルが実現できました!

前回のフローティングパネルで、さんざん悩んだアレはいったい何だったのか・・・

いや・・・

“Every cloud has a silver lining.”

「どんな雲にも銀の縁取りがある」

あの経験が僕を今日に導いてくれたのだから・・・

ヒトも、プログラムも、進化します。

明日も Delphi と、肩ならべて、歩こう!!!

4.お願いとお断り

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

My Mark Sheet Maker

プロが作ったプログラムには、到底敵わないけれど・・・ でも、今の自分の精一杯。
持てる力の全てをつぎ込んで書きました。

マークシートを(半自動ですが)作ってくれるプログラムです。
罫線の幅や長さ等、微調整が必ず必要ですが、動きます!

この世界に残したかった、夢のひとつをかたちにしました。
My Mark Sheet Maker です。

4ブロックまで。最大19選択肢。A3, A4, B4, B5版のマークシートが作成可能です。

【追記】(20260901)

手書き解答欄を作成した場合、結合セルに入力した文字列が一部しか印刷されない不具合を解消しました。現在ダウンロード出来るプログラムは、不具合を解消した最新版にアップデートしてあります。ご迷惑をおかけした皆さまに、心からお詫び申し上げます。

【もくじ】

1.使い方
(1)用紙と向きを選択(新規作成の場合)
(2)タイトルを入力
(3)群数、行数、列数、解答欄群(ブロック)の間隔を設定
(4)マークの縦長楕円の幅と高さを調整
(5)選択肢番号の大きさの変更他
(6)表示の更新と設定の保存
(7)選択肢数(=列数)の設定
(8)設問番号の変更
(9)罫線の設定及び長さの調節
(10)上余白・■■■ 位置・氏名記入欄の設定
(11)印刷
(12)セクション名の追加と削除(の仕様)
2.sk4d.dllについて
3.ダウンロード
4.まとめ
5.お願いとお断り

1.使い方

(1)用紙と向きを選択(新規作成の場合)

プログラムを起動したら、最初に作成するマークシートの用紙と向きを選びます。これだけでデフォルト設定に従ったマークシートが描画されます。

一般的な試験用途でしたら、A4・横向きが最適だと思います。


描画される内容が『チープ』だと感じる方が多いと思いますが、機械にマークをしっかり読ませることが最大の目的なので、これを阻害しそうな要因は出来るだけ取り除いた結果が、細く薄い印字と罫線になっています。行列切り出しの目安となる解答欄の枠線は残しましたが、解答欄内の縦罫線は消去できる設定としたのもこのためです。罫線があった方が好ましいと感じられる方も、もちろんいらっしゃると思いますが、縦罫線がない状態の方が機械の目に対する悪影響は排除できます。

デフォルト設定では、解答欄の群数(ブロック数)は4、1群あたりの行数は25行(行数はすべてのブロックで等しくする必要があります)、選択肢数は8としてあります。ブロックどうしの間隔(群間と表示)も調整できます。群数・行数・列数を増減すると、その設定に合わせたマークシートが再描画されます。1366 × 768 ピクセルの解像度で開発しています(高 DPI 環境には非対応です)。

カチカチ、クリックして値を設定するより、TSpinEdit に直接入力した方が早いカモ?です。

・既存のマークシート設定がある場合

以前に使用して保存したマークシート設定がある場合は、それを呼び出して利用することもできます。プログラムの起動後に ComboBox の選択肢から選ぶだけです(用紙のサイズや方向の設定は必要ありません)。

(2)タイトルを入力

後からでもかまいませんが、用紙と向きを選んだら、次に「タイトル」を入力してください。

内容は自由です。

例えば、こんな感じです。

あまり長くしない方が管理が楽?かな・・・


タイトル名でマークシート設定を管理( INI ファイルに保存&呼び出し)しますので、教科 or 科目の名称、考査の時期、日付等を入れておくと管理しやすいのではないかと思います。Edit コントロールへ入力すると同時にマークシートの描画も更新されます。入力確定後、再度 Enter キー押し下げで、次のコントロールへフォーカスが移動します。

フォントは「MS Pゴシック」一択です(すみません)。

タイトルを入力するComboBoxの右隣には、次のアイコンがあります。クリックすると、デフォルトのタイトル文字列『タイトル 考査名 解答用紙(20XX/MM/DD)』を設定できます。

クリックすると、次の確認メッセージが表示されます。
「はい」をクリックすると更新されます。
新しいタイトルを設定してください。

(3)群数、行数、列数、解答欄群(ブロック)の間隔を設定

ここで、前述した群数、行数、列数、解答欄群(ブロック)の間隔を設定してください。ここでのフォーカスの移動は TAB キー押し下げです(Shift + TAB で戻ります)。


解答欄の纏まりを「群(ブロック)」と表現しています。1~4群(ブロック)まで設定できます。群数を変更すると、それに合わせてマークシートの描画内容も更新されます。列数と行数は1つの群(ブロック)内での値を設定してください。

群数1、行数25、列数8の場合(実行時の画面)
群数1、行数25、列数8の場合(PDF出力時の画面)

マークする部分(楕円)の大きさは、後から調整できます。
群数2、行数25、列数8の場合(実行時の画面)

解答欄群(ブロック)矩形の右側にややはみ出した横罫線の長さはあとから微調整できます。
群数2、行数25、列数8の場合(PDF出力時の画面)
群数3、行数25、列数8の場合(実行時の画面)
群数3、行数25、列数8の場合(PDF出力時の画面)
群数4、行数25、列数8の場合(実行時の画面)
群数4、行数25、列数8の場合(PDF出力時の画面)

行数、列数、解答欄群(ブロック)の間隔の設定も同様に TSpinEdit の値を変更するだけで行えます。TSpinEdit の値の変更に合わせて画面の描画内容が自動更新されます。

(4)マークの縦長楕円の幅と高さを調整

塗りつぶし範囲を示す、選択肢番号を囲む縦長楕円(=マーク)の幅と高さを調整します。やり方はこちらも TSpinEdit 右側の ▲ や ▼ をカチカチするだけです。広すぎると塗りつぶしに時間がかかり、狭すぎると(読み取りプログラムのアルゴリズムにもよりますが)読み取り判定に問題が生じる可能性が高くなります。

過去に私が自作したマークシートリーダーは(設定によっては)髪の毛1本分の・・・マークとは到底言い難いような・・・「線」でも反応しましたので、マークシートの印刷用紙に黒いシミがあったり、受験者の使用した消しゴムの屑が付着していた場合にも(読み取り閾値の設定で)対応できるよう、やはり、マークはある程度の幅と高さがあった方がいいのかもしれません。

・・・というようなこれまでの経験と、自分的な好みでデフォルトの幅と高さは設定しました(やや大きめです)ので、適宜変更してください。

長径が「高さ」、短径が「幅」です。
(負の数も設定できます)
いちばん小さくした場合で、これくらいでしょうか?

(5)選択肢番号の大きさの変更他

選択肢番号の数字の大きさを変更したい場合は、画面右上の □設定 をチェックしてください。

□部分をクリックして、チェックを入れます。

各種設定を変更できるバーが新しく表示されます。その中の「 Font サイズ」(文字が小さいです)で選択肢番号の数字の大きさを調整できます。

デフォルトは10ポイントに設定してあります。

【マークする部分の設定例】

FontSize : 10, 楕円の長径 : +0, 楕円の短径 : +0 (デフォルト設定)
FontSize : 16, 楕円の長径 : +2, 楕円の短径 : +2 に設定
FontSize : 7, 楕円の長径 : -3, 楕円の短径 : -3 に設定

ちなみに▼をクリックして入力できる下限値は「7」です。

これ以上小さく出来ません・・・と言いたいところですが


プロパティで MinValue を「7」に指定しても・・・

MaxValue は大きすぎ?・・・
直接入力すれば「1」指定も出来てしまいます。もちろん、数字は判読困難・・・

直接入力する場合、欄を空欄にすると( SpinEdit が内部で StrToInt 関数のような仕組みを使い、入力値を整数に変換する際に)例外が発生しましたので、StrToIntDef 関数を使用して空欄でもエラーメッセージが表示されないようにしましたが、MinValue 以下の値が直接入力された場合に、強制的に値を変更して SpinEdit に「7」と表示するのはワザと避けてあります(そういう仕様だと御理解ください)。

つまり・・・

    ACanvas.Font.Name  := 'MS Gothic';
    //ACanvas.Font.Size  := StrToInt(seFontSize.Text);
    ACanvas.Font.Size  := StrToIntDef(seFontSize.Text, 7);

・・・という記述は入れましたが、

procedure TForm1.SpinEdit1Change(Sender: TObject);
begin
  if SpinEdit1.Value < 7 then
    SpinEdit1.Value := 7;
end;

や

procedure TForm1.SpinEdit1Exit(Sender: TObject);
begin
  if SpinEdit1.Value < 7 then
    SpinEdit1.Value := 7;
end;

みたいな記述はしてません。

なので、TSpinEdit を空欄にすると表示される・・・

このメッセージはでません。

カチカチクリックじゃなくて、コントロールに数値を直接入力すれば・・・

FontSize := 1 も可能

これもナニかに使えるかも・・・と思った次第です。
思っただけで、ナニに使えるかは、まだ思いつきませんが・・・

ちなみに「0」を入力してみたら・・・

選択肢番号が巨大化しました!

これは Windows の仕様で、TFont.Size プロパティに 0 を代入すると、Windowsはそれを「サイズ指定なし(デフォルト)」と解釈するようです。その結果、そのシステムやデバイスの「標準のフォントサイズ」が適用されてしまい、大きな文字が表示される・・・でいいのかな?

Font サイズ指定周辺のコントロールについても触れておきます。


「選択肢始番」(自分でも読み方がわかりません・・・)は、正確に表現すれば「マークシートの選択肢の番号の始まりの数字を0(ゼロ)にするか、1(イチ)にするかの指定」を意味します。

そこにどういった必然性があるのか、わたしにはまったく理解できませんが、高等学校の教科「情報」の共通テストのマークシートでは、選択肢の始まりは「1」ではなく「0」です。このことから、選択肢の番号が「0」始まりのマークシートも作成可能としました。

また、「印刷濃度」は、罫線を含めたマークシート印刷時の灰色の濃さの調整です。このプログラムは(別に作成中の)マークシートリーダーで使用するマークシートを作成するために開発しました。その開発中のプログラムで実際に読み取りテストを行って、問題なく動作した値(140)をデフォルト値に設定してあります。

マークの読み取りに限って言えば、ヒトの目で選択肢番号が読み取れるギリギリの線まで印刷の濃さは薄くしたいところ(=機械的な読み取り判定ミスを防止するため)ですが・・・

自分的には、できればこれくらいの濃さで仕上げたい気がします・・・

しますが・・・

実際の運用では、これくらい印刷濃度を薄くすると、以下のような様々な問題が生じます。

問題1 スキャナーで解像度 200dpi を指定してスキャンすると選択肢の数字が読めなくなる。
問題2 罫線もほぼ消えるので、読み取りプログラムで読取範囲を設定するのに一苦労する。
問題3 採点プログラムで採点し、返却用シートとして印刷すると「ほぼ白紙化」する。等々

ですので、後の処理を考慮して気持ち濃い目に設定してあります。このプログラムを、万一、使用される場合は、必要十分な試行を行って、「試験実施後の処理に支障の出ない濃さ」でマークシートを印刷していただけますようお願い申し上げます。

試しに様々な濃度で印刷したマークシートを、実際に現場で使用しているスキャナーでスキャンして、読み取った後の画像がどのような状態になるのか、確認してみました。

【 濃度 140 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 150 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 160 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 170 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 180 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 190 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)

【 濃度 200 】

実行時の PC 画面
スキャンしたJpeg画像(部分・カラー・200dpi)


自分的には、濃度 200 くらいで印刷した状態が『しっくり』きますし、これをスキャンするとほとんど白紙状態になりますので、機械的に読み取り精度を上げるという意味では、これくらいの濃度に設定したいところです。

ただ、ヒトがマークの読み取り結果を目視して確認したり、マークシート画像上に採点結果を印刷して答案返却用に使用するという、採点に伴う諸々の作業を円滑に行うためにはスキャンした画像で文字の判別がギリギリ可能な濃度 140 以下に設定する必要がありそうです。このマークシート作成プログラムで作成したマークシートを使用して、試験の準備→実施→採点→答案返却という実際の現場の作業を何回か繰り返す中で最適な濃度の設定値を見つけなければなりません。経験して初めてわかることがどうしてもあるからです。そのようなことから、現在は濃度のデフォルト設定値をスキャナーでスキャンした際に選択肢の数字・文字がギリギリ読める限界の値 140 としてあります。

(6)表示の更新と設定の保存

ジョイスティックの絵文字は「操作」を意味します。

モニター画面のアイコンがあるボタンが「表示の更新」、フロッピーディスクにギヤのアイコンがあるボタンが「設定の保存」です。

「表示の更新」ボタンは、このプログラムの設計当初設置したもので、その名前の通り、このボタンをクリックして表示(描画)を更新するために設けたものです。ですが、すぐに設定値の変更と同時に画面を更新しても動作がそれほど重たくならないことに気づき、画面への描画は自動化することにしました。しかし、何かの拍子に(あぁこの操作のためには、このボタン残しておかないと・・・)と思った記憶があり、つまり、確かに何か明確な理由があって残してある「表示の更新」ボタンなのですが、その・・・確かにあったはずの明確な理由が、今、どうしても思い出せません。

たしかにナニか、あったんだけどなー

なので、あとから思い出せたら追記することにします!

「設定の保存」ボタンは、用紙の選択や向き、タイトル他の設定を一括して exe と同じ場所に自動的に作成される INI ファイルに保存するためのボタンです。INI ファイルはただのテキストファイルなので、メモ帳等のエディタで開いて直接編集することも可能ですが、推奨はしません。設定の変更と保存はなるべくプログラムを起動して行っていただけますよう、お願いいたします。

ちなみに INI ファイルの内容は次の通りです。

[情報Ⅰ  考査①  解答用紙(2026/05/25)]
SectionNmae=情報Ⅰ  考査①  解答用紙(2026/05/25)
PaperSize=A4
Orientation=横
Blocks=4
Rows=25
Cols=8
Space=5
Major=0
Minor=0
StartingNumber=1
Gray=140
TateKeisen=-21
YokoKeisen=0
TopMarginMM=14
TDPosition=30
Copies=1
PDF_Margin=0
SectionNumber=1
Multiple=0
UseKana=0
OffVertLine=1
Option=1
Name=1
rbtn1=1
rbtn2=0
FontSize=10

(7)選択肢数(=列数)の設定

19列(選択肢)を指定すると大語群対応がチェックできるようになります。
(上の図はチェックした状態です)


今回作成したプログラムでは、この Blog の過去の記事で紹介した「 100 選択肢に対応したマークシートリーダー」用のマークシートも作成できます。自分で言うのも何ですが、「 100 選択肢に対応したマークシートリーダー」は好評で、複数の方が定期的に使用してくださっています。ただ、Word や Excel を利用して作成したマークシートでは、私の作成技術が稚拙なため、どうしてもマークとマークの間隔が狭くなってしまい、大きく(乱暴に?)塗りつぶされた場合に複数マーク判定が出やすくなる(大語群の場合には空欄と同様に扱っていますが)という問題がありまして、これをどうにか改善したいと考えたことも、「プログラムでマークシートを作ってみよう!」と思った理由のひとつです。

新しく作成中のマークシートリーダーでも、もちろん、この大語群対応マークシートが読めるようにしてありますが、まだ実際の試験で使用して問題がないかどうかの確認を行っていませんので、現時点での公開はできませんが、この Blog で過去に公開してきたマークシートリーダーより、読み取り速度は比較にならないくらい高速化することに成功しています。設定によりますが、輪郭検出を最初の1枚のみに限定した場合で、私の PC では、1秒間に約47,500マークを読み取り可能です。

【テストに使用したPCのスペック】
プロセッサ	11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz (3.00 GHz)
実装 RAM	32.0 GB (31.7 GB 使用可能)	
システムの種類	64 ビット オペレーティング システム、x64 ベース プロセッサ

【OS】
エディション	Windows 11 Pro
バージョン	25H2

開発中の MS_Reader V3(2026年1月現在)

上の図のように、8選択肢 × 100 設問 × 42 枚 = 33600 マークを 0.708 秒で読みますので、1秒間あたりでは、47457 マークを読める計算になります。

すべてのマークシートについて輪郭検出を行った場合(すべてのマークシートについて、画像左上の ■■■ 座標を検出し、予め登録してあるそこからの位置座標から読み取り範囲を設定する=印刷時やスキャン時のズレ・ブレの問題を完全に解消できます)は、

1秒間あたり、25946 マーク読みます。過去に私が作ったマークシートリーダーの読み取り速度に比較すれば、これでも十分、高速です。

※ 読み取り時間はあくまでも参考です。使用する PC の性能により、この時間は大きく変化します。

(8)設問番号の変更

デフォルト設定の設問番号は、1から始まる数字の連番ですが、□ 設問番号カナにチェックすることで解答欄群(ブロック)毎に半角カタカナのアから五十音順で始まる表記に変更できます。

主に数学の採点用途での使用を想定しましたので、
設問番号をカナ表記にした場合は解答欄群上に大問番号が表示されます。


また、表示される大問番号は任意の値から開始することができます。

・・・と、ここまで記事を書いて、重要な設定をし忘れたことに気づきました。あわわ・・・

プログラムを修正(改良)してから、この続きを書くことにします。

72時間ほど経過しました。

で、ナニをしたかと言うと・・・

共通テストの数学解答用マークシート形式での出力を可能に設定


どうせやるなら、理科も・・・


情報も・・・

16進数に関連した問題が脳裏をヨギリマス。


作り方は簡単。選択された列数が11列、12列、16列であった場合には次のようなメッセージを表示して、ユーザーの返答が「はい」の場合には共通テスト仕様のマークシートを出力するようにプログラムを修正しました。返答が「はい」なら共通テスト仕様の、「いいえ」なら選択肢が1始まり・連番のマークシートが作成されます。

次の列数指定に移動するには「キャンセル」をクリックします。

(9)罫線の設定及び長さの調節

罫線の種類を変更したり、太さを変えたりすることは出来ませんが長さの調整は可能です。

列数や行数を変更すると、ド素人が書いたプログラムですので、計算が微妙に狂って、次のようにブロックの右側や下側の罫線がはみ出したり、足りなくなったりすることがあります。

右側の横罫線がはみ出し
右側の横罫線がはみ出し & 縦の罫線の長さが足りない


このような場合には、□ 設定をチェックして・・・


表示されたコントロールの「縦罫調整」及び「横罫調整」の値を増減させて調整してください。1クリックで1ピクセル単位の調整が可能です。このプログラムは画面サイズ 1366 × 768 での使用を前提として開発していますので、必要なコントロールを幅の制限内に詰め込むため、キャプションは「意味が通じれば可」とし、「罫線」の「線」は省略しました(入力ミスではありません)。


これで次のように修正できます。

ちなみに、罫線の長さ調整コントロールの左にある「群内縦罫線なし」のチェックを OFF にすれば列を示す縦罫線も描画できますが、正確なマークの読み取り処理を実行するには、縦罫線はない方が絶対に良いので、デフォルト設定では「なし」にしています。

   ↓

【縦罫線がある状態】

縦罫線の使用は推奨しません!

(10)上余白・■■■ 位置・氏名記入欄の設定

上の余白を設定すると、解答欄群(ブロック)の高さが変わらないように、下の余白も連動して調整されます。左右の余白は自動設定です(左右の余白を手動で調整することはできません)。

■■■ の位置は、デフォルト設定で画像化したマークシートの左端から300ピクセル以内、上端から300ピクセル以内で、解答欄に重ならないように、また、数学用途の場合は大問番号とも重ならないように位置を設定してください。最も左の解答欄群(ブロック)の設問番号欄の右側縦罫線を上に延長した位置に ■■■ の左側が一致するように設計してあります(左右の位置の調整はできません)。

■■■ は、輪郭検出のターゲットとして利用し、マーク読み取り時に
解答欄群(ブロック)の位置を決定する計算の基点になります。


氏名記入欄は、敢えてマークを利用しない設定としてあります。クラスや番号をマークで・・・と考えたこともあるのですが、実際の運用上では塗りつぶしミスが絶対にあることや、スキャンする前にマークシートの並べ間違い(番号順になっていない・上下が逆さま・裏返し等)のチェックを必ず行う必要があることから、採点業務を円滑に進めるためには「氏名記入欄は手書きが最も良い方法」であると考えました。なお、表示(描画)位置は、シートの右上限定です。

氏名部分の下線から大きくはみ出して氏名を記入する受験者もいます。
解答欄との間に十分な余白を設けているのは、氏名のはみ出し対策です。

(11)印刷

印刷関連のコントロールとその役割です。

追記(20260624)

一部のプリンターに出力しようとすると、データを送信した瞬間に印刷エラーになる場合があることを確認していたのですが、その真の原因は、印刷ジョブを「ドキュメント名が空」のまま送信していたことでした。一部のプリンターは空のジョブ名を受け取るとジョブを拒否してエラーにするようです。

ドキュメント名を何でもいいので指定して、プリンタに出力すれば問題なく印刷できることを本日確認しました。現在、ダウンロードできるプログラムは、この問題に対応済みのものです。

以下の記事は誤りを含んでいますので、ご注意願います。

大変申し訳ありませんが一部のモバイルプリンターで、このプログラムで作成したマークシートを印刷出来ないことを確認しました。機種名は記載しませんが、どうやら一部のモバイルプリンターでは他のプリンターとドライバ互換性や DevMode 処理が異なり、Delphi の TPrinter で印刷エラーが発生しやすいようです。私の場合も PrinterSetupDialog 実行後、GetPrinter / SetPrinter による dmCopies 設定が My モバイルプリンターでは失敗し、BeginDoc できない状態になりました(普段マークシートの印刷に利用している複合機ではちゃんと印刷できました)。

仕方がないのでデータをビットマップに保存し、これを印刷する方法(以下のコードを参照)を試したら印刷できましたが、この方法では余白の設定に問題が残り・・・結局、元の「一部のモバイルプリンターで印刷出来ない」コードに戻しました。

procedure TForm1.PrintImageShell;
var
  TmpFile: string;
  Bmp: TBitmap;
  i: Integer;
begin
  CopyCount := StrToInt(seCopies.Text);
  if (Image1.Picture = nil) or (Image1.Picture.Graphic = nil) then Exit;

  Bmp := TBitmap.Create;
  try
    Bmp.Assign(Image1.Picture.Graphic);
    TmpFile := ExtractFilePath(ParamStr(0)) + 'temp_print.bmp';
    Bmp.SaveToFile(TmpFile);

    for i := 1 to CopyCount do
    begin
      //ShellExecute(Handle, 'print', PChar(TmpFile), nil, nil, SW_HIDE);
      ShellExecute(Handle, 'open', 'mspaint.exe', PChar('/p "' + TmpFile + '"'), nil, SW_HIDE);
      Sleep(3000);  //プリンタ待ち延長
    end;
    Sleep(5000);  //全印刷完了待ち
    DeleteFile(TmpFile);
  finally
    Bmp.Free;
  end;
end;

ですので、このプログラムを利用してマークシートを作成した場合、すべてのプリンターで印刷できるわけではないことを申し添えます。

追記(20260330)

以前、PDF 関連のプログラムを書いた際にその存在を知り、PC にインストールしておいた、ドイツの Geek Software GmbH が開発した PDF24 Creator に出力し、PDF24 Creator から上記の「印刷できなかったモバイルプリンター」への出力を試したところ、無事、印刷できました。もちろん、余白等の問題も発生しませんでした。 PDF24 は、“完全無料で広告のみで運営されている” PDF ツール群で、オンライン版(PDF24 Tools)とオフライン版(PDF24 Creator)があります。利用したのは(名前からおわかりかと思いますが)、オフライン版(PDF24 Creator)です。

【正答シート・読み取りテスト用シートの印刷】

正答シート・読み取りテスト用シートの印刷を実行すると、次のメッセージが表示されます。

「はい」を選択:正答シートを1枚印刷
「いいえ」を選択:読み取りテスト用シートを指定枚数分印刷
「キャンセル」:処理を終了

正答シート・読み取りテスト用シートを印刷するためには、次のデータをCSV形式で指定 Path に準備する必要があります(開発中のマークシートリーダーでも、このデータを使用します)。

指定 Path:exe があるフォルダ\ProcData\採点用マークシート画像を保存したフォルダ

指定ファイル名:MS_ScoringData.csv

1行目の第1列から設問数分、正答データを入力してください。
数学なら-(マイナス)記号、理科なら a, b、情報なら a, b, c, d, e, f の入力に対応しています。

「はい」を選ぶと作成される正答シートの例:

マークシートリーダーの読み取りテストに使用できます。


「いいえ」を選ぶと作成される読み取りテスト用マークシートの例:

空欄や誤答マーク(複数マーク)入りの読み取りテスト用マークシートを作成できます。
※ 印刷実行前に印刷枚数を設定してください。


実際の操作は次の流れになります。

PrinterSetupDialog が表示されます。
設定を確認し、OK をクリックしてください。
数学・理科・情報の場合に確認メッセージが表示されます。

MS_ScoringData.csv のあるフォルダを指定してください。

(12)セクション名の追加と削除(の仕様)

設定の保存ボタンをクリックすると、現在表示されているマークシートの設定が exe と同じ場所に自動的に作成される INI ファイル内に「タイトル名をセクション名として」保存されます。次回、プログラムを起動する際、プログラムは自動的に INI ファイル内のセクション名を取得して、タイトルを表示する ComboBox の選択肢に設定します。

セクションの削除と、画面の表示は、『連動しない仕様』としてある点にご注意願います。

例えば、「情報Ⅰ」と「情報Ⅱ」の2種類のマークシート設定があり、「情報Ⅱ」を表示している状態で「情報Ⅱ」のセクションを INI ファイルから削除したとします。

マークシート設定の削除は、このアイコンをクリックして実行します。
削除したいタイトル(セクション名)を表示されたリストから選んで右クリックし、
さらに表示されるサブメニューの削除をクリックしてください。

確認メッセージが表示されます。

「はい」をクリックすると、セクションが削除されます。

INI ファイル内のマークシート設定(セクションとして管理)はこの操作で削除されますが、例え、現在表示されているマークシート設定(のセクション情報)を削除したとしても、画面の描画内容には影響を与えません。単に、次回から(その設定を)呼び出せなくなるだけです。

この例では、「情報Ⅱ」のマークシート画面を表示しながら、「情報Ⅱ」のマークシート設定を削除しましたが、「情報Ⅱ」のマークシート画面は表示されたままになっています。このままプログラムを終了してもマークシート設定の保存の可否を確認するメッセージ等は表示されません。

このように、現在の描画内容はセクション名とは無関係に、独立した存在として扱う仕様となっていることにご留意いただけますよう、お願い申し上げます。

2.sk4d.dllについて

このプログラムは PDF の作成に Googleが中心となって開発している、オープンソースの強力な2D描画ライブラリ Skia (スキア)を使用しています。ですので正常に動作させるには、exe と同じ場所に Skia の描画エンジン本体(sk4d.dll)が必須です(ダウンロード Zip に同梱されています)。

このプログラムの正常な動作に必要なファイルの一覧です。

sk4d.dll のライセンスはMIT Licenseです。ですので、アプリ(商用を含む)への組み込み・再配布・改変まで自由にできますが、著作権表示とライセンス文の同梱が必要です。

※ MarksheetMaker は非商用・有償販売禁止という利用条件でダウンロードとその使用を可としておりますので、ご注意ください。

3.ダウンロード

今回の記事でご紹介した MarksheetMaker プログラム一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt をご覧ください。本ソフトウェアは PDF ファイルの出力に Skia を使用しています。Skia のライセンスは MIT ライセンスです。このライセンスの詳細につきましては同梱した Skia4Delphi_and_Skia_LICENSE.txtをご覧ください。

また、動作には sk4d.dll が必要です。MarksheetMaker.exe と同じフォルダ内に sk4d.dll を置いてプログラムを実行してください。

4.まとめ

今回の記事でご紹介した MarksheetMaker は、当 Blog の過去記事で紹介したマークシートリーダー及び現在開発中の超高速・軽量マークシートリーダー( MS_Reader V3 )で利用可能なマークシートを作成・印刷するプログラムです。

5.お願いとお断り

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

最小構成の OpenCV で輪郭検出に成功!

すべてを忘れて、無我夢中で、書きました!
自分史上、最高速のマークシートリーダーです。
100 設問8選択肢のマークシート 42 枚を、My PC では、最短 0.902 秒で読みます。

ちゃんとマークしてない、~ みたいな線でもしっかり読んでくれます・・・

 ↑ この画面左下部分を拡大

1枚ではありません。42枚(33,600個のマーク)読んでます。


しかも、このサイズ! フロッピーディスク2枚に収ま・・・らないか。3枚なら余裕。
(収めたくても、フロッピー。もぉ、ないけど!)


このプログラムは、まだ実際の採点現場で動作検証しておりませんので公開できませんが、このプログラムで利用した Secret Weapon 的存在が Delphi 用の OpenCV です。

・・・ ということで、今回は「マークシートリーダー的なプログラムを作るために必要な、最小構成の OpenCV 」についてのお話です。

【もくじ】

1.そもそも OpenCV とは?
2.最小構成のOpenCVで画像を表示
3.最小構成のOpenCVで輪郭検出
4.まとめ
5.お願いとお断り

1.そもそも OpenCV とは?

正式名称は Open Source Computer Vision Library。よく説明に使われるのは「ソースコードが公開されている画像処理ライブラリ」、つまり、誰でも自由に見たり、使ったり、改良したりできる仕組みを持った、みんなで育てるソフトウェアということでしょうか。

自分的には『 コンピュータの眼 』だと、思っとります。

Python を学ぶ課程で、OpenCV を利用すると様々な画像処理が手軽に実装できることを知りました。例えばマークシートの解答欄を検出するための基準となる特徴点の検出や、解答欄矩形の座標の取得等、OpenCV のライブラリを使えば「夢みたいなこと」が「走召!」簡単とは言えませんが、僕でも勉強すれば、Python を介して何とか実装できることを知ったのです!

知ったのですが・・・

ただ、僕には、それを直接 Delphi から、使うことはできませんでした・・・

それを導入しようとしても・・・、エラーが、いっぱい、出て。

だから、今、思えば、ものすごく遠回りをして、OpenCV の機能を使ってきました。Python4Delphi や Embeddable Python には、感謝しても、感謝しきれませんが・・・。

今回、生成 AI から様々なヒントをもらい、ようやく My PC で最小構成の OpenCV を使って画像処理を実行することに成功しました!

出来てしまえば、それは、あっけないくらい、カンタンなことでしたが。

もしかしたら・・・、ほんとうに、もしかしたら、ですが、僕と同じように、画像の表示・輪郭検出等が可能な最小構成の OpenCV の実装方法について悩んでいらっしゃる方がいるかもしれないと思い、(自分的には私的究極の目的であるマークシートリーダーを作成するために必要な・・・という但し書き付きですが)、僕的に必要とする最小構成の OpenCV について、ごく浅い、拙い経験を元に、記事としてここに残すことにした次第です。

2.最小構成のOpenCVで画像を表示

ナニはともあれ、OpenCV がなくては何にも始まりませんので、まず、最初にコレを入手します。

ただし、イロイロと問題がありまして、私の場合、最新版ではないモノが欲しいのです。なぜなら、作りたいプログラムは 32 ビット版だから。となると、現在入手可能な 32 ビット版の OpenCV はこれ一択のようです。(間違っていたらごめんなさい)

https://github.com/Laex/Delphi-OpenCV

それからDelphi 環境で OpenCV を実行するためには OpenCV の DLL が必要なので、SourceForge から OpenCV 2.4.13.6 をダウンロード。こっちは『OpenCV 2.4.13.6』で検索してヒットする SourceForge の Download opencv-2.4.13.6-vc14.exe (OpenCV) リンクをクリックしたら Windows 用バイナリが勝手にダウンロードされたので、これを展開。

展開先の opencv-2.4.13.6-vc14\opencv\build\x86\vc14\bin に DLL があるので、必要な DLL を Delphi で作成する exe があるフォルダにあとからコピーします。

あと、OpenCV 2.4.13.6 の Windowsビルドは Visual C++ 2015 (VC14) でコンパイルされているそうなので、msvcp140.dll や vcruntime140.dll が必要。

https://my.visualstudio.com/Downloads/Featured?mkt=ja-jp

上のリンク先『ダウンロードとプロダクト キー』へ行き、表示される画面の左側の『Developer Tools』に『Visual Studio 2022』(リンク)をクリック。

表示されたページの中ほどに「Visual C++ Redistributable for Visual Studio 2022」があるので、その右側の ComboBox みたいな部分で x64 を x86 に変更して『ダウンロード』ボタンをクリック。

「Visual C++ Redistributable for Visual Studio 2022」は「VC_redist.x86.exe」で保存されました。これをインストールすると、2015 以降のランタイム(2015, 2017, 2019, 2022)は統合されているため、「Visual C++ 2015 Redistributable」も含まれてインストールされるようです。

実際のインストール作業では、VC_redist.x86.exe をダウンロードから任意のフォルダにコピペして、ダブルクリックして実行(実行時、インストール画面はタスクバー内に出現するので注意!)→ インストール完了後、システムの再起動が必要でした。

再起動後、PowerShell(x86) を起動して確認。x86 版のインストールを確認するには、PowerShell も x86 版でないといけません。x64 版で確認しようとして『パスが存在しないため検出できません』と赤文字で表示され、慌てふためいた経験を持つ人がここに約1名。ぎゃはは。

PowerShell(x86)で次のコマンドを入力してインストール結果を確認します。

PS C:\Users\xxx> Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86"
画面中ほどに、Installed : 1 を発見!


また、コントロールパネルの「プログラムと機能」を開いて存在を確認。


DLL の存在も確認。PowerShell で次のコマンドを入力して True が返ることを確認します。

Test-Path "C:\Windows\SysWOW64\msvcp140.dll"
Test-Path "C:\Windows\SysWOW64\vcruntime140.dll"
これは x64 版でも、x86 版でも可能でした( x64 版の方がカラフル!)

で、先にダウンロードしていた Delphi-OpenCV-master.zip も任意の場所に展開して、source フォルダの中にある拡張子が .pas のファイルから次のファイルのみ、Delphi のプロジェクトファイル(.dproj)があるフォルダへコピーします。


ちなみに各ファイルの役割分担は・・・

型定義 → ocv.compat.pas
画像構造体(IplImage) → ocv.core.types_c.pas
コア処理 → ocv.core_c.pas
画像読み込み / 保存 → ocv.highgui_c.pas
画像処理(ぼかし・二値化・輪郭など) → ocv.imgproc_c.pas
ユーティリティ → ocv.utils.pas

これだけで次の機能はすべて使えました!

・画像読み込み(cvLoadImage)
・グレースケール化
・ぼかし
・二値化
・輪郭抽出(マーク位置判定の要)
・画像の部分切り出し

「opencv-2.4.13.6-vc14\opencv\build\x86\vc14\bin に DLL がある」と先に言いましたが、次の DLL を exe と同じ場所へコピーします(先に Form だけ作っておいて、1回、実行して exe を作成しておく必要があります)。

将来、作成したプログラムを公開(配布)する予定がある場合は、OpenCV 本体の LICENSE ファイルをここでダウンロードして exe と同じ場所に DLL と一緒に置いておくのがよいと思います。OpenCV を配布する際は、Delphi‑OpenCV の LICENSE( MPL-1.1.txt )に加えて、OpenCV 本体の LICENSE ファイルも同梱して配布する必要があるからです。忘れないように必ずやっておきましょう。

ちなみに、OpenCV 本体の LICENSE ファイルは、ダウンロードする必要があるようです。次の場所から入手できました。

https://github.com/opencv/opencv/blob/2.4/LICENSE

これでプログラミングする準備ができました!
OpenCV で画像を表示するコードを書いて、本当に OpenCV が使えるか? 確認です。

Form1 の上に Memo1 と Button1 と ScrollBox1(親)と Image1(子 )を置いて、Memo1 の Align を alBottom に設定し、続けて Button1 も Align を alBottom に設定しておいて、ScrollBox1 の Align を alClient に設定します。Form1 は、最大化状態で表示されるよう、オブジェクトインスペクタで WindowState プロパティを wsMaximized にしておきます。

構造ペインを見ていただければ、ScrollBox1 の存在と親子関係がわかるかと・・・


exe と同じ場所にマークシートの画像( sheet.jpg )を用意して、次のコードを書きます。
あと Memo1 の Lines は Memo1 を消して空にしておきます。

procedure TForm1.FormShow(Sender: TObject);
var
  img: pIplImage;
begin
  img := cvLoadImage(
    PAnsiChar(AnsiString(ExtractFilePath(Application.ExeName) + 'sheet.jpg')),
    CV_LOAD_IMAGE_COLOR);
  if Assigned(img) then
  begin
    frameBitmap := TBitmap.Create;
    frameBitmap.PixelFormat := pf24bit;
    frameBitmap.HandleType := bmDIB;

    IplImage2Bitmap(img, frameBitmap);

    //Image1 のサイズを画像に合わせて変更
    Image1.Picture.Assign(nil);
    Image1.Width  := frameBitmap.Width;
    Image1.Height := frameBitmap.Height;

    //表示
    Image1.Picture.Bitmap.Assign(frameBitmap);

    Memo1.Lines.Add('Loaded and displayed sheet.');
    cvReleaseImage(img);
  end else
    Memo1.Lines.Add('Failed!');
end;

実行してみました!

やった! やった!!

ちなみに、マウスのホイールで画像をスクロールさせるなら、ScrollBox1 の OnMouseWheel 手続きを作成して、次のコードを記述します。

procedure TForm1.ScrollBox1MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
const
  ScrollStep = 40; //ホイール1回の移動量
begin
  if ssShift in Shift then
  begin
    //横スクロール(Shift + ホイール)
    ScrollBox1.HorzScrollBar.Position :=
      ScrollBox1.HorzScrollBar.Position - (WheelDelta div 120) * ScrollStep;
  end
  else
  begin
    //縦スクロール(通常)
    ScrollBox1.VertScrollBar.Position :=
      ScrollBox1.VertScrollBar.Position - (WheelDelta div 120) * ScrollStep;
  end;
  //処理済み
  Handled := True;
end;

ながーい間、夢だった OpenCV で( Python の力も借りずに)画像が表示できました!
自分には無理だとあきらめていた時期もありましたので、まさに感無量ですが、まだ確認しなければならないことがあります。

それは『輪郭検出』。

マークシートリーダーを作成するために OpenCV が必要な理由のすべてがここにあります。

私が過去に作ったマークシートリーダーでは、マークシートの左上に印刷されたトリプル・ドット。この ■■■ の左上の座標を起点として各設問群矩形までの距離を計測(左上・右下の座標を取得すれば矩形の幅と高さが計算できる)し、各設問群のブロックを切り出して、それを行、そして列に分割、ひとつひとつのマークを切り出して塗り面積の大きなものを「マークあり」と判定しています。

マークシートは印刷物ですから、1枚1枚ごとにどうしても微妙な印刷位置のズレが発生するのは避けられません(現在のプリンタは優秀でほとんどズレませんが)。また、マークシートをスキャナーでスキャンした際にも、1枚ごとに異なる微妙なズレや傾きが発生します。しかし、■■■ と各設問群矩形の距離は紙が濡れて乾いた後ならともかく、通常の状態では絶対に変化しません。ですので、最初に ■■■ の位置を検出して、そこからの距離を用いてマークシートを切り出せば、印刷位置のズレに強い、すなわち、マーク切り出し位置が「ブレない」プログラムが作れます。

ですので、この『輪郭検出』が安定して成功するか・どうががマークシートリーダーそのものの成否のカギを握っています。

『輪郭検出』 はたして、それが出来るか、どうか?

3.最小構成のOpenCVで輪郭検出

Button1 のクリックイベント(手続き)に、次のコードを書きます。

procedure TForm1.Button1Click(Sender: TObject);
const
  {$IFNDEF CV_GAUSSIAN}
  CV_GAUSSIAN = 2;
  {$ENDIF}
  {$IFNDEF CV_GRAY2BGR}
  CV_GRAY2BGR = 8;
  {$ENDIF}
  {$IFNDEF CV_THRESH_BINARY_INV}
  CV_THRESH_BINARY_INV = 1;
  {$ENDIF}

type
  TRectInfo = record
    x, y, w, h: Integer;
  end;

var
  src, gray, bin: pIplImage;
  colorImg: pIplImage;
  contours, c: pCvSeq;
  storage: pCvMemStorage;
  rect: TCvRect;
  bmp: TBitmap;
  y, i, j, k, l: Integer;
  Lcandidates: array of TRectInfo;
  ti: TRectInfo;
  found: Boolean;
  bestIdx: Integer;
  dx1, dx2: Integer;
  minX, minY, maxX, maxY: Integer;
  strMsg: string;
  LeftX, TopY: Integer;
  intX, intY: Integer;
  rowHeight, colWidth: Double;
  x1, x2, y1, y2: Integer;
  R: TRect;
  roi: pIplImage;
  meanVal: TCvScalar;
  intRowNum, intSelNum: integer;
  GridRow: Integer;
  area: Integer;
  marks: TStringList;
  roiArea: Integer;
  whiteRatio: Double;
  ratios: array of Double;
  maxRatio: Double;
  BNum, ENum: Integer;
  localRow: Integer;
  orig: pIplImage;
  bmpOrig: TBitmap;

  function SafeSetROI(img: pIplImage; x1,y1,x2,y2: Integer): Boolean;
  begin
    Result := False;
    if img = nil then Exit;
    if (x1 < 0) or (y1 < 0) then Exit;
    if (x2 > img^.width) or (y2 > img^.height) then Exit;
    if (x2 <= x1) or (y2 <= y1) then Exit;

    cvSetImageROI(img, cvRect(x1,y1,x2-x1,y2-y1));
    Result := True;
  end;

begin

  Memo1.Clear;

  src := cvLoadImage(PAnsiChar(AnsiString('sheet.jpg')), CV_LOAD_IMAGE_GRAYSCALE);
  if src = nil then
  begin
    Memo1.Lines.Add('マークシート画像が読み込めません');
    raise Exception.Create('画像読み込み失敗');
  end;

  gray := cvCloneImage(src);
  cvSmooth(src, gray, CV_GAUSSIAN, 5, 5);

  bin := cvCreateImage(cvSize(gray^.width, gray^.height), IPL_DEPTH_8U, 1);
  cvThreshold(gray, bin, 80, 255, CV_THRESH_BINARY_INV);

  storage := cvCreateMemStorage(0);
  contours := nil;
  cvFindContours(bin, storage, @contours,
                 SizeOf(TCvContour),
                 CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE,
                 cvPoint(0,0));

  colorImg := cvCreateImage(cvSize(gray^.width, gray^.height), IPL_DEPTH_8U, 3);
  cvCvtColor(gray, colorImg, CV_GRAY2BGR);

  SetLength(Lcandidates, 0);
  c := contours;
  while c <> nil do
  begin
    rect := cvBoundingRect(c, 0);

    if (rect.x < 400) and (rect.y < 250) and
       (rect.width >= 10) and (rect.width <= 30) and
       (rect.height >= 10) and (rect.height <= 30) and
       (Abs(rect.width - rect.height) <= 6) then
    begin
      ti.x := rect.x;
      ti.y := rect.y;
      ti.w := rect.width;
      ti.h := rect.height;
      i := Length(Lcandidates);
      SetLength(Lcandidates, i + 1);
      Lcandidates[i] := ti;
      Memo1.Lines.Add(Format('candidate: x=%d y=%d w=%d h=%d', [ti.x, ti.y, ti.w, ti.h]));
    end;

    c := c^.h_next;
  end;

  if Length(Lcandidates) < 3 then
  begin
    Memo1.Lines.Add('候補が3未満のため、条件を緩めて再収集します');
    SetLength(Lcandidates, 0);
    c := contours;
    while c <> nil do
    begin
      rect := cvBoundingRect(c, 0);
      if (rect.x < 500) and (rect.y < 400) and
         (rect.width >= 6) and (rect.width <= 60) and
         (rect.height >= 6) and (rect.height <= 60) and
         (Abs(rect.width - rect.height) <= 12) then
      begin
        ti.x := rect.x; ti.y := rect.y; ti.w := rect.width; ti.h := rect.height;
        i := Length(Lcandidates); SetLength(Lcandidates, i + 1); Lcandidates[i] := ti;
        Memo1.Lines.Add(Format('relaxed candidate: x=%d y=%d w=%d h=%d', [ti.x, ti.y, ti.w, ti.h]));
      end;
      c := c^.h_next;
    end;
  end;

  if Length(Lcandidates) < 3 then
  begin
    strMsg:='特徴点が見つかりませんでした'+ sLineBreak +'処理を中止します。';
    Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
    bmp := TBitmap.Create;
    try
      bmp.PixelFormat := pf24bit;
      bmp.Width := colorImg^.width;
      bmp.Height := colorImg^.height;
      for y := 0 to bmp.Height - 1 do
        Move((colorImg^.imageData + y * colorImg^.widthStep)^, bmp.ScanLine[y]^, colorImg^.width * 3);
      DrawBitmapKeepAspect(Image1, bmp);
    finally
      bmp.Free;
    end;
    cvReleaseImage(colorImg);
    cvReleaseImage(bin);
    cvReleaseImage(gray);
    cvReleaseImage(src);
    cvReleaseMemStorage(storage);
    Exit;
  end;

  for i := 0 to Length(Lcandidates) - 2 do
    for j := i + 1 to Length(Lcandidates) - 1 do
      if Lcandidates[i].x > Lcandidates[j].x then
      begin
        ti := Lcandidates[i];
        Lcandidates[i] := Lcandidates[j];
        Lcandidates[j] := ti;
      end;

  found := False;
  bestIdx := -1;
  for i := 0 to Length(Lcandidates) - 3 do
  begin
    if (Abs(Lcandidates[i].y - Lcandidates[i+1].y) <= 8) and (Abs(Lcandidates[i].y - Lcandidates[i+2].y) <= 8) then
    begin
      dx1 := Lcandidates[i+1].x - Lcandidates[i].x;
      dx2 := Lcandidates[i+2].x - Lcandidates[i+1].x;
      if (dx1 >= 10) and (dx1 <= 60) and (dx2 >= 10) and (dx2 <= 60) and (Abs(dx1 - dx2) <= 12) then
      begin
        found := True;
        bestIdx := i;
        Break;
      end;
    end;
  end;

  if not found then
  begin
    strMsg:='特徴点が見つかりませんでした(詳細候補数=' + IntToStr(Length(Lcandidates)) + ')'+ sLineBreak +'処理を中止します。';
    Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
    bmp := TBitmap.Create;
    try
      bmp.PixelFormat := pf24bit;
      bmp.Width := colorImg^.width;
      bmp.Height := colorImg^.height;
      for y := 0 to bmp.Height - 1 do
        Move((colorImg^.imageData + y * colorImg^.widthStep)^, bmp.ScanLine[y]^, colorImg^.width * 3);
      DrawBitmapKeepAspect(Image1, bmp);
    finally
      bmp.Free;
    end;
    // cleanup
    if colorImg <> nil then cvReleaseImage(colorImg);
    if bin <> nil then cvReleaseImage(bin);
    if gray <> nil then cvReleaseImage(gray);
    if src <> nil then cvReleaseImage(src);
    if storage <> nil then cvReleaseMemStorage(storage);
    Exit;
  end;

  minX := Lcandidates[bestIdx].x;
  minY := Lcandidates[bestIdx].y;
  maxX := Lcandidates[bestIdx].x + Lcandidates[bestIdx].w;
  maxY := Lcandidates[bestIdx].y + Lcandidates[bestIdx].h;

  for i := 1 to 2 do
  begin
    if Lcandidates[bestIdx + i].x < minX then
      minX := Lcandidates[bestIdx + i].x;
    if Lcandidates[bestIdx + i].y < minY then
      minY := Lcandidates[bestIdx + i].y;
    if (Lcandidates[bestIdx + i].x + Lcandidates[bestIdx + i].w) > maxX then
      maxX := Lcandidates[bestIdx + i].x + Lcandidates[bestIdx + i].w;
    if (Lcandidates[bestIdx + i].y + Lcandidates[bestIdx + i].h) > maxY then
      maxY := Lcandidates[bestIdx + i].y + Lcandidates[bestIdx + i].h;
  end;

  cvRectangle(
    colorImg,
    cvPoint(minX, minY),
    cvPoint(maxX, maxY),
    CV_RGB(255, 0, 0),
    3);

  Memo1.Lines.Add(Format(
    '外接矩形: x=%d y=%d w=%d h=%d',
    [minX, minY, maxX - minX, maxY - minY]
  ));

  bmp := TBitmap.Create;
  try
    bmp.PixelFormat := pf24bit;
    bmp.Width := colorImg^.width;
    bmp.Height := colorImg^.height;
    for y := 0 to bmp.Height - 1 do
      Move((colorImg^.imageData + y * colorImg^.widthStep)^, bmp.ScanLine[y]^, colorImg^.width * 3);

    ScrollBox1.HorzScrollBar.Position := 0;
    ScrollBox1.VertScrollBar.Position := 0;
    Image1.Align := AlNone;

    Image1.Picture.Graphic := nil;
    Image1.Stretch := False;
    Image1.Left := 0;
    Image1.Top := 0;
    SetStretchBltMode(Image1.Canvas.Handle, HALFTONE);
    Image1.Picture.Bitmap := bmp;

  finally
    bmp.Free;
  end;

  //特徴点検出成功時の追加処理
  //if chkInfo.Checked then  //直後にぼかしのない画像を表示しているので、案内は必ず必要!
  //begin
    strMsg := '特徴点が正しく検出されたことを確認し、設問群数を設定してください。'+#13#10+
    '※設問群数: 解答欄を内包する幅と高さが同じ矩形の個数';
    Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
  //end;

  //元画像(ぼかしなし)を再読み込みして表示
  orig := cvLoadImage(
    PAnsiChar(AnsiString('sheet.jpg')),
    CV_LOAD_IMAGE_COLOR
  );

  if orig <> nil then
  begin
    bmpOrig := TBitmap.Create;
    try
      bmpOrig.PixelFormat := pf24bit;
      bmpOrig.Width := orig^.width;
      bmpOrig.Height := orig^.height;

      for y := 0 to bmpOrig.Height - 1 do
        Move(
          (orig^.imageData + y * orig^.widthStep)^,
          bmpOrig.ScanLine[y]^,
          orig^.width * 3
        );

      //初期化
      ScrollBox1.HorzScrollBar.Position := 0;
      ScrollBox1.VertScrollBar.Position := 0;
      Image1.Align := alNone;
      Image1.Picture.Graphic := nil;
      Image1.Stretch := False;
      Image1.Left := 0;
      Image1.Top := 0;

      Image1.Picture.Bitmap := bmpOrig;

    finally
      bmpOrig.Free;
      cvReleaseImage(orig);
    end;
  end;

  // cleanup
  if colorImg <> nil then cvReleaseImage(colorImg);
  if bin <> nil then cvReleaseImage(bin);
  if gray <> nil then cvReleaseImage(gray);
  if src <> nil then cvReleaseImage(src);
  if storage <> nil then cvReleaseMemStorage(storage);

end;

祈るような気持ちで、実行!

OpenCV は ■■■ をしっかり見つけてくれました!

これさえ出来れば、この勝負、
もう勝ったようなものです。

あとは・・・

日に夜を次いでプログラミングに没頭し、

自分史上最高のマークシートリーダーを完成させるだけです!

4.まとめ

(1)32 bit 環境で使える OpenCV は opencv-2.4.13.6-vc14 がある。
(2)動作には Visual C++ Redistributable for Visual Studio 2022 のインストールも必要だった。
(3)必要最小限の .pas と OpenCV の DLL を用意すればマークシートリーダーは作成可能。

5.お願いとお断り

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

PaintBoxでラバーバンド

現在、開発中の軽量&超高速マークシートリーダー

( 50 設問のマークシート 40 枚を1秒程度で読んでくれます!)

僕にはずっと以前から、どうしてもやりたかったことが1つあって、それは Delphi 用の OpenCV ラッパー(OpenCV の C/C++ API を Delphi から呼び出すための橋渡しするプログラム)を使って、軽量かつ超高速なマークシートリーダーを作ること。

初めてマークシートリーダーのプログラムを書いた時は、もうそれが動いただけで本当にうれしかった。マークシート 40 枚を読み取るのにたとえ数分!かかっても大満足だった。

でも、だんだん、もう少し、早くできないかな・・・って思うようになって、PythonForDelphi を使ってバックグラウンドで Python 用の OpenCV を動かして、最初に作ったものよりは・・・かなり高速に動作するバージョンアップ版(?)をなんとか完成させた。

あの頃はもちろん AI なんてなかったし、Google 先生だけが頼りで、わからないことを解決して前へ進むのに今の何倍も時間が必要だったけれど、読み取り速度がどうあろうと、期待した通りにちゃんと動くマークシートリーダーが出来たことが、むちゃくちゃうれしかった。ただ、OpenCV をはじめとして、どうしても必要なライブラリをインストールした Embeddable Python は 158 MB くらいになっちゃって(泣)。

Zip ファイルを展開するにも時間がかかるし、それより何より、初回起動時に Windows による DLL のチェックが走って、マシンによっては数分間フリーズ状態が発生して・・・

いちばんイイのは、Delphi 用の OpenCV ラッパーを使ってプログラムを書けばいいんだって、頭ではわかっていたんだけれど、どうしてもそれが出来なかった。なぜかって言うと、答えは簡単。僕には Delphi 用の OpenCV ラッパーを PC に導入して動かすことが、どうしても出来なかったんだ。

エラーが、たくさん、出て。そして、それを自力で解決することができなかった・・・。

先日、ふと思い立って、マークシートリーダーを作るために必要な必要最低限の OpenCV ラッパーの構成を AI に訊いてみた。少し苦労したけど、なんとか、最小構成の OpenCV ラッパーを僕の PC に導入することに成功。夢の実現にやっと一歩、近づけた。

そこで、新しいマークシートリーダーを作り始める前に準備しておこうと思ったのが、今回紹介する PaintBox で作ったラバーバンド。マークシートのマーク欄のブロックの座標を取得する際に、これがどうしても必要なんだけれど、夢に描いたような・・・1ピクセル単位で大きさの微調整が可能で、その内部をドラッグすれば、なめらかに移動も出来るラバーバンドは自分史的にはまだ未実装・・・。ずっと、ずっと、長い間、微調整が可能な、思った通りに動いてくれるラバーバンドが作りたかった。

今回、あくまでも自分的には・・・ですが、かなり満足できるものができたので、どなた様かの参考になればと思い、自分自身の備忘録も兼ねて、ここに書いておくことにしました。

【もくじ】

1.機能
2.実装
3.まとめ
4.お願いとお断り

1.機能

ラバーバンドに持たせたい機能は次の通り。

・マウスで TImage 上をドラッグして、ラバーバンド描画(赤の細い点線を使用)
・四隅+縦横中央の 8 ハンドル(グラブハンドル)でサイズ変更可能
・ラバーバンド内をクリックで「そのままの大きさ」でドラッグ移動
・ドラッグ中に自動スクロール(ドラッグ中にスクロール領域の端に来たらスクロール)
・描画終了(MouseUp)でラバーバンドの画像座標を出力
・ラバーバンド内クリックでフォーカスを取得
・矢印キーで、その向きに 1px 移動、Shift+矢印で辺ごとに 1px 拡大/縮小
・ラバーバンド外クリックで消去

完成したラバーバンド

2.実装

点線の枠は、左が Image1、右が PaintBox

構造ペインで親子関係も確認


Delphi を起動して、「ファイル」→「新規作成」→「Windows VCL アプリケーション」

Form1 上に、ScrollBox1 を置いて、その上に Image1 と PaintBox1 を置く。

あと Button1 と Memo1 も用意(見た目にこだわるなら、Memo1の lines プロパティを開いて、Memo1 というデフォルトで入っている文字列を消去しておいた方がいいかも?)。

Align の設定は、特にこだわりがなければ、Memo1 は alBottom に、続けて Button1 も alBottom に設定、ScrollBox1 は alClient に設定する。

Form を常に最大化して表示したければ、Form1 の WindowState プロパティを wsMaximized にする。

ここで1回実行(F9)して exe を作り、exe と同じ場所に任意のBMP(ビットマップ)画像を置いておく。


interface 部のコードは次の通り。

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.StdCtrls;

//ラバーバンド用に宣言
type
  THandlePos = (hpNone, hpLeftTop, hpTop, hpRightTop, hpRight,
                hpRightBottom, hpBottom, hpLeftBottom, hpLeft);

type
  TForm1 = class(TForm)

変数は次のように準備する。

  private
    { Private 宣言 }
    FBitmap: TBitmap;

    //Rubber band
    FRubberBandActive: Boolean;
    FHasRubber: Boolean;
    FRubberRect: TRect;

    FDragging: Boolean;
    FModeMoveSize: Boolean;
    FDragHandle: THandlePos;

    FStartPointImg: TPoint;

    //Zoom
    FZoom: Double;  //将来の拡張に備えて準備したもの

    //Scroll
    FAutoScrollMargin: Integer;
    FAutoScrollStep: Integer;


まずは、FormCreate 手続きを作成。

オブジェクトインスペクタをイベントに切り替えて、OnCreate をダブルクリック。


次の内容を記述する。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //フォームがキーを受け取る
  Form1.KeyPreview := True;

  FBitmap := TBitmap.Create;
  Image1.Stretch := False;
  Image1.AutoSize := False;
  Image1.Center := False;

  //初期設定
  Image1.Align := AlNone;
  Image1.Stretch := False;
  Image1.AutoSize := False;
  Image1.Center := False;

  FRubberBandActive := False;
  FHasRubber := False;
  FDragging := False;
  FDragHandle := hpNone;
  FModeMoveSize := False;

  FZoom := 1.0;  //拡大表示が将来必要になった時への備え

  FAutoScrollMargin := 16;  //適宜変更してください
  FAutoScrollStep := 10;

  //PaintBox を Image1 上に重ねる
  PaintBox1.Parent := ScrollBox1;
  PaintBox1.Left := Image1.Left;
  PaintBox1.Top := Image1.Top;
  PaintBox1.Width := Image1.Width;
  PaintBox1.Height := Image1.Height;
  PaintBox1.BringToFront;
  //透明に見せる設定
  PaintBox1.ControlStyle := PaintBox1.ControlStyle - [csOpaque];

  //PaintBox を Image1 上に重ねる
  PaintBox1.Parent := ScrollBox1;
  PaintBox1.Left := Image1.Left;
  PaintBox1.Top := Image1.Top;
  PaintBox1.Width := Image1.Width;
  PaintBox1.Height := Image1.Height;
end;


続いて、Create と同様にして FormDestroy 手続きを準備。ビットマップを解放し忘れたらたいへんだ。

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if assigned(FBitmap) then
  begin
    FBitmap.Free;
  end;
end;


次に、Button1 をダブルクリックして、Button1Click 手続きを作成。

procedure TForm1.Button1Click(Sender: TObject);
begin

  //読み込み(ビットマップ)
  FBitmap.LoadFromFile('Sheet-001.bmp');

  //ScrollBox のスクロール位置をリセット
  ScrollBox1.HorzScrollBar.Position := 0;
  ScrollBox1.VertScrollBar.Position := 0;

  Image1.Align := AlNone;
  //画像をクリア
  Image1.Picture.Graphic := nil;
  Image1.Stretch := False;
  Image1.AutoSize := False;  //UpdateImageDisplaySize に任せる
  Image1.Center := False;
  Image1.Left := 0;  //ScrollBox 左上に合わせる
  Image1.Top := 0;

  //画像を表示
  Image1.Picture.Bitmap.Assign(FBitmap);

  UpdateImageDisplaySize;

  Image1.Invalidate;
  Memo1.Lines.Add('Loaded: ' + 'Sheet-001.bmp');

  FRubberBandActive := not FRubberBandActive;
  if not FRubberBandActive then
  begin
    FHasRubber := False;
    PaintBox1.Invalidate;
  end;

  PaintBox1.BringToFront;

end;

UpdateImageDisplaySize が「未定義の識別子エラー」になるので、private 宣言部に UpdateImageDisplaySize 手続きを宣言。

  private
    { Private 宣言 }
    ・・・(省略)・・・
    procedure UpdateImageDisplaySize;

Shift+Ctrl+C して、次のように記述する。

procedure TForm1.UpdateImageDisplaySize;
var
  W, H: Integer;
begin

  if not Assigned(FBitmap) then Exit;

  //拡大後サイズ
  W := Round(FBitmap.Width  * FZoom);
  H := Round(FBitmap.Height * FZoom);

  //Image
  Image1.Align    := alNone;
  Image1.AutoSize := False;
  Image1.Stretch  := False;
  Image1.SetBounds(0, 0, W, H);

  //PaintBox(完全同期)
  PaintBox1.Align := alNone;
  PaintBox1.SetBounds(0, 0, W, H);

  PaintBox1.Invalidate;
end;

実行(F9)して、動作確認。フォームが表示されたら、Button1 をクリックして、exe と同じ場所に置いたビットマップ画像が表示されることを確認。右上の閉じるボタンをクリックして、終了。


いよいよラバーバンド作成本番へ。

Form1 上の PaintBox をクリックして選択し、OnMouseDown 手続きを作成。

オブジェクトインスペクタの OnMouseDown をダブルクリックする。


コードは、次の通り。「未定義の識別子エラー」が複数出るが、未作成の手続きや関数があるためなので、その手続きや関数を続けて作成する。

procedure TForm1.PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  imgP: TPoint;
begin
  if not FRubberBandActive then Exit;

  imgP := ScreenToImagePoint(X, Y);

  if FHasRubber then
  begin
    //ラバーバンド内クリック
    if PointInRectInclusive(FRubberRect, imgP) then
    begin
      FDragHandle := GetHandleAtPosImg(imgP);
      FModeMoveSize := (FDragHandle = hpNone);
      FDragging := True;
      FStartPointImg := imgP;

      //追加:掴んだハンドルに応じてカーソルを確定
      if FDragHandle <> hpNone then
        PaintBox1.Cursor := CursorFromHandle(FDragHandle)
      else
        PaintBox1.Cursor := crSizeAll;  //内部クリック=移動

      ScrollBox1.SetFocus;
      Exit;
    end
    else
    begin
      //外クリック → 削除
      FHasRubber := False;
      PaintBox1.Invalidate;
      UpdateRubberCursor;  //カーソルを戻す
      Exit;
    end;
  end;

  //新規作成
  FDragging := True;
  FModeMoveSize := False;
  FDragHandle := hpNone;
  FStartPointImg := imgP;
  FRubberRect := Rect(imgP.X, imgP.Y, imgP.X, imgP.Y);

  //新規作成中は十字カーソル固定
  PaintBox1.Cursor := crCross;

  PaintBox1.Invalidate;
end;


エラーをひとつずつ潰す。最初に、private 宣言部に ScreenToImagePoint 関数を宣言して、Shift+Ctrl+C を押し下げ。

この後、何度も private 宣言部に戻る必要があるので、変数宣言の最後の方(手続き・関数宣言の最初)あたりをクリックして、Shift+Ctrl+1 を実行しておけば、Ctrl+1 ですぐに private 宣言部に戻ってこれるのでとても便利。

  private
    { Private 宣言 }
    FBitmap: TBitmap;

    // Rubber band
    FRubberBandActive: Boolean;
    ・・・(省略)・・・

    function ScreenToImagePoint(X, Y: Integer): TPoint;


で、次のように記述。

function TForm1.ScreenToImagePoint(X, Y: Integer): TPoint;
begin
  Result.X := Round(X / FZoom);
  Result.Y := Round(Y / FZoom);
end;


次は、PointInRectInclusive 関数を作成。まず宣言して、

  private
    { Private 宣言 }
    ・・・(省略)・・・
    function ScreenToImagePoint(X, Y: Integer): TPoint;
    function PointInRectInclusive(const R: TRect; const P: TPoint): Boolean;


Shift+Ctrl+C して、次のように記述する。

function TForm1.PointInRectInclusive(const R: TRect; const P: TPoint): Boolean;
begin
  Result :=
    (P.X >= R.Left) and (P.X <= R.Right) and
    (P.Y >= R.Top)  and (P.Y <= R.Bottom);
end;


次は、GetHandleAtPosImg 関数を宣言。

  private
    { Private 宣言 }
    ・・・(省略)・・・
    function GetHandleAtPosImg(const P: TPoint): THandlePos;


こちらも Shift+Ctrl+C して、次のように記述する。

function TForm1.GetHandleAtPosImg(const P: TPoint): THandlePos;
const
  HSIZE = 8;
var
  cx: Integer;
begin

  Result := hpNone;

  // Top
  cx := (FRubberRect.Left + FRubberRect.Right) div 2;
  if Abs(P.Y - FRubberRect.Top) <= HSIZE then
  begin
    if Abs(P.X - FRubberRect.Left) <= HSIZE then Exit(hpLeftTop);
    if Abs(P.X - FRubberRect.Right) <= HSIZE then Exit(hpRightTop);
    if Abs(P.X - cx) <= HSIZE then Exit(hpTop);
  end;

  // Bottom
  if Abs(P.Y - FRubberRect.Bottom) <= HSIZE then
  begin
    if Abs(P.X - FRubberRect.Left) <= HSIZE then Exit(hpLeftBottom);
    if Abs(P.X - FRubberRect.Right) <= HSIZE then Exit(hpRightBottom);
    if Abs(P.X - cx) <= HSIZE then Exit(hpBottom);
  end;

  //Left /Right
  if Abs(P.X - FRubberRect.Left) <= HSIZE then Exit(hpLeft);
  if Abs(P.X - FRubberRect.Right) <= HSIZE then Exit(hpRight);

end;


次は、CursorFromHandle 関数を宣言。

  private
    { Private 宣言 }
    ・・・(省略)・・・
    function CursorFromHandle(H: THandlePos): TCursor;

こちらも Shift+Ctrl+C して、次のように記述する。

function TForm1.CursorFromHandle(H: THandlePos): TCursor;
begin
  case H of
    hpLeft, hpRight:
      Result := crSizeWE;

    hpTop, hpBottom:
      Result := crSizeNS;

    hpLeftTop, hpRightBottom:
      Result := crSizeNWSE;

    hpRightTop, hpLeftBottom:
      Result := crSizeNESW;

    else
      Result := crDefault;
  end;
end;


残りのエラーはあとひとつ。UpdateRubberCursor;

  private
    { Private 宣言 }
    ・・・(省略)・・・
    procedure UpdateRubberCursor;

Shift+Ctrl+C して、次のように記述する。

procedure TForm1.UpdateRubberCursor;
begin
  if FRubberBandActive then
    PaintBox1.Cursor := crCross
  else
    PaintBox1.Cursor := crDefault;
end;


次は、PaintBox の OnMouseMove 手続きを作成。

オブジェクトインスペクタの OnMouseMove をダブルクリックする。


こちらは、次のように記述する。

procedure TForm1.PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  curImg: TPoint;
  screenPt: TPoint;
  dx, dy: Integer;
  H: THandlePos;
begin
  if not FRubberBandActive then Exit;

  curImg := ScreenToImagePoint(X, Y);

  //ドラッグ中のみ自動スクロール
  if FDragging then
  begin
    screenPt := PaintBox1.ClientToScreen(Point(X, Y));
    AutoScrollIfNeededFromScreen(screenPt);
  end;

  //矢印キーの形状をより適切に
  if not FDragging then
  begin
    if FHasRubber and PointInRectInclusive(FRubberRect, curImg) then
    begin
      H := GetHandleAtPosImg(curImg);
      if H <> hpNone then
        PaintBox1.Cursor := CursorFromHandle(H)
      else
        PaintBox1.Cursor := crSizeAll;  //内部=移動
    end
    else
      PaintBox1.Cursor := crCross; //ラバーバンド描画モード
    Exit;
  end;

  dx := curImg.X - FStartPointImg.X;
  dy := curImg.Y - FStartPointImg.Y;

  if FModeMoveSize then
  begin
    FRubberRect.Offset(dx, dy);
  end
  else
  begin
    case FDragHandle of
      hpLeftTop:
      begin
        FRubberRect.Left := curImg.X;
        FRubberRect.Top  := curImg.Y;
      end;

      hpTop:
        FRubberRect.Top := curImg.Y;

      hpRightTop:
      begin
        FRubberRect.Right := curImg.X;
        FRubberRect.Top   := curImg.Y;
      end;

      hpRight:
        FRubberRect.Right := curImg.X;

      hpRightBottom:
      begin
        FRubberRect.Right  := curImg.X;
        FRubberRect.Bottom := curImg.Y;
      end;

      hpBottom:
        FRubberRect.Bottom := curImg.Y;

      hpLeftBottom:
      begin
        FRubberRect.Left   := curImg.X;
        FRubberRect.Bottom := curImg.Y;
      end;

      hpLeft:
        FRubberRect.Left := curImg.X;

      hpNone:
      begin
        FRubberRect.Right  := curImg.X;
        FRubberRect.Bottom := curImg.Y;
      end;
    end;
  end;

  FStartPointImg := curImg;

  NormalizeRect(FRubberRect);

  //画像エリアを超えないよう制限
  if FRubberRect.Left < 0 then FRubberRect.Left := 0;
  if FRubberRect.Top < 0 then FRubberRect.Top := 0;
  if FRubberRect.Right > FBitmap.Width then FRubberRect.Right := FBitmap.Width;
  if FRubberRect.Bottom > FBitmap.Height then FRubberRect.Bottom := FBitmap.Height;

  PaintBox1.Invalidate;

end;


AutoScrollIfNeededFromScreen と、NormalizeRect が「未定義の識別子エラー」になるので、こちらもprivate 宣言部に手続きを宣言して、

  private
    { Private 宣言 }
    ・・・(省略)・・・
    procedure AutoScrollIfNeededFromScreen(const ScreenPt: TPoint);
    procedure NormalizeRect(var R: TRect);


Shift+Ctrl+C して、それぞれ次のように記述する。

procedure TForm1.AutoScrollIfNeededFromScreen(const ScreenPt: TPoint);
var
  pt: TPoint;
begin
  pt := ScreenToClient(ScreenPt);

  if pt.X < FAutoScrollMargin then
    ScrollBox1.HorzScrollBar.Position :=
      ScrollBox1.HorzScrollBar.Position - FAutoScrollStep
  else if pt.X > ScrollBox1.ClientWidth - FAutoScrollMargin then
    ScrollBox1.HorzScrollBar.Position :=
      ScrollBox1.HorzScrollBar.Position + FAutoScrollStep;

  if pt.Y < FAutoScrollMargin then
    ScrollBox1.VertScrollBar.Position :=
      ScrollBox1.VertScrollBar.Position - FAutoScrollStep
  else if pt.Y > ScrollBox1.ClientHeight - FAutoScrollMargin then
    ScrollBox1.VertScrollBar.Position :=
      ScrollBox1.VertScrollBar.Position + FAutoScrollStep;
end;
procedure TForm1.NormalizeRect(var R: TRect);
var
  Ll, Lt, Lr, Lb: Integer;
begin
  Ll := R.Left; Lt := R.Top;
  Lr := R.Right; Lb := R.Bottom;

  R.Left := Min(Ll, Lr);
  R.Top := Min(Lt, Lb);
  R.Right := Max(Ll, Lr);
  R.Bottom := Max(Lt, Lb);
end;

ここで、今度は Min と Max が「未定義の識別子エラー」になるので、uses に System.Math を追加する。

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.StdCtrls,  System.Math;


次は、PaintBox の OnMouseUp 手続きを作成。

オブジェクトインスペクタの OnMouseUp をダブルクリックする。


次の内容を記述。

procedure TForm1.PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if not FRubberBandActive then Exit;
  if not FDragging then Exit;

  FDragging := False;
  NormalizeRect(FRubberRect);
  FHasRubber := True;

  //ドラッグ終了後は状態に応じてカーソルを戻す
  UpdateRubberCursor;

  //矢印キー操作を有効にする
  ActiveControl := nil;

  EnsureRubberVisible;
  PaintBox1.Invalidate;

  //ラバーバンドの座標を取得
  UpdateRubberInfo('Mouse ');
end;


ここでは、EnsureRubberVisible と、 UpdateRubberInfo が「未定義の識別子エラー」になるので、private 宣言部で、EnsureRubberVisible 手続きと UpdateRubberInfo 手続きを宣言。

  private
    { Private 宣言 }
    ・・・(省略)・・・
    procedure EnsureRubberVisible;
    procedure UpdateRubberInfo(const Prefix: string = '');

次のように、それぞれの手続きを記述する。

procedure TForm1.EnsureRubberVisible;
var
  R: TRect;
  p: TPoint;
begin
  R := ImageToScreenRect(FRubberRect);
  p := PaintBox1.ClientToScreen(Point(R.Left, R.Top));
  AutoScrollIfNeededFromScreen(p);
end;
procedure TForm1.UpdateRubberInfo(const Prefix: string);
var
  R: TRect;
begin
  if not FHasRubber then Exit;

  R := FRubberRect;

  if Memo1.Lines.Count = 0 then
    Memo1.Lines.Add('')
  else
    Memo1.Lines[Memo1.Lines.Count - 1] := '';  //最終行を書き換える

  Memo1.Lines[Memo1.Lines.Count - 1] :=
    Format('%sL:%d T:%d W:%d H:%d',
      [Prefix, R.Left, R.Top, R.Right - R.Left, R.Bottom - R.Top]);
end;


すると、EnsureRubberVisible 手続きの ImageToScreenRect が「未定義の識別子エラー」になるので、これも潰す。

  private
    { Private 宣言 }
    ・・・(省略)・・・
    function ImageToScreenRect(const R: TRect): TRect;

Shift+Ctrl+C して、次のように記述する。

function TForm1.ImageToScreenRect(const R: TRect): TRect;
begin
  Result.Left   := Round(R.Left   * FZoom);
  Result.Top    := Round(R.Top    * FZoom);
  Result.Right  := Round(R.Right  * FZoom);
  Result.Bottom := Round(R.Bottom * FZoom);
end;


次は、Form1 の OnKeyDown 手続きを作成。まず、Form1 のタイトルバー付近をクリックして、Form1 をアクティブにしておき、

オブジェクトインスペクタの OnKeyDown をダブルクリック。


次のように記述する。

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin

  if not (FRubberBandActive and FHasRubber) then Exit;

  case Key of
    VK_LEFT:
      if ssShift in Shift then Dec(FRubberRect.Right)
      else FRubberRect.Offset(-1, 0);

    VK_RIGHT:
      if ssShift in Shift then Inc(FRubberRect.Right)
      else FRubberRect.Offset(1, 0);

    VK_UP:
      if ssShift in Shift then Dec(FRubberRect.Bottom)
      else FRubberRect.Offset(0, -1);

    VK_DOWN:
      if ssShift in Shift then Inc(FRubberRect.Bottom)
      else FRubberRect.Offset(0, 1);
  end;

  NormalizeRect(FRubberRect);
  PaintBox1.Invalidate;

  //ラバーバンドの座標を取得
  UpdateRubberInfo('Key ');

end;


次は、PaintBox1Paint 手続きを作成。こちらは PaintBox1 をクリックしてアクティブにしておいて、

オブジェクトインスペクタの OnPaint をダブルクリックする。


次の内容を記述。

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
  C: TCanvas;
  rS: TRect;
  pLT, pRT, pRB, pLB, pT, pB, pL, pR: TPoint;

  procedure DrawHandle(const P: TPoint);
  var
    R: TRect;
    S: Integer;
  begin
    S := 5;
    R := Rect(P.X - S, P.Y - S, P.X + S, P.Y + S);
    C.Brush.Color := clWhite;
    C.Pen.Color := clRed;
    C.Rectangle(R);
  end;

begin

  C := PaintBox1.Canvas;
  C.Brush.Style := bsClear;

  //初回ドラッグ中、ラバーバンドが描画されない
  //if not (FRubberBandActive and FHasRubber) then Exit;

  //初回ドラッグ中でもラバーバンドが描画される
  if not FRubberBandActive then Exit;
  if not (FDragging or FHasRubber) then Exit;

  rS := ImageToScreenRect(FRubberRect);

  //枠
  C.Pen.Style := psDot;
  C.Pen.Color := clRed;
  C.Rectangle(rS);

  //ハンドル位置
  pLT := ImageToScreenPoint(FRubberRect.Left,  FRubberRect.Top);
  pRT := ImageToScreenPoint(FRubberRect.Right, FRubberRect.Top);
  pRB := ImageToScreenPoint(FRubberRect.Right, FRubberRect.Bottom);
  pLB := ImageToScreenPoint(FRubberRect.Left,  FRubberRect.Bottom);

  pT := Point((pLT.X + pRT.X) div 2, pLT.Y);
  pB := Point((pLB.X + pRB.X) div 2, pLB.Y);
  pL := Point(pLT.X, (pLT.Y + pLB.Y) div 2);
  pR := Point(pRT.X, (pRT.Y + pRB.Y) div 2);

  DrawHandle(pLT);
  DrawHandle(pRT);
  DrawHandle(pRB);
  DrawHandle(pLB);
  DrawHandle(pT);
  DrawHandle(pB);
  DrawHandle(pL);
  DrawHandle(pR);

end;


ImageToScreenPoint が「未定義の識別子エラー」になるので、これを潰す。private 宣言部に戻って、ImageToScreenPoint 関数を宣言し、

  private
    { Private 宣言 }
    ・・・(省略)・・・
    function ImageToScreenPoint(Ix, Iy: Integer): TPoint;

Shift+Ctrl+C して、次のように記述する。

function TForm1.ImageToScreenPoint(Ix, Iy: Integer): TPoint;
begin
  Result.X := Round(Ix * FZoom);
  Result.Y := Round(Iy * FZoom);
end;


実行(F9)すると、「[dcc32 ヒント] Unit1.pas(529): H2443 インライン関数 ‘Point’ はユニット ‘System.Types’ が USES リストで指定されていないため展開されません」というヒントが表示されるので、uses に System.Types を追加する。

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

【マウスホイールでスクロールさせるには?】

ちなみに、マウスのホイールを回転させて画像をスクロールさせたい場合は、

ScrollBox1 の OnMouseWheel をダブルクリックして手続きを作成。


次のコードを記述する。Shift キーを押したまま、ホイールを回転させると、ホイールを手前に回転させた場合は右へ(逆なら左へ)画像はスクロールする。

procedure TForm1.ScrollBox1MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
const
  ScrollStep = 40; //ホイール1回の移動量
begin
  if ssShift in Shift then
  begin
    //横スクロール(Shift + ホイール)
    ScrollBox1.HorzScrollBar.Position :=
      ScrollBox1.HorzScrollBar.Position - (WheelDelta div 120) * ScrollStep;
  end
  else
  begin
    //縦スクロール(通常)
    ScrollBox1.VertScrollBar.Position :=
      ScrollBox1.VertScrollBar.Position - (WheelDelta div 120) * ScrollStep;
  end;
  //処理済み
  Handled := True;
end;

3.まとめ


Button1 をクリックして、Image1 に画像を表示し、その上をマウスでドラッグすると伸縮・移動が自由自在なラバーバンドが表示される。

以下の内容を追記しました(20260209)

【ラバーバンドの新規作成】

ボタンクリックで新しくラバーバンドを描画するコードです。

procedure TForm1.btnNewRectClick(Sender: TObject);
begin

  //新規にラバーバンドを描画

  //1. ラバーバンド描画モードをON
  FRubberBandActive := True;

  //2. 現在表示されているラバーバンドを非表示に
  FHasRubber := False;

  //3. ドラッグ中の状態もリセット
  FDragging := False;

  //4. PaintBoxを最前面にする(マウス操作を確実に受け取る)
  PaintBox1.BringToFront;

  //5. カーソルを「新規作成」用の十字型(crCross)に変更
  UpdateRubberCursor;

  //6. PaintBoxを再描画して、古い枠を画面から消す
  PaintBox1.Invalidate;

  //7. ユーザーへ通知
  StatusBar1.SimpleText := '新規ラバーバンド作成準備完了:ドラッグしてください';

end;

【ラバーバンドの座標を取得】

現在、Image1上に描画されているラバーバンドの座標を取得するコードです。
ごく一般的な記述では 3-a の座標形式だと思いますが、実際の運用では解答欄の矩形座標を取得し、これを保存する際、印刷やスキャン時のズレの問題を解消するため、画像上の特徴点からの距離で座標を記録するようにしていますので、3-b はその例となっています。

procedure TForm1.btnGetRectClick(Sender: TObject);
var
  R: TRect;
  CoordStr: string;
begin

  //座標を取得

  //1. ラバーバンドが存在するかチェック
  if not FHasRubber then
  begin
    ShowMessage('ラバーバンドが描画されていません。');
    Exit;
  end;

  //2. 現在の座標を取得
  R := FRubberRect;

  //3-a. 表示用の文字列を作成 (例: [L:10, T:20, W:100, H:50])
  //CoordStr := Format('L:%d, T:%d, W:%d, H:%d',
  //                   [R.Left, R.Top, R.Width, R.Height]);

  //3-b. 表示用の文字列を作成 (例: [LX, TY, RX, BY])
  //LeftX, TopYは矩形切り出しの原点とする特徴点左上の座標
  //(矩形印刷位置・スキャン時のズレに強くする独自の工夫)
  CoordStr := Format('%d, %d, %d, %d',
                     [R.Left - LeftX, R.Top - TopY,
                      R.Left + R.Width - LeftX, R.Top + R.Height - TopY]);

  //4. Memo1の現在のキャレット位置に文字列を挿入
  //SelTextに代入すると、選択範囲がある場合は上書き、ない場合はキャレット位置に挿入される
  Memo2.SetFocus; // メモにフォーカスを当てる
  Memo2.SelText := CoordStr;

  //5. ラバーバンドを消去
  ClearRubberBand;
  StatusBar1.SimpleText := '';

end;

【ラバーバンドの消去】

現在、Image1上に描画されているラバーバンドを消去するコードです。
上の「ラバーバンドの座標を取得」の最後に ClearRubberBand; として呼び出しています。消去手続きとして作成しておけば、何度も同じコードを書かなくて済むので便利ですね。

procedure TForm1.ClearRubberBand;
begin
  //1. 枠が存在するというフラグを降ろす
  FHasRubber := False;

  //2. ラバーバンド操作モード自体をOFFに
  FRubberBandActive := False;

  //3. ドラッグ中の状態もリセット
  FDragging := False;

  //4. 座標データをゼロで初期化
  FRubberRect := Rect(0, 0, 0, 0);

  //5. カーソルをデフォルトに戻す
  UpdateRubberCursor;

  //6. PaintBoxを再描画(画面から消去)
  PaintBox1.Invalidate;
end;

※ UpdateRubberCursor 手続きは、本記事内で作成済みの手続きです。

4.お願いとお断り

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

同じ大きさの矩形内にキャプチャします!

例えば、次のように複数の画像があり、その1つ1つを別々にキャプチャするとき、同じ大きさの矩形内に収まるようにしたい場合が結構あります。

そんな時、マウスカーソルを含めてキャプチャできるように開発したオリジナルプログラムがとても便利に使えましたので、紹介させていただきます。

【重要】そう感じたのは、私だけ!かもしれません。もしそうだったら、「ごめんなさい」です。

【注意】この用途に使う場合は、「マウスカーソルを含めてキャプチャする」機能は OFF に。

デフォルト設定では、次のようにチェックされていると思いますが、


このチェックボックスのチェックを外してください。


これで、より、安心・安全に使えます。

Word 文書に複数の図形を貼り付けてサンプルを作成


本当は、もっと別のサンプルというか、実例をご紹介したかったのですが、著作権の問題があり・・・

操作の雰囲気だけでも感じていただけたら幸いです。
以下、下のリンクからダウンロードできるアプリを使用したその方法です。

アプリのご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt をご覧ください。

【重要】

高解像度画面の拡大率は100%で使用してください。100%でないと期待通りに動作しません。

追記_機能の追加(20251117)

ラバーバンドで囲った範囲を上下反転、もしくは左右反転できるようにしました。また、全選択ボタンで元画像全体を選択しておいて反転を実行すれば、元画像全体を反転表示することも可能です。

このプログラムで行える画像処理の一覧は、次のリンク先記事の「7.まとめ」にあります。

【もくじ】

1.プログラムを起動します
2.キャプチャします!
3.同じ大きさの矩形内に画像を入れて、矩形内の画像のみ保存します!
4.お願いとお断り

1.プログラムを起動します

プログラムをダウンロード後、Zip ファイルを展開すると起動可能になります。
次のアイコンをダブルクリックしてください。


このプログラムは、現在表示されているデスクトップをそのままキャプチャするプログラムなので、タスクバー内に静かに起動します。Windows の設定で許可されていれば、起動を知らせる通知が出ることもあります(たいていの場合、この通知が出ると思います)。


これでキャプチャする準備ができました!

2.キャプチャします!

キャプチャしたい画面を表示した状態で、画面の中心付近に画像が来るようにして、Shift + Ctrl + C を実行します(私の場合、Shift キーと Ctrl キーを左手の中指と人差し指で同時に押し下げたまま、右手の人差し指で C キーを押し下げます)。

デスクトップの状態がそのままキャプチャされます。


(Word の場合であれば画面を下へスクロールして)次の画像をキャプチャします。
もちろん、操作方法は、1枚目と同じ、Shift + Ctrl + C です。

2枚目です。


同様に、3枚目も、4枚目もキャプチャします。同様に Shift + Ctrl + C です。

3枚目です。
4枚目です。


必要な画像を全てキャプチャしたら、タスクバーの次のアイコンをクリックしてください。


こんな画面になるはずです。


次に、同じ大きさの矩形の中に図形が収まるように矩形(ラバーバンド)を描画して、その大きさを調整、矩形で囲った範囲内のみを画像として保存します。保存する画像の形式は、画面左上のオプションを選択することで BMP・PNG・JPEG のいずれかを指定できます。

3.同じ大きさの矩形内に画像を入れて、矩形内の画像のみ保存します!

画面右に表示されているサムネイルのうち、いちばん上に表示されているサムネイルをクリックしてください


保存したい画像が収まるように、画像の 左上から右下へ マウスでドラッグしてください。


【参考】ラバーバンドの線の太さと色は、別途指定できます。

線の見た目が変わるだけで、機能は同じです。
グラブハンドルは大きさを調整するためにラバーバンド上に表示されている □ です。


保存したい範囲を指定できたら、ラバーバンド内を右クリックしてください。右クリック位置に表示されたサブメニューのいちばん上にある「名前を付けて保存」をクリックします。

このラバーバンド矩形は他の画像の保存範囲指定にも使用しますので、描画したラバーバンドを誤って消去しないように、十分注意してください。


次のダイアログが表示されます。必要であれば、画像の名称の変更も可能です。画像の名称の確認 or 設定後、よろしければ保存ボタンをクリックしてください。ラバーバンド内の画像が保存されます。


次の画像のサムネイルをクリックしてください。


画像がラバーバンド内の中心に収まるように、ラバーバンドの位置を微調整します。

ラバーバンドの位置の移動は、Shift + Ctrl + 上下左右の矢印キー押し下げで実行できます。矢印キーを1回押し下げると1ピクセルだけ、ラバーバンドが押し下げた矢印キーの方向へ移動します。

【注意】

「Shift + 上下左右の矢印キー押し下げ」は、ラバーバンドのサイズ変更になります。
ラバーバンドの左上を原点として、矢印キーの押し下げ向きに1ピクセルずつサイズが変わります。

ショートカットキーの押し間違いには、細心の注意を払っていただけますよう、お願い致します。

調整が完了したら、ラバーバンド内を右クリックして表示されるサブメニューの「名前を付けて保存」をクリックして、保存のダイアログを出し、必要であれば名称を変更等して、ラバーバンドで囲った範囲の画像を保存します。

保存後、次のサムネイルをクリックして、同様に作業します。

必要な画像すべてについて、この作業を繰り返します。

これをサンプル画像について、実行した結果は、次のようになります。

同じ大きさの矩形内に画像をキャプチャできました!


マウスカーソルを含めたデスクトップ画面のキャプチャを手軽に実現したくて書いたこのプログラムですが、その副産物として『任意の大きさで指定した矩形内にある画像を、その矩形サイズで連続キャプチャして保存』することが出来ることに気づき、実際にそれを業務で使ってみたら、これまでかなり面倒だった作業を、ほんとうに快適に、効率よく実行できましたので、その手順を紹介させていただきました。

もちろん、私が知らないだけで、もっと簡単に、効率よく、このような作業を実行できるプログラムが間違いなく存在していることと思いますが・・・ 私はそれを探して、実際に使ったことがありません。

4.お願いとお断り

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

AI と話したら、僕は Ctrl + DVRC!

AI と会話しない日がなくなって久しいです。彼(彼女?)は時々間違えることもあるけれど、その間違いを見抜く力さえあれば、Google 先生とあわせて活用することで、調べものにかかる時間と手間を大いに軽減できて、すーぱー Goooooooooooooooooooooood!!← アボガドロ数的感情表現のつもり。

ただ、その会話内容をコピペして印刷しようとすると問題になるのが、チャットのコピーに含まれているマークダウン表記。気にしなければイイという考えも、もちろんあるかと思いますが、私は『どうしてもプレーンテキスト( 平文 )が欲しい!』派です。

そこで自分専用に作ったのがコレ。

NotebookLMとの会話の一部を抜粋


(左右のテキストを見比べていただければおわかりになるかと思いますが)

そうです。マークダウン表記の混じったテキストを平文(プレーンテキスト)化するプログラムです。

時々、思った通りに動作しないこともありますが、そこは手動修正。これは自信を持って言えますが、何と言っても、この私が書いた(自分専用の)プログラムですから、まともに動くわけがありません! と、開き直るわけではないのですが、自分的には、十分、活用できています。ご試用にあたり、変換結果の確認は必須で、不具合のある箇所は手動修正する必要のあるプログラムでもよろしければ、下のリンク先からダウンロードできますので、バグあり・サポート一切なし・完全自己責任の下で、もしよろしければお試しください。

変換結果の確認ですが、変換結果の平文テキストそのものを AI に投げて、数式を含めた内容の矛盾点の有無を指摘してもらう方法もあるのではないかと思います。

また、どうしても『 – マイナス記号』など、消えては困る記号が消える場合は(手間はかかりますが)いったん削除対象文字からその記号を外してプレーンテキストに変換し、さらに、その記号を再度指定して変換した結果を合体させるという方法もあるかと思います。

不出来なプログラムで本当に申し訳ないのですが、変換に不具合が発生するのは特定の箇所に集中しているのではないかと思われますので、その部分をピンポイントで手動修正する方向でお試しいただけたら幸いです。

いずれにしましても、問題が発生するのは・・・元々、削除対象文字に指定した(=ユーザー側の意向で消せと命令した)文字を「プログラム側で自動的に忖度して部分的に残そう」・・・という場合だと思います。『消せと命令したのに残す?』ごく冷静に考えれば、その処理自体が矛盾を孕んでいるわけです。ですので、この問題に関しては、やはり、どう工夫しても、最終的に、ユーザーサイドでの変換結果の確認と、部分的な手直しが必要になるのは、どうしても避けられないことなのではないか?・・・と、思えてなりません。

【重要な情報提供】

私のようなド素人が書いた、バグ満載の、信頼性に乏しく、状況に応じて名前まで変わるような、ヘンなプログラムでなく、Markdown、HTML、LaTeX、Word、PDF など多様な形式に対応した Pandoc という文書フォーマットを相互に変換できる、きちんとした変換ツールが公開されていることを申し添えます(オープンソースの Pandoc は、もちろん無料で利用できます)。

以下、バグ満載(例: 数式中の – マイナス記号等、消えて欲しくない記号が実際に消えてしまうことがあります)ではないかと、書いた本人も心配しつつ、書いた本人が使ってみた範囲では、時々期待を裏切りながらも、それなりに使えてる気がする、My Original Application の使い方です。

【追記】(20260609)

・ユーザーからの要望を受け、消音機能はユーザーの意向でON/OFFできるように修正しました。

・より広いスペースをテキストの表示に使用できるよう、GUI を改良しました。

(1)削除対象文字を指定

全角/半角にも注意して、削除対象文字を指定します。
絵文字はプログラム側で一括処理できます。


削除対象文字には、マークダウン表記に使用される記号の他、任意の文字を追加可能です。AI が好んで?用いる絵文字は(ユーザーが指定すれば)プログラム側で見つけて処理するようにしました(ただ、今後 Unicode が更新されて新しい絵文字等が追加され、プログラム側で処理しきれなくなる可能性があります。その場合でも、ここに文字を追加すればなんとかなるかな・・・ ならないかな やっぱり)。

正直に白状しますと、設計当初は削除対象文字に絵文字もいちいち指定しておりましたので、削除対象文字の入力用 VCL コントロールは、単一行テキスト入力専用の TEdit ではなく、複数行入力が可能な TRichEdit を使っています。

動作テストを重ねるうちに、削除対象文字としてこの TRichEdit に指定した絵文字の数が激増し、それでも(おかしいなー!)と思いつつ都度『新発見絵文字』を気持ち半分喜びながら毎日追加しておりましたが、日々増え行く絵文字列が2行目の折り返しを間近に控えた頃、ようやく『 AI が使用する絵文字の種類は限定されてるに違いない』という自身の強い思い込みが『完全な誤り』であることに気づき(かつ、TRichEdit は絵文字に完全対応ではないみたいな話を AI から聞いたこともあり)、個別にいちいち指定する方式から一括削除する方式に仕様を変更したという、微笑ましくもどこか悲しい、ごく私らしい個人的なエピソードもあります。

ちなみに、私が現在削除対象文字に指定しているのは「*-`$#|」だけです。(万一、プログラムをダウンロードして使用される場合、削除対象とする文字を指定する際に、文字と文字の間にスペースを入れたり、カンマ区切りにしたりすると、それも「削除対象文字」になりますので、くれぐれもご注意ください)

また、削除対象文字として入力した内容は、プログラムの終了時に、特に何も指示等しなくても自動的に C:\Users\(ユーザー名)\AppData\Roaming\MarkdownRemover フォルダ内にある settings.ini に保存され、次回起動時に、前回終了時の状態が自動的に復元されます。

(2)平文化したい AI との会話内容をコピーして、変換プログラムを起動し、

Ctrl + V → R → C

一言で言えば、上の通りですが、手順をきちんと説明すると・・・

まず、プログラムを起動して、実行したい処理をチェックします。

Info にチェックを入れると、操作する際に、うざいメッセージが出ます。

【重要】

✅マークダウン表記を平文にするをチェックした場合、削除対象文字に指定した文字でも状況により、削除対象から自動的に除外して(=削除しないで)処理することがあります(例:処理対象のテキスト内に数式があり、その数式内でアスタリスク * などが使用されている場合、数式内のアスタリスク * は削除しないようにプログラミングしたつもりです・・・が、状況によってはうまく動作しないことがあり得ると思います)。

実行したい処理をチェックしたら、Ctrlキーを押し、そのまま、V → R → C の順にキーを押し(下げ)ます。

Ctrl + V で、画面左側の TMemo に貼り付け
Ctrl + R で、画面右側の TMemo にプレーンテキストに変換して表示
Ctrl + C で、プレーンテキストをクリップボードへ送信

ちなみに2回目以降は、

Ctrl + D → V → R → C

Ctrl キーは押し下げたまま、D → V → R → C の順にキーを押し下げます。

Ctrl + D で、画面左側の TMemo を初期化(表示内容を消去)
Ctrl + V で、画面左側の TMemo に貼り付け
Ctrl + R で、画面右側の TMemo にプレーンテキストに変換して表示
Ctrl + C で、プレーンテキストをクリップボードへ送信

で、他のアプリ(エディタ等)への貼り付けは、アプリを切り替えて Ctrl+V。

【重要】

前回貼り付け分にさらに新しいテキストを「追加」する場合は、Ctrl+D は行わないでください。左側の Memo をクリックしてアクティブにし、改行を2つ程度入れて、次のテキストを入力してください。

基本的な使い方はこれだけです。

あっ! これだけはお断りしておいた方がいいかな? と思うことが3つありました。

【注意】このプログラムには、テキストを保存する機能はありません。

【注意】意図した通りに動作しない可能性が多いにあります!!!

【注意】重要な情報(記号・符号)が部分的に消えてしまう可能性も0ではありません。

テキストを保存する機能がないのは、事故防止のためです(貼り付け元としたオリジナルテキストを操作しません)。このプログラムはあくまでもテキスト整形のための中継器のように使用することを前提に作成しました。また、改行や空白(全角・半角いずれも対応)の削除も可能ですので、用途に応じて必要な機能のみを利用することもできます。

「マークダウン表記を平文にする」をチェックした場合は、Form のキャプション(タイトル)が『MarkdownRemover』と表示されますが、チェックを外すと Form のキャプション(タイトル)が『CharSweeper』と変化するのは、用途に応じた必要な機能の提供という部分へのこだわりをタイトル的に表現したものです。名前が変わるプログラムなんて私は見たことがありませんが・・・

それでもよろしければ完全自己責任の下でお試しください。(ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt をご覧ください)

以下は、余程の事がない限り・・・

お読みいただく価値のない文言の羅列です

・・・ ので、もしお時間を無駄になさってもよろしければ、このプログラムの私的仕様の詳細なるものをご確認ください。

【もくじ】

1.きっかけ
2.表示フォントの指定方法
3.チェックしても何も起きない? CheckBox
4.そのうちに消せない絵文字も出てくるカモ?です。
5.プログラム終了時に Beep 音?
6.お願いとお断り

本プログラムの私的仕様の詳細です!

1.きっかけ

AI とチャットした際、その会話内容は AI の方で勝手に保存してくれますが(ただ、過去のチャットの「特定部分」を探すのがエライ面倒なことも多々あり、やはり、自分にとって『走召!』重要な情報は別に保存しておきたいなー!みたいな気が・・・私はしますし)、会話の特定部分を資料的に印刷等して活用したい場合は、どうしてもプレーンテキストでないと困る場合が・・・自分的には・・・ほとんどですと言うか、はっきり言って全部です。

もちろん、Pandoc などの変換ツールを自由自在に使いこなせる方なら、何の問題もないと思いますが・・・。わたくしの能力では、Pandoc の Lua フィルターを使いこなし、中間AST(抽象構文木)を操作して、出力フォーマットをカスタマイズするのは至難の技というか・・・、妻の命でもかかっていれば話は別ですが、仕事で疲れて帰宅した夜にハイボールを呷りながらやりたい作業ではありません・・・。

それより何より、Pandoc はライセンスが GPLv2 or later ですので、法的な問題をきちんとクリアしないと Pandoc を利用したアプリケーションはもちろん公開できません。これが最大の理由で、Pandoc の利用を今回はあきらめることにしました。(個人的にはもちろん!試用してみました。デフォルト設定のまま動かしてみたのですが、コードブロックの変換部分で、インデントの処理に独自ルールが適用されるようで、変換結果のプログラミングコードをコピペする際にちょっと困るかもと思いましたが、その他は期待通りに動作しました)

自分的には、できるだけ制約の「ない」状態で、アプリケーションを公開したい・・・というのがほんとうの気持ちですので、ある制約の中に自分自身がが組み込まれてしまうことに、どうしても抵抗を感じてしまうのです。ただ、それは私個人の思いであって、GPLv2の目指すところはもちろん尊重しますし、その規約に賛同する方々の思いに水を差すようなこともしたくありません。

ですので、My プログラムのマークダウン表記の平文化処理のアルゴリズムは完全オリジナルで作成しました。これが(使って下さる方が万一いて下さったと仮定した場合)、私の想定外のシーンでは、期待に反する平文化処理を実行してしまう可能性を十分に秘めているアルゴリズムだと思う所以です。

ともあれ、このプログラムを書きたいと思った『きっかけ』は AI とのチャット記録の保存です。自分的な理想形でそれを実現したいと思ったとき、初めてマークダウン表記のプレーンテキスト化という目的が私の中に生まれました。

ほんとうに、「きっかけ」とは不思議なものです。ものごとの始まりには必ず何らかの「きっかけ」があって、すべての物語はその「きっかけ」から始まります。もし、AI との会話を「プレーンテキスト化して記録したい」と思わなければ、このプログラムは生まれませんでした。私はいつでも、その「きっかけ」を探し続けているのように思えてなりません。ドキドキ・わくわくするような、まだ世界のどこにもないプログラムが書きたくなる、私だけの『きっかけ』を・・・。*(^_^)*♪

話が横道にそれました。もとへ。

目的の実現にあたり、マークダウン表記に使用される記号を文章中から取り除くのはもちろんですが、そうした結果生まれた空白や改行の処理も問題となり・・・つまり、プログラム文ではない行については、行頭にスペースがあれば削除しますが、それがプログラム文である場合、コードブロック部分のインデントはそのまま残したい・・・と思ったり。

最初はあれやこれやと 無駄な抵抗 無駄に工夫(=無駄な苦労 )して、各種プログラムの予約語があればそこはイジらない・・・みたいな楽しい ♪ ことをしてたんですが、やがてコードブロック表記を利用すれば、この問題は「さらっとクリア」できることに気づきます。

さんざん3 くらい まわり道してから気づくのが、私の王道パターンですので、今回は さんざん2 のまわり道くらいで気づいたので、まぁよしとして・・・

次に、問題になったのは、「数式」や「普通文」の中に削除対象として指定した文字がある場合です。例えば、次のように削除対象文字を指定した場合ですが・・・

*-`$#|

処理の対象とする行に「数式があるか/ないか or 数式を含むか/含まないか」をプログラム的に見分けなければいけません。で、その判定結果が True (=数式がある)であればイジらない、False であればイジる(=削除対象文字を探して削除する)ことにしました。

で、数式の存在を確認する方法を調べて見ると、マークダウン表記では、$...$ (インラインで文中に数式を挿入)や $$...$$ (独立した行にブロック形式で数式を表示)するようなので、まずは文章中のドル記号( $ )で囲まれた部分を数式と見なし、そこに削除対象文字があっても無視するよう設定。

さらにドル記号( $ )の前後がバッククォート(`)で囲まれたインラインコード※形式になっていたり、数式をドル記号( $ )で囲まず、単にバッククォート(`)のみで囲まれていることも実際にあるようなので、バッククォート(`)もフラグとして利用することにしました。

※インラインコード:プログラムのコードやコマンド、記号などを文章中でそのまま表示させたい場合に用いる。表示上は、等幅フォント(monospace)でレンダリングされ、強調や整形の影響を受けなくなる。

さらにさらに、ドル記号やバッククォートで『囲まれていない数式』があった場合(それが実際にあるのかどうかは未確認ですが)困ったことになりますので、次のように任意の行内に数式が含まれるか、どうかを判定する関数を作成・・・

  //行内に数式が含まれるか、どうかを判定
  //関数名は ContainsMathLikePattern とした方がイイかな・・・とも。
  function ContainsMathExpression(const S: string): Boolean;
  const
    //数式らしいパターンを探す
    Pattern = '(\d+\s*[\+\-\*/\^=]\s*\d+)|([a-zA-Z]+\s*\([\d\w\s\+\-\*/\^=]*\))|([\+\-\*/\^=]{1,})';
  begin
    Result := TRegEx.IsMatch(S, Pattern);
  end;

実際にテストしてみました!

削除対象として「*」を指定していますが、それが数式中にある場合は、とりあえず消えていないようです。


結果ですが(あくまでも私がテストした範囲に限っての話ですが)なんとか期待通りに動作しているようです。

でも、本当に数式中の演算子が消えずにちゃんと残るか? 今でも心配です。万一、このプログラムをダウンロードしてお使いになる場合は、完全自己責任でお願いします。「数式中の演算子が絶対に消えません」という保証は一切できませんので、変換後の結果を必ず確認する必要があることにくれぐれもご留意いただけますよう、お願い申し上げます m(__)m

数式に限らず、このプログラムを用いて重要な文言を含むテキストを変換する場合は、変換後のテキストの内容をすみずみまで十分ご確認いただけますよう、伏してお願い申し上げます。

2.表示フォントの指定方法

好きなフォントを指定できます。個人的には「プログラムコードが読みやすい」Consolas がおすすめです!

「フォント」ボタンをクリックします。


フォント名、スタイル、サイズ等を指定できます。指定したら OK ボタンをクリックしてください。

部分的に色を変更することは出来ません!


上の条件で設定した、実際の画面です。

AI とのチャット内容をブログで公開することについて、AI に質問してみた結果を変換しました!

3.チェックしても何も起きない? CheckBox

この記事の冒頭で「私的仕様」と書きましたが、例えば、次の CheckBox チェック時の扱いがその一例です。「日付を挿入」にチェックを入れても、表示されている平文のどこにも日付は入りませんし、同様に「空行を詰める」をチェックしても、やはり表示されている平文化テキストの改行箇所は一切変更されません。

この CheckBox の処理の対象は、クリップボードに送信したプレーンテキストです!


このような仕様にした理由は2つあります。

1つめは、処理前のテキストと処理後のテキストを見比べて比較する場合、「✅スクロールの同期」の CheckBox にチェックが入っていれば、Memo の上下・左右のスクロールが連動します。処理後のプレーンテキストの改行部分を削除すると、左右の Memo の内容の比較が(改行位置が異なりますので)大変面倒なことになります。

2つめは、プレーンテキスト化したテキストを他のエディター等に貼り付けた際に、(私的に不要と感じる)改行が自動的に削除されていて欲しいことと、これまた極めて私的な理由で、プレーンテキストの末尾に1行改行して(西暦年月日)を自動的に挿入(追加)したかったからです。

ちなみに私は愛用の階層化テキストエディター(←この言い方が好きなので、こう書きますが、世間一般には「アウトラインプロセッサー」と呼称するようです)にノード分類して、次のように、その時々で実際に役立った Delphi のコードを記録・保存し、必要に応じて参照しています。

~~~階層化テキストエディターのとあるノード~~~

実際に使用したコード(=最終的に使ったコード)

以下、参考資料——————————————————

Web 上の情報源や、AI との会話内容等

(西暦年月日)

その実際の画面が、こちら

「あ行」の Tips


この階層化テキストエディターに、プレーンテキストをコピペした後の処理が必要最小限になるよう、「日付を挿入」&「改行を詰める」処理をクリップボードへのデータ送信時に行いたかったわけです。

ちなみに「改行を詰める」を✅した場合ですが、1行改行はそのまま、2行連続して改行があった場合は1行のみの改行へ変換、3行以上連続して改行が続いた場合は2行連続した改行に変換する仕様としてあります(ユーザーによる設定の変更はできません)。

日付は最終行に次のように挿入されます。

全角( )内に半角数字で、西暦年月日が入ります。


私の、この玉石混合状態(玉が3割程度か?)のプログラミング Tips は、もう 20 年近く書き溜めていますので、気づけば膨大な量となりました。その時々で仕事上の課題を解決するためのプログラムを主に作成してきましたので、ある意味では、これまでの生き方の記録と言っても過言ではありません。最近は AI に訊けば些細なことから、超絶!難しいことまで、懇切丁寧な解答が得られるので参照する機会はずいぶんと減りましたが、日付を見ると( あぁ、あの頃はこんなことやってたんだ・・・ )みたいな記憶がよみがえって、うれしいと言うか、なつかしいと言うか、よく生きてきたなー!みたいな感じがして・・・。年月日という、わずか1行にも満たない記録ですが、遠い未来で見返した際にそれがあることによって生まれる、何とも言えない懐かしい感覚が好きなのです。

2017年頃からは、いろいろ事情があって Python に触れる(= Python でプログラムを書く)機会がずいぶんと多くなりましたので、Python 用のプログラミング Tips もあることはあるのですが、こちらはなぜか、単なる記録集で、Delphi のそれとは異なり、過去の記録を参照して今日に役立てることはほとんどありません。

どちらかと言えば、Python でプログラムを書く場合は、その時々の課題解決専用のものを書いて、The END となることが「 ほとんど 」だからでしょうか・・・?

4.そのうちに消せない絵文字も出てくるカモ?です。

Unicode Consortium は毎年新しい絵文字を提案・承認しているそうで、2025年末には Emoji 17.0 に167種類が追加予定とのこと。さらに Apple の「Genmoji」など、生成AIによる絵文字生成も登場し、標準外の絵文字が個人レベルで作れる時代に突入しているようです。

プログラムでは、削除対象として検索する絵文字の範囲を次のように指定していますので、将来的には自動削除の対象にならない絵文字が出現する可能性があるように思います。気がつけばもちろんバージョンアップしますが、最悪の場合、削除対象文字列に『消えない絵文字をコピペ』すれば、確実に消せます。

    //削除対象として検索する絵文字の範囲
    if not (
      (code >= $1F600) and (code <= $1F64F) or // 顔文字
      (code >= $1F300) and (code <= $1F5FF) or // 天気・場所・物
      (code >= $1F680) and (code <= $1F6FF) or // 乗り物・地図
      (code >= $2600)  and (code <= $26FF)  or // 記号
      (code >= $2700)  and (code <= $27BF)  or // その他記号
      (code >= $FE00)  and (code <= $FE0F)  or // 表示スタイル
      (code >= $1F900) and (code <= $1F9FF) or // 拡張絵文字
      (code >= $1FA70) and (code <= $1FAFF)    // Emoji 13以降
    ) then

2025年10月末現在での確認事項となりますが、「 絵文字を除去 」のチェックボックスをチェックすることで(特に個別に指定しなくても)上記アルゴリズムが適用される範囲内の絵文字は一括除去が可能です。

5.プログラム終了時に Beep 音?

原因の解明ができていないのですが、このプログラムの終了時に「ポーン」という Beep 音※が鳴ることがあります(私の PC では時々発生します)。

※ 正しくは「システム通知音」(System Notification Sound)と言うようです。

このプログラムでは、開始時に音量設定が0でなければ自動消音し、終了時に開始時の音量設定を復元しています。なぜ、そのようにしたかというと、周囲に人がいるような環境では Beep 音が鳴らない方がよいと思ったからです。「入力ミス」などがあった際にユーザーに対して注意喚起するような目的で使われるこの音ですが、一人で PC を使用していてもメッセージが表示された際などに鳴ると結構(私は)気になります。

ですので、プログラムを公開する前に、プログラム終了時になぜBeep 音が「鳴ることがある」のか、しっかり解明しようと思っていたのですが、それが出来ませんでした。

これには、実は深い事情がありまして、この問題(?)に気づきました当日の午前4時頃、プログラムを様々に操作してみて、いったい何をどうすると音が鳴るのか? それを確認しよう!と、思っていた矢先のことでありました。

クー クー 眠っていると思っていた、いっしょに暮らしている、すーぱー たのしい 女性 が、いきなり目覚めて騒ぎはじめ、『 わたしの Word の仕事文書の罫線枠を直せ 』と。仰りまして・・・

なんでも、「10分間も苦労したのに思い通りにならない」とのことで。

( それはアンタが、Word の正しい使い方を知らないだけ )で、と、心底、海より深く、思いつつも。

世界全人類の平和と、彼女の心の平安は『イコール』であると☆信じて疑わない☆私的には

元より断る理由など、露ほどもございませんので。

・・・と、言うか、断ったりしたら、それはもう、ほんとに たいへんな コト に・・・ 。

最愛の彼女に心からご満足いただけますよう、(表面的仕草としては)心を込めたふりを装いつつ、

『 うん、この罫線指定ねー。奥にあるから。ほんと、わかりにくいよねー 』

『 うん。まるで、わかんない 』

( ソレハ アンタ ガ ムチャ で 気が短い カラ☆ でしょ? )・・・ と、思いつつ

『 でもね、今は AI に丁寧に訊けば、すごくやさしく教えてくれるよー!!』って。

そう、言いながら、彼女の PC の Word 文書の罫線を ちゃちゃっと修正したのですが・・・

当の、彼女は・・・

『 うん、もちろん、そう、思ったんだけど。』

『 うちには、解説だけじゃなくってー。全部やってくれる AI がいる からー☆ 』って。

とー とー オレ、AI に なっちまった( もしかして、格上げ?)。

たしか、つい、この前までは・・・

『 クルマ、いつもキレイねーって、職場で言われるの。』

( それは、よかったねー! )

『 ドロんこになったクルマ キレイに洗ってくれる 洗ヒグマが うちに・・・』と、確か。

オレ、クマだったはずなんだけど・・・ ってか、オレ、AI じゃない んだけど、みたいな。

それで、心が、折れまして・・・

もぉ 朝だし、まぁ いいかー☆ みたいな。

しかし、いったい、どこをどう走ると『あんなに』クルマを汚せるのか?

それは未だに 謎 ですが・・・

彼女曰く『 駐車場に吹く風が犯人 』とのことで・・・

見ればクルマの後部ガラスは、細かな塵や埃で今日も真っ白に・・・

彼女のクルマが『 洗ってー☆ 』と、
私に語り掛けてくる気が・・・ してならない朝でございます。

このプログラム、もっと良くしたいのは山々ですが。

やっぱり、今、私にも出来る
いちばんよいことをしようと思いました!

彼女のクルマ、
洗ってきまーす☆

6.お願いとお断り

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

Delphi のコードだけは、インデント等がきちんと維持されるか/どうか等検証する変換テストを(私に出来る範囲で)実行しましたが、その他のプログラミング言語については未検証のまま、プログラムを公開しておりますことを申し添えます。

マウスカーソルの形状も含めてデスクトップ画面をキャプチャしたくなりました!みたいな時は、もしかしたら『コレ』が使えるカモ?しれません・・・②

“Say Hello to Capity Plus.” A Lightweight screen capture utility

上の図のように、マウスカーソルの形状も含めてキャプチャできます!
範囲の選択には、矩形に加え、正方形/楕円/正円も使えるようになりました!


画像編集に際し、自分が欲しいと思う必要最低限の機能のみを実装したプログラムを前回アップロードし、その紹介記事で次のように書きましたが・・・

『このアプリは本格的な画像編集に使用するための素材、もしくは、操作方法の解説を作成するために必要な情報画像(部分的な切り抜き画像)を簡単に作成したいという目的を実現するために開発しました。ですので「現在、表示されている画面の全部、もしくは一部を、必要であればマウスカーソルの形状を含めた画像データとして取得する」ことしかできません。保存した画像データを再度読み込んで表示したり、キャプチャした画像を加工する(例えば、ぼかす・モザイクをかけるといったような)機能はありません。ただし、画像の指定範囲を「ぼかす・モザイクをかける」機能は、後日、追加できたら、追加したいと考えています。』

今回、「ぼかす・モザイクをかける」といった機能に加え、既存の画像ファイルを読み込んで表示したり、アルファチャンネルを用いた透明化処理を PNG 形式の画像処理に追加するといった、自分では使わないかな? と思う機能も搭載した新しいバージョンができましたので紹介させていただきます。

【追記】(20260517)

ラバーバンドをドラッグすると発生していたフリッカーですが、ラバーバンドとして利用していた VCL コントロールを TImage から TPaintBox に変更することで解消できました。修正プログラムは、この下のリンクからダウンロードできます。なお、今回の修正にあたり、プログラムの機能はそのまま、GUI もより使いやすくなるよう変更・改良しました。

【もくじ】

0.基本的な使い方と名称について(前回の記事 Plus α)
(1)起動方法
(2)キャプチャ方法
(3)操作パネルの位置の変更
(4)ラバーバンド形状
(5)処理一覧
 ・名称について
1.追加機能①「開く」
2.追加機能②「円形選択と保存・送信を可能に」
3.追加機能③「ぼかし処理」
4.追加機能④「モザイク処理」
5.追加機能⑤「白色化処理」
6.プログラムのダウンロード
7.まとめ
8.お願いとお断り

0.基本的な使い方と名称について

(1)起動方法

このアイコンをダブルクリックして起動します。

現在表示されているデスクトップ画面(の一部)をキャプチャするのが、このプログラムの主たる目的なので、メイン画面は起動時には表示されません。

起動時の画面
トースト通知(Toast Notification)の表示は Windows の設定により、出ない場合もあります。


元々、このプログラムを作ろうと思ったいちばんの理由は、『マウスカーソルの形状を含めて画面をキャプチャする必要が生じ、探した範囲では手軽に使えるアプリが見つからなかったので、それなら自分で書こうと思った』ことです。なので、この機能をいちばん最初に実装しました。

チェックボックスをチェックすればカーソルも含めて画面をキャプチャできます。

(2)キャプチャ方法

ショートカットキー「Shift+Ctrl+C」で現在表示されている画面全体をキャプチャできます(画面を指定してキャプチャすることはできません)。キャプチャした画像は「静かに」プログラムのメイン画面へと送られ(表示され)ます。その際、メッセージ等は何も表示されません。

キャプチャ後、タスクバーにあるオレンジ色のアイコンをクリックすると、メイン画面が表示されます。

もちろん、自分自身のキャプチャも可能です。
画面右側にキャプチャした画像のサムネイルが表示されます。(画面はリリース時のもの)
さらに機能を追加した現在の画面です。


(3)操作パネルの位置の変更

上の図に示したように、操作パネルは「メイン画面の上部/下部」いずれかへの配置を選択できるようにしました。

画面上部に操作パネルを表示する場合です。
(設定は即適用&自動的に保存され、次回起動時に適用されます)


(4)ラバーバンド形状

ラバーバンドの形は、円形も選択できるようにしました。Shiftキーを押しながらドラッグすることで、矩形を選択している場合は「四角形 → 正方形」、円形を選択している場合は「楕円 → 正円」へとラバーバンドの形状が変化します。なお、いったん、四角形(長方形)や楕円のラバーバンドを描画し、その後、ラバーバンドのグラブハンドルをクリックしてリサイズする場合も、Shiftキーを押しながら操作すると、ラバーバンドの形状は「四角形 → 正方形」or 「楕円 → 正円」へ変化します。

ラバーバンドの形状は「矩形」or「円形」いずれかを選択できます。


ラバーバンドの線については、太さと色を指定できます。プログラムは、終了時の設定を自動的に記録し、次回起動時は前回終了時の設定を読み込んで(=復元して)起動します。

線の太さは10段階で指定可能です。


色は、TColorBox のデフォルトの設定色3種類から選べます。

上記3種類にチェックがある場合、
2つ上の図の TColorBox には184色が選択可能な Item として設定されました。


(5)処理一覧

あとは、「画像をそのまま保存」したり、「矩形/正方形/楕円/正円のいずれかのラバーバンドでさらにキャプチャしたい範囲を選択して、選択範囲内で右クリックすると表示されるメニューから選択できる処理を選んで実行する」ことが可能です。

このような解説画面を『とにかく簡単に』作りたくて作ったのが Capity です!


処理可能な画像数は、正直、自分でもよくわかりません。お使いの PC 環境(搭載しているメモリの大きさ等)により変化するものと思われます。保存するファイルの名称は、もちろん任意の名称を付けることも可能ですが、デフォルト設定では「 Screenshot_20251005_032342.png 」のように Screenshot_ に続けて西暦年月日時分秒が自動的に付くので、これまでファイルとして保存する際に面倒に感じていた「名前を付ける」作業から完全に解放されました。作った自分で言うのもナンですが、すごく便利です!!

・名称について

Capity という名称は、こちらも前回の記事で、『 AI に相談して決めた!』と書きましたが、その際 AI が示してくれたのが次の内容です。

・発音が柔らかく親しみやすい。技術系にも一般向けにも通用する響き。
・Capture + Simplicity / Utility / Clarity などの抽象的な価値を含められる造語。
・「City(都市)」や「Clarity(明快さ)」にも近い響きがあり、好ましい印象を与える。
・「キャプチャの能力(Capacity)」を連想させることもでき、機能性の高さを暗示。
・ロゴ・UI・ドメイン名・SNS ハンドルなどにも使いやすく、拡張性が高い。

それがほんとうか、どうかは使ってくださった方のお気持ち次第ではありますが・・・ 自分的には、この AI が示してくれた内容を具現化したプログラムになるよう、精一杯努力したつもり・・・です。

もちろん「特許情報プラットフォーム J-Plat Pat」で、特許・実用新案、意匠、商標の各権利について過去に、この名称での申請・登録のないことは確認済みです。(2025年10月5日現在)

1.追加機能①「開く」

最初は既存の画像を開く処理は不要と考え、実装していませんでしたが、簡単に実装できますし、「ない」よりは「ある」方がいいかと思い直して実装しました。ただ、あくまでもこのプログラムは、「現在表示されている画面を簡単にキャプチャする」ことが主な目的なので、ボタンの位置は深く考えずにほとんどおまけ程度に実装しましたので、ボタン自体の使い勝手はよくないと思います・・・。

ボタンクリックで TOpenDialog が表示されます。


ファイルを開く場合の Path の設定は、TOpenDialog の機能まかせ(=Windows まかせ)です。前回使用したフォルダが記憶されていれば、そのフォルダが自動的に選択されます。

表示したい画像を選択して、「開く」をクリックしてください。


表示された画像に対して、必要な処理を適用してください。

2.追加機能②「円形選択と保存・送信を可能に」

範囲を選択するのに使うラバーバンドは、矩形に加え、円形の形状をしたものも使えるようにしました。さらに Shift キーを押しながらドラッグすることにより、正方形や正円も描画できます。

目的に応じて使い分けてください。
(この画像も自分自身で作成しました)


(1)円形選択時の保存処理

画像の保存について解説します。ラバーバンドの形状が矩形・円形のいずれであっても、画像の保存形式は BMP ・ PNG ・ JPEG から選んで1種類を指定できます。

デフォルト設定は PNG 形式ですが、ここでは JPEG 形式を選択しています。


キャプチャした画像の一部をラバーバンドで範囲選択し、選択した範囲内の任意の位置を右クリックすると次のようなサブメニューが表示されます。ここでは、まず、保存の処理から順に説明します。

ラバーバンド内を右クリックした際に表示されるメニューのコマンドは、
すべてラバーバンド内の画像のみに適用されます。


ラバーバンドが円形の場合、画像の保存時には注意が必要です。ラバーバンドの枠の内部の画像のみ保存対象とするのは、画像の形式によらず共通ですが、BMPとJPEG形式で保存する場合、枠外部分の透明化処理は行われず、枠外の部分は「白に塗りつぶされ」て保存されます。

「いいえ」をクリックした場合は、保存の処理自体がキャンセルされます。


保存された画像(例:BMP形式の場合)です。

青の部分がきれいに保存されています。


保存された画像(例:JPEG形式の場合)です。

圧縮処理が行われたため、青や緑が濃くなっています。


PNG 形式で保存する場合、次のメッセージが表示されます。用途に応じてラバーバンドの枠外の部分を「透明化する」もしくは「白く塗りつぶす」いずれかの処理を選択できます。

使用目的に合わせて処理を選択してください。


PNG 形式かつ「透明化あり」で保存した画像をフォトで見た場合です。

透明化した部分は黒くなっています。

同じ形式で「透明化なし・枠外を白く塗りつぶして保存」した場合の画像をフォトで見ると・・・

こちらは枠外の部分が黒くなっていません(当然ですが)


PNG 形式かつ「透明化あり」で保存した画像をパワーポイントに挿入してみました!

ラバーバンドの枠外が透明化されています。


同じ画像をWordに挿入してみました。

いったん保存してから挿入を行った結果です。


このように、PNG 形式かつ「透明化あり」で保存した画像は、「挿入」することで透明化処理が適用された状態で再利用できます。

(2)円形選択時のクリップボードへの送信

次に、クリップボードへの送信について説明します。

ラバーバンド内を右クリックして表示されるメニューから
「クリップボードへ送る」をクリックしてください。


ラバーバンドの枠が円形指定で、さらに範囲選択した部分を PNG 形式でクリップボードへ送信する場合、次のメッセージが表示されます。


「はい」を選んだ場合、例えば古いお絵描きソフトで背景色「黒」の画像を新規に作成しておいて、そこに円形(楕円)選択した範囲をクリップボードへ送信して(クリップボード経由で)貼り付けてみました。なお、このような場合には「背景色を透過色として貼り付ける(と同等の機能を利用して実行する)」必要があります。

思い出せないくらい、ながーい間愛用しているお絵描きソフトに
「背景色を透過色として」貼り付けてみました。


こちらが貼り付けた結果です。


背景が白の画像を円形で範囲選択して、背景が白の画像にクリップボード経由で貼り付けると困ったことになりますので、注意してください。

黒い字の部分が読めなくなってしまいます・・・


BMP や JPEG 形式を選択してクリップボードへ送信した場合は、次のメッセージが表示されます。


BMP 形式を選択し、表示されたメッセージの「はい」を選択して、クリップボードへ送信したデータを Word に貼り付けてみました。

ヒトの顔の切り抜きとか、そういう用途には使えるカモしれません。


この円形のラバーバンドに関する処理は、矩形時のそれにくらべると、要した時間は3倍以上かかっていると思います。とにかくない袖にタオルと雑巾を付け足して作った袖を振り回し、なんでもいいや、とにかくすーぱー頑張って作成しましたが、自分自身がこの円形のラバーバンドを使用する機会は今回限りであるような気が・・・。どこかで、どなたさまかのお役に立ってほしいと切に祈ります。

3.追加機能③「ぼかし処理」

ほんとうのことを言うと、円形のラバーバンドよりこちらを先に作成したのですが、どうしても実装したかった機能のひとつがこの「ぼかし処理」です。より低速になるのはわかりきっていましたが、搭載するならボックスブラーではなく、ガウシアンブラーと決めていました。

理由はただひとつ。少しくらい遅くても、「美しさ」を優先したかったのです。

ぼかす元画像です。


Box ぼかしを適用した画像です。

速いのですが、どうしてもジャギーな感じになります。

レベルは 10 まで指定できます。


ガウスぼかしを適用した画像です。

ごく自然な感じでぼかせます。
(レベルは5です)


文字にもガウスぼかしをかけてみました。レベルは5です。

ぼかす前の文字のある画像
ラバーバンドの内部をぼかした画像


ガウスぼかしとボックスぼかしのコードのセットです。ガウスぼかしのコードの下に、ボックスぼかしのコードがあります。Boxぼかしに変更するときは、ガウスぼかしの変数はそのまま、var 宣言部の count 変数だけコメントアウトを解除してください。コード部分は、ガウスぼかしのコードをすべてコメント化して、Box ぼかしのコメントアウトを解除してください。

procedure TForm1.HandleGaussianBlur(Sender: TObject; const SelRectOnParent: TRect);
var
  //ガウスぼかし
  bmpSrc, bmpTemp: TBitmap;
  x, y, dx, dy, i, j: Integer;
  //固定小数点演算のため Int64 を使用 (合計値が非常に大きくなるため)
  r, g, b: Int64;
  radius: Integer;
  //カーネルを固定小数点値 (Int32) の配列として定義し直す
  kernel: array of Int32;
  pSrc, pTemp: PByteArray;
  blurLevel: Integer;
  selRectLocal: TRect;

  cx, cy, rx, ry: Double;
  IsEllipse: Boolean;

  //Box ぼかし
  //count: integer;  //Box ぼかしをかける場合はここのコメントアウトを解除する

  //ガウスカーネル生成関数(固定小数点対応版)
  function CreateGaussianKernel(radius: Integer; var kernel: array of Int32): Double;
  var
    k: array of Double; //一時的に浮動小数点カーネルを作成
    sigma, sum: Double;
    i: Integer;
  begin
      //浮動小数点カーネルの計算
      SetLength(k, radius * 2 + 1);
    sigma := radius / 2.0;
    sum := 0.0;
    for i := -radius to radius do
    begin
      k[i + radius] := Exp(-Sqr(i) / (2 * Sqr(sigma)));
      sum := sum + k[i + radius];
    end;

    //正規化と固定小数点へのスケーリング
    for i := 0 to High(k) do
      //正規化して SCALE_VALUE を乗算し、整数に丸める
      kernel[i] := Round((k[i] / sum) * SCALE_VALUE);
    Result := sigma;
  end;

  function IsInsideEllipse(x, y: Integer): Boolean;
  var
    dx, dy: Double;
  begin
    dx := (x - cx) / rx;
    dy := (y - cy) / ry;
    Result := (dx * dx + dy * dy) <= 1.0;
  end;

begin

  //ガウスぼかし
  if not Assigned(imgPreview.Picture.Graphic) then Exit;

  PushUndo;

  selRectLocal.TopLeft :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.TopLeft));
  selRectLocal.BottomRight :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.BottomRight));

  Screen.Cursor := crHourGlass;

  bmpSrc := TBitmap.Create;
  try
    bmpSrc.PixelFormat := pf24bit;
    bmpSrc.SetSize(imgPreview.Picture.Width, imgPreview.Picture.Height);
    bmpSrc.Canvas.Draw(0, 0, imgPreview.Picture.Graphic);

    bmpTemp := TBitmap.Create;
    try
      bmpTemp.PixelFormat := pf24bit;
      bmpTemp.SetSize(bmpSrc.Width, bmpSrc.Height);

      blurLevel := TrackBar1.Position;
      radius := EnsureRange(blurLevel, 1, 10);
      SetLength(kernel, radius * 2 + 1);
      CreateGaussianKernel(radius, kernel);

      IsEllipse := (RadioGroup1.ItemIndex = 1);
      if IsEllipse then
      begin
        cx := (selRectLocal.Left + selRectLocal.Right) / 2;
        cy := (selRectLocal.Top + selRectLocal.Bottom) / 2;
        rx := (selRectLocal.Right - selRectLocal.Left) / 2;
        ry := (selRectLocal.Bottom - selRectLocal.Top) / 2;
      end;

      // 横方向ブラー
      for y := selRectLocal.Top to selRectLocal.Bottom - 1 do
      begin
        pSrc := bmpSrc.ScanLine[y];
        pTemp := bmpTemp.ScanLine[y];
        for x := selRectLocal.Left to selRectLocal.Right - 1 do
        begin
          if IsEllipse and not IsInsideEllipse(x, y) then
          begin
            pTemp[x * 3 + 2] := pSrc[x * 3 + 2];
            pTemp[x * 3 + 1] := pSrc[x * 3 + 1];
            pTemp[x * 3 + 0] := pSrc[x * 3 + 0];
            Continue;
          end;

          r := 0; g := 0; b := 0;
          for dx := -radius to radius do
          begin
            i := EnsureRange(x + dx, 0, bmpSrc.Width - 1);
            r := r + Int64(pSrc[i * 3 + 2]) * kernel[dx + radius];
            g := g + Int64(pSrc[i * 3 + 1]) * kernel[dx + radius];
            b := b + Int64(pSrc[i * 3 + 0]) * kernel[dx + radius];
          end;
          pTemp[x * 3 + 2] := Byte(r shr SCALE_SHIFT);
          pTemp[x * 3 + 1] := Byte(g shr SCALE_SHIFT);
          pTemp[x * 3 + 0] := Byte(b shr SCALE_SHIFT);
        end;
      end;

      // 縦方向ブラー
      for x := selRectLocal.Left to selRectLocal.Right - 1 do
      begin
        for y := selRectLocal.Top to selRectLocal.Bottom - 1 do
        begin
          if IsEllipse and not IsInsideEllipse(x, y) then
            Continue;

          r := 0; g := 0; b := 0;
          for dy := -radius to radius do
          begin
            j := EnsureRange(y + dy, 0, bmpSrc.Height - 1);
            pTemp := bmpTemp.ScanLine[j];
            r := r + Int64(pTemp[x * 3 + 2]) * kernel[dy + radius];
            g := g + Int64(pTemp[x * 3 + 1]) * kernel[dy + radius];
            b := b + Int64(pTemp[x * 3 + 0]) * kernel[dy + radius];
          end;
          pSrc := bmpSrc.ScanLine[y];
          pSrc[x * 3 + 2] := Byte(r shr SCALE_SHIFT);
          pSrc[x * 3 + 1] := Byte(g shr SCALE_SHIFT);
          pSrc[x * 3 + 0] := Byte(b shr SCALE_SHIFT);
        end;
      end;

      imgPreview.Canvas.CopyRect(selRectLocal, bmpSrc.Canvas, selRectLocal);

    finally
      bmpTemp.Free;
    end;
  finally
    bmpSrc.Free;
    Screen.Cursor := crDefault;
  end;


  //BoxBlurを試す場合は、上のガウスぼかしのコードをすべてコメントアウトする
  //BoxBlur
  {
  if not Assigned(imgPreview.Picture.Graphic) then Exit;

  PushUndo;

  selRectLocal.TopLeft :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.TopLeft));
  selRectLocal.BottomRight :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.BottomRight));

  Screen.Cursor := crHourGlass;

  bmpSrc := TBitmap.Create;
  try
    bmpSrc.PixelFormat := pf24bit;
    bmpSrc.SetSize(imgPreview.Picture.Width, imgPreview.Picture.Height);
    bmpSrc.Canvas.Draw(0, 0, imgPreview.Picture.Graphic);

    bmpTemp := TBitmap.Create;
    try
      bmpTemp.PixelFormat := pf24bit;
      bmpTemp.SetSize(bmpSrc.Width, bmpSrc.Height);

      blurLevel := TrackBar1.Position;
      radius := EnsureRange(blurLevel, 1, 10);

      IsEllipse := (RadioGroup1.ItemIndex = 1);
      if IsEllipse then
      begin
        cx := (selRectLocal.Left + selRectLocal.Right) / 2;
        cy := (selRectLocal.Top + selRectLocal.Bottom) / 2;
        rx := (selRectLocal.Right - selRectLocal.Left) / 2;
        ry := (selRectLocal.Bottom - selRectLocal.Top) / 2;
      end;

      // 横方向ボックスぼかし
      for y := selRectLocal.Top to selRectLocal.Bottom - 1 do
      begin
        pSrc := bmpSrc.ScanLine[y];
        pTemp := bmpTemp.ScanLine[y];
        for x := selRectLocal.Left to selRectLocal.Right - 1 do
        begin
          if IsEllipse and not IsInsideEllipse(x, y) then
          begin
            pTemp[x * 3 + 2] := pSrc[x * 3 + 2];
            pTemp[x * 3 + 1] := pSrc[x * 3 + 1];
            pTemp[x * 3 + 0] := pSrc[x * 3 + 0];
            Continue;
          end;

          r := 0; g := 0; b := 0;
          count := 0;
          for dx := -radius to radius do
          begin
            i := EnsureRange(x + dx, 0, bmpSrc.Width - 1);
            r := r + pSrc[i * 3 + 2];
            g := g + pSrc[i * 3 + 1];
            b := b + pSrc[i * 3 + 0];
            Inc(count);
          end;
          pTemp[x * 3 + 2] := Byte(r div count);
          pTemp[x * 3 + 1] := Byte(g div count);
          pTemp[x * 3 + 0] := Byte(b div count);
        end;
      end;

      // 縦方向ボックスぼかし
      for x := selRectLocal.Left to selRectLocal.Right - 1 do
      begin
        for y := selRectLocal.Top to selRectLocal.Bottom - 1 do
        begin
          if IsEllipse and not IsInsideEllipse(x, y) then
            Continue;

          r := 0; g := 0; b := 0;
          count := 0;
          for dy := -radius to radius do
          begin
            j := EnsureRange(y + dy, 0, bmpSrc.Height - 1);
            pTemp := bmpTemp.ScanLine[j];
            r := r + pTemp[x * 3 + 2];
            g := g + pTemp[x * 3 + 1];
            b := b + pTemp[x * 3 + 0];
            Inc(count);
          end;
          pSrc := bmpSrc.ScanLine[y];
          pSrc[x * 3 + 2] := Byte(r div count);
          pSrc[x * 3 + 1] := Byte(g div count);
          pSrc[x * 3 + 0] := Byte(b div count);
        end;
      end;

      imgPreview.Canvas.CopyRect(selRectLocal, bmpSrc.Canvas, selRectLocal);

    finally
      bmpTemp.Free;
    end;
  finally
    bmpSrc.Free;
    Screen.Cursor := crDefault;
  end;
  }
end;

4.追加機能④「モザイク処理」

もうひとつ、出来たら実装したかったのが指定範囲に「モザイクをかける」処理です。文字情報を隠す用途であれば、強くぼかし処理する(or 重ね掛けする)だけで十分な気もしましたが、私には「ぼかす」+「モザイクをかける」の二手間を1セットにして文字情報を隠したい場合に画像を処理するクセがあり(このブログの過去記事を見ていただければ理解していただけると思います)、今回も2つで1セットのような気がして・・・。

モザイク処理する元画像です。


とりあえず、レベル5を設定して・・・

レベルは10まであります。


モザイク処理してみた結果です。ボックスぼかしみたいですね。なのでボックスぼかしは実装しませんでした。


文字をモザイク処理してみました。レベルは5です。

モザイクをかける前の画像
モザイク処理したラバーバンド内は、色の違いしか、わからなくなりました!


モザイクをかける処理のコードです。ご参考まで。

procedure TForm1.HandlePixelation(Sender: TObject; const SelRectOnParent: TRect);
var
  selRectLocal: TRect;
  bmpSrc: TBitmap;
  startX, startY: Integer;
  dx, dy: Integer;
  blockSize: Integer;
  r, g, b, count: Integer;
  pLineRead, pLineWrite: PByteArray;
  BlockWidth, BlockHeight: Integer;
  cx, cy, rx, ry: Double;
  IsEllipse: Boolean;

  function IsInsideEllipse(x, y: Integer): Boolean;
  var
    dx, dy: Double;
  begin
    dx := (x - cx) / rx;
    dy := (y - cy) / ry;
    Result := (dx * dx + dy * dy) <= 1.0;
  end;

begin

  if not Assigned(imgPreview.Picture.Graphic) then Exit;

  PushUndo;

  selRectLocal.TopLeft :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.TopLeft));
  selRectLocal.BottomRight :=
    imgPreview.ScreenToClient(plImage1.Parent.ClientToScreen(SelRectOnParent.BottomRight));

  bmpSrc := TBitmap.Create;
  Screen.Cursor := crHourGlass;

  try
    bmpSrc.PixelFormat := pf24bit;
    bmpSrc.SetSize(imgPreview.Picture.Width, imgPreview.Picture.Height);
    bmpSrc.Canvas.Draw(0, 0, imgPreview.Picture.Graphic);

    blockSize := EnsureRange(TrackBar2.Position, 2, 50);

    IsEllipse := (RadioGroup1.ItemIndex = 1);
    if IsEllipse then
    begin
      cx := (selRectLocal.Left + selRectLocal.Right) / 2;
      cy := (selRectLocal.Top + selRectLocal.Bottom) / 2;
      rx := (selRectLocal.Right - selRectLocal.Left) / 2;
      ry := (selRectLocal.Bottom - selRectLocal.Top) / 2;
    end;

    startY := selRectLocal.Top;
    while startY < selRectLocal.Bottom do
    begin
      startX := selRectLocal.Left;
      while startX < selRectLocal.Right do
      begin
        r := 0; g := 0; b := 0; count := 0;

        BlockWidth := blockSize;
        if startX + BlockWidth > selRectLocal.Right then
          BlockWidth := selRectLocal.Right - startX;

        BlockHeight := blockSize;
        if startY + BlockHeight > selRectLocal.Bottom then
          BlockHeight := selRectLocal.Bottom - startY;

        //平均色の計算(楕円内のみ)
        for dy := 0 to BlockHeight - 1 do
        begin
          pLineRead := PByteArray(bmpSrc.ScanLine[startY + dy]);
          for dx := 0 to BlockWidth - 1 do
          begin
            if IsEllipse and not IsInsideEllipse(startX + dx, startY + dy) then
              Continue;

            r := r + pLineRead[(startX + dx) * 3 + 2];
            g := g + pLineRead[(startX + dx) * 3 + 1];
            b := b + pLineRead[(startX + dx) * 3 + 0];
            Inc(count);
          end;
        end;

        if count > 0 then
        begin
          r := r div count;
          g := g div count;
          b := b div count;
        end;

        //平均色の適用(楕円内のみ)
        for dy := 0 to BlockHeight - 1 do
        begin
          pLineWrite := PByteArray(bmpSrc.ScanLine[startY + dy]);
          for dx := 0 to BlockWidth - 1 do
          begin
            if IsEllipse and not IsInsideEllipse(startX + dx, startY + dy) then
              Continue;

            pLineWrite[(startX + dx) * 3 + 2] := r;
            pLineWrite[(startX + dx) * 3 + 1] := g;
            pLineWrite[(startX + dx) * 3 + 0] := b;
          end;
        end;

        Inc(startX, blockSize);
      end;
      Inc(startY, blockSize);
    end;

    imgPreview.Canvas.CopyRect(selRectLocal, bmpSrc.Canvas, selRectLocal);

  finally
    bmpSrc.Free;
    Screen.Cursor := crDefault;
  end;

end;

ガウスぼかし + モザイク処理の結果です。レベルはどちらも5です。

未処理の画像です。
ラバーバンド内をガウスぼかし + モザイク処理(ともにレベルは5)

5.追加機能⑤「白色化処理」

つい、(不要なのに)マウスの形状を含めてキャプチャしちゃった! みたいな場合、役に立つかも・・・と考え、実装しました。私の場合、背景色は「白」であることが多いので、単に指定範囲を「白で塗りつぶす」処理です。

不要なマウスカーソルまでキャプチャしちゃった!


マウスカーソル部分を範囲選択して右クリック、メニューの「白色化」をクリックします。


不要なカーソルは消えました☆

これだけです!!!

【追記_20251006】

ぼかし加工やモザイク処理を行った画像データが保存できない不具合を修正しました。この修正に合わせて、ぼかし加工やモザイク処理、及び白色化等、画像データを加工した場合は、任意の段階で「保存」ではなく、メモリ上の表示用データに「反映」する機能を追加しました。具体的には、次の通りです。

次の図のように画像の一部を加工します。例:白色化

画面右のサムネイルには変更が反映されていません。


この状態で、画面右のサムネイルをクリックすると、画像に設定した変更内容はすべて破棄され、加工前の画像が表示(復元)されます。加工状態がサムネイルに反映されていない状態でのサムネイル・クリックは、「一気に元に戻す処理になる」とお考えください。

メモリ上の画像データの更新は「反映(英語表記は Apply)」ボタンをクリックすることで実行できます。

ちいさなボタンですが・・・


このボタンをクリックすることで、メモリ上の画像データが、メイン画面に表示されている加工した状態の画像データに更新されます。

画面右のサムネイルにも変更が反映されます。


このようにメモリ上のデータの更新処理をユーザー側に委ねることで、Undo / Redo の履歴操作とサムネイル更新が完全に分離され、処理の整合性が保たれるようにしました。

初期バージョンで作者の保存機能に関する確認作業が至らなかったため、ご迷惑をおかけした皆さまに、こころからお詫び申し上げます。誠に申し訳ありませんでした。

現在、未発見の不具合が見つかりました場合は、こちらで報告し、速やかに修正版を掲載いたしますので、万一、お使いいただける場合は、修正版のアップロードの有無にご注意いただけましたら幸いです。

【追記_20251007】

自分で使っていて(そのために作ったプログラムですが)、画面のキャプチャだけでなく、クリップボードに画像データがある場合、それをショートカットキー( Ctrl + V )で貼り付けて、さらに加工等できたらいいなぁ・・・ と思いましたので、本日即、実装しました。

ショートカットキーを利用して、このプログラムのメイン画面に貼り付け可能な画像形式は、BMP、PNG、JPEG、GIF、TIFF に加え、画像の読み込みに必要な外部ライブラリが導入されていれば、HEIC や WebP にも対応可能と思われます。※ すべての画像形式について、その読み込みの可否に関する動作検証は行っておりません。もし、クリップボードにあるデータが、このプログラムで読み込めない画像データであった場合には、その旨を伝えるメッセージが表示されます。

また、乱暴な言い方で申し訳ないのですが、プログラムのバージョン管理は、まったく考えておりませんでしたので、この下のダウンロードリンクからダウンロードできるものが最新版です。

このプログラムは EXE ファイル1つで単体動作します(プログラム終了時の VCL コントロールの諸設定は C:\Users\ユーザー名\AppData\Roaming\Capity\settings.ini に保存されますが、この ini ファイルはなくてもプログラムは動作します)。また、レジストリは一切汚しておりませんので、 EXE ファイルを上書きすれば、プログラムの更新作業は完了します。

【追記_20251009】

自分で使っていて欲しいと思った機能をさらに追加しました。それは、矢印キーによるラバーバンドの移動と、形状の微調整です。以下、具体的な操作方法です。

移動方法:ラバーバンドが選択(Visible)されている状態で、Shiftキー + Ctrlキー + 各矢印キー押し下げで、現在の幅と高さを保ったまま、押し下げた矢印キーの方向に1ピクセルずつ移動できます。

形状の微調整:Shiftキー + 右向き矢印キーで幅を1ピクセルずつ増加、Shiftキー + 左向き矢印キーで幅を1ピクセルずつ減少、Shiftキー + 上向き矢印キーで高さを1ピクセルずつ減少、Shiftキー + 下向き矢印キーで高さを1ピクセルずつ増加させることができます。

【追記_2025_1012-1025】

さらに複数の機能を追加しました。追加した機能は、次の通りです。

(1)起動時に自動消音

操作時の Beep 音が気になるので、起動時に自動消音するようにしました。

起動直前の音量設定は保存されており、終了時に復元されます。


(2)グレイスケール / セピア 色の画像に変換

表示されている画像を、Grayscale もしくは Sepia いずれかの画像へ変換できるようにしました。

変換元の画像


・グレースケールに変換

グレースケールに変換


・セピアに変換

Sepia を指定した場合は、その強度を 0.1 ~ 1.0 の範囲で設定できます。

セピア色変換の強度を指定


強度「1.0」で変換した例です。

セピア色に変換


(3)背景色の切り替え

表示する画像に応じて、背景色を「白/黒」のいずれかに指定できるようにしました。背景が「白」の画像を表示する際は、背景色に「黒」を指定すると画像境界が明確になります。

背景色は「白」を指定した状態です。


背景色が「白」だと、画像の境界がわからない場合があります。


背景色を「黒」に切り替えました。画像の境界が明瞭になり、範囲選択しやすくなります。


なお、背景色の設定は、他の設定同様、終了時の設定が自動的に保存され、次回起動時は前回終了時の設定が復元された状態で起動します。

(4)回転

操作パネルの「回転」部分に任意の値を指定して「▶」ボタンをクリックしてください。正の値で時計回り(右回り)、負の値で反時計回り(左回り)に表示されている画像が回転します。

回転元画像です。

回転角度は、時計回りに3°を指定


「▶」ボタンクリックで回転が行われます。


メモリ上のデータに反映するか・どうかの確認が行われます。「はい」で反映され、「いいえ」で「取り消し処理(UnDo)」が実行されます。


(5)拡大と縮小

拡大と縮小処理を行います。100.00 %が等倍で、それより小さい値を指定すれば縮小、大きな値を指定すれば拡大されます。

縮小率 50 %を指定


うっすらと、図の周囲に灰色の線が描画されますが、ご容赦ください。


回転と同様に、メモリ上のデータに反映するか・どうかの確認が行われます。「はい」で反映され、「いいえ」で「取り消し処理(UnDo)」が実行されます。


(7)枠線

枠線部分はそのままですが、色は色名の表示をカットし、選択できる色数も減らしました。


(8)グラブハンドルの非表示

表示/非表示を切り替える CheckBox を追加

ラバーバンドのみ描画する状態も選択できるようにしました。工夫次第で次のような色枠で囲んだ画像を作成することもできます。


(9)表示している画像全体を選択可能にしました。クリアボタンクリックで選択を解除できます。


その他 20251025 のアップデートでは、ラバーバンド内を右クリックすると表示されるサブメニューの項目の Enabled を設定可能とし、起動直後は「取り消し」処理が選べないようにしました。

【追記_2025_1109】

次の不具合を修正しました。

(1)グラブハンドルの表示のON・OFFがチェックボックスの状態と矛盾しないように修正

ラバーバンドが表示されていない状態で、グラブハンドルの表示のチェックボックスのチェックをOFFにした場合、グラブハンドルの表示のON・OFFがチェックボックスの状態の反転してしまう不具合を修正しました。具体的には、ラバーバンドが表示されたら、チェックボックスの操作を可とすることでチェックボックスのチェック状態とグラブハンドルの表示状態の整合性が保たれるようにしました。

(2)サブメニュー表示時にスクロールバーが消える不具合を修正

ラバーバンドを表示した状態で、ラバーバンド内を右クリックしてサブメニューを表示するとスクロールバーが消える不具合を修正しました。

(3)サムネイル表示位置の不具合を修正

画面右に表示されるサムネイルの表示に「隙間が空く・重ねて表示される」等の不具合が発生することがありました。サムネイルが常に正しく表示されるよう、必要な修正を加えました。

ご迷惑をおかけしましたことを、心からお詫び申し上げます。もしかしたら、まだ未発見の不具合が他にもあるかもしれません。発見次第速やかに修正し、こちらで報告させていただきます。

6.プログラムのダウンロード

今回の記事で紹介した PC の画面キャプチャを実行するプログラム Capity Plus 一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt をご覧ください。

なお、プログラムの初回起動時には、Windows Defender SmartScreen による警告画面が表示されます。この警告画面に関する詳細は、当 Blog の次の過去記事をご参照ください。

7.まとめ

初期バージョンに追加した機能のまとめです。

(1)既存の画像も開けるようになりました。
(2)ラバーバンドの形状に円形( Drag : 楕円/Shft & Drag : 正円)を追加しました。
(3)操作パネルの位置を上 or 下に設定できるようにしました。
(4)クリップボードへの送信機能を円形対応にバージョンアップしました。
(5)指定範囲に「ガウスぼかし」をかけることができるようになりました。
(6)指定範囲を「モザイク処理」できるようになりました。
(7)指定範囲を「白色化」できるようになりました。
(8)終了時設定を C:\Users\ユーザー名\AppData\Roaming\Capity\settings.ini に自動保存。
(9)ショートカットキー( Ctrl + V )でクリップボードにある画像データを貼り付け。
(10)画像の加工内容をメモリ上の画像データに反映する機能を追加。
(11)起動時にボリュームが0より大きい場合は、自動消音(終了時に自動復元)。
(12)グレイスケール / セピア 色の画像への変換機能を追加。
(13)背景色の切り替えが可能になりました(白と黒のいずれかを指定)。
(14)画像の回転(時計回り・反時計回り)
(15)画像の拡大と縮小

機能の追加(20251019)

(16)グラブハンドルの表示/非表示の切り替えが可能になりました。

機能の追加(20251025)

(17)表示している画像全体を選択・選択解除

全体として、「操作パネル」では「現在、表示されている画像全体の加工・設定」を行い、画像上をマウスでドラッグすることで描画される「ラバーバンドの内側」を右クリックすると表示されるサブメニューのコマンド群は、「ラバーバンドで指定した範囲内のみを加工・設定」するものとお考えいただけましたら幸いです。

(18)不具合を修正(20251109)

グラブハンドルの表示/非表示の切り替えがチェックボックスのチェック状態と反転する問題、右クリックメニュー表示時にスクロールバーが非表示になる問題、サムネイル表示位置の問題それぞれについてプログラムに必要な修正を加えました。

機能の追加(20251116)

(19)ショートカットキー( Ctrl + C )でラバーバンドで囲った範囲をクリップボードへ送信。

なんでこの機能を最初から準備しなかったのか、自分でもよくわからないのですが、遅ればせながら次のコードを FormKeyDown 手続きに追加しました。

  //Ctrl+Cで選択範囲をクリップボードへ送る
  if plImage1.Visible then
  begin
    //Ctrl + C(Shiftを含まない)のみ許可
    if (Key = Ord('C')) and (ssCtrl in Shift) and not (ssShift in Shift) then
    begin
      //plResizeImage の「クリップボードへ送る」を実行
      if Assigned(plImage1.MenuClipboardRef) then
      begin
        plImage1.MenuClipboardRef.Click;
      end;
      //ショートカットキーを他に伝播させない
      Key := 0;
    end;
  end;

これで Ctrl + C で、ラバーバンドで囲んだ範囲を、クリップボードへ画像データとして送信できるようになりました。

機能の追加(20251117)

(20) ラバーバンドで囲った範囲を「上下反転」、「左右反転」できるようにしました。

例えば、次のような画像データに対して・・・


軸の目盛りの数値部分はそのまま、グラフの部分だけをラバーバンドで選択し、その選択した範囲内の任意の位置を右クリックして表示されるサブメニューから「上下反転」を選べば・・・


グラフの部分だけ、上下反転できます。


同様に、元のデータに対して左右反転を選べば・・・


それをさらに上下反転させることももちろん可能です。


具体的な利用例としては、例えばテストの選択問題の選択肢とする数種類のグラフを、元データとする数値の入力なしで上記のように簡単に作成できます。

また、表示されている画像データ全体を「上下反転」もしくは「左右反転」したい場合は、メニューにある「全選択」ボタンをクリック(画像全体を範囲指定)してから、画像上の任意の位置を右クリックしてサブメニューを出し、「上下反転」もしくは「左右反転」を実行してください。


なお、この「上下反転」及び「左右反転」の処理は、GDI+を使用して書きましたので、画像が大きくても高速に動作すると思います(私の環境で実行した限りですが、期待通りに動きました)。

機能の追加(20251123)

(21)ネガティブイメージの作成を可能にしました。

グレイスケールとセピアへの変換に加え、ネガティブイメージも作成できるようにしました。仕事をする上でその必要性が生じましたので、早速、新機能として追加しました。アルゴリズムには ScanLine を使用しましたので、それなりに高速動作すると思います。また、32bit画像(pf32bit) 対応済みです。

機能の修正(20260517)

・ラバーバンドをドラッグする際、発生していたフリッカー(激しい振動)問題を解消しました。

【重要】
元に戻す( Ctrl + Z )際には、表示されている画像の状態をメモリ上のデータに「反映」させるか・どうかを問うメッセージは表示されません。これは「どこまで元に戻すのか?」の判定を自動で実行(判断)することが不可能なためです。ですので、どのタイミングで処理を「確定」させるかは、ユーザーサイドにお任せする仕様としてあります。もちろん、処理を「反映」させても元に戻す処理はある程度効きますが、画像への加工内容をメモリ上のデータに「反映」させるタイミングはあくまでもユーザーサイドでの判断に委ねる仕様ですので、この点につきましては十分ご留意ください。

また、画像を多量に扱うプログラムの場合は、メモリリークが心配ですので、プロジェクト・ソースの CapityPlus.dpr ファイルに以下のように” ReportMemoryLeaksOnShutdown := True ”を設定して、プログラムの終了後、もし、メモリリークがあればその状況を表示するようにして(あくまでも、私がテストした範囲内に限ってのことではありますが)プログラムが正常終了した際にメモリリークが発生しないことを確認済みです。

program CapityPlus;
uses
  Vcl.Forms,
  Unit1 in 'Unit1.pas' {Form1};

{$R *.res}

begin
  ReportMemoryLeaksOnShutdown := True;
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

8.お願いとお断り

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

作者が気がついた限りの範囲ではありますが、動作検証を行い、発生したエラー等の不具合は誠心誠意取り除いたつもりですが、まだまだ未発見のバグは必ずあると思います。もともと、このブログの記事を書くために開発したプログラムなので、今後も使用する中で発見できた不具合は、可能な限り速やかに修正して、こちらの記事で追加報告させていただきます。上記ダウンロードリンクからダウンロードできる版が最新版です。実行ファイル( exe )は単体で動作しますので、バージョンアップは旧版を削除して新版にするか、単に新版を旧版に上書きするだけで完了します。

マウスカーソルの形状も含めてデスクトップ画面をキャプチャしたくなりました!みたいな時は、もしかしたら『コレ』が使えるカモ?しれません・・・

【追記_20251005】

ここで紹介している画面キャプチャプログラムに新しい機能を追加した Capity Plus を公開しました。新しいプログラムは以下のリンク先からダウンロードできます。

(以下、2025年9月27日に掲載した記事です)

ほとんど、自分専用のプログラムですが、夢見た通りのモノができました! *(^_^)*♪

マウスのカーソルを含めて画面をキャプチャできます!


起動時に出てくるのはこれだけです!
( OS 側の設定によっては、表示されないこともあるようです)

アプリ起動時に表示される案内です。


過去記事で、怖れ多くも『マウスカーソルの形状も含めて画面のハードコピーを取るプログラムを自分で書いてしまいました。後日、機会がありましたら、この Blog でご紹介したいと思います。』と、ご案内しましたアプリケーションもどきです。

私が試した限りですが、期待通りに動作している気がします・・・。

【推奨動作環境】

業務用PCの 1366 × 768 、拡大率 100 % のモニターで制作・動作確認を行っています。高 DPI 環境下で拡大率 150 %や 200 %の設定では意図した通りに画面をキャプチャできません(Windows がアプリに対してスケーリングされた論理座標を提供するため、キャプチャ結果がズレたり、キャプチャサイズが異なったりします)。ですので、このアプリについては、画面の拡大率 100 %での使用を強く推奨させていただきます。

【もくじ】

1.手作りの『それ』が必要になった理由
2.使い方
3.☆ここではまりました☆
4.ダウンロードはこちらから
5.お願いとお断り

1.手作りの『それ』が必要になった理由

前回、『解答欄(矩形)検出器を改良しました!』の記事を書いた際、どうしても必要になったのがマウスカーソルの形状を含めて画面をキャプチャできるプログラムです。

AI に「マウスカーソルの形状を含めて画面をキャプチャするいちばんかんたんな方法は?」と尋ねてみると Windows 標準搭載の「拡大鏡」を使えばいいよ ♪・・・とのご回答を頂戴したのですが、試して3秒後には、

( 思ったのと、ちがう )

そう感じて画面を閉じてしまいました。

他に、いつも愛用している「 Lightscreen 」という画面キャプチャユーティリティも AI から紹介されましたが、こちらはもう、ほんとうに素晴らしいソフトウェアで、この Blog に掲載した画像もほぼ全部、LightScreen でキャプチャしたものと言っていいくらいです。

ただ、残念ながら、今回の目的である『簡単に』マウスカーソルの形状を含めてキャプチャするという目的の実現に関しては、『タイマーの設定が必要』等の部分で、この目的の実現に関する部分に限っての話ですが、LightScreen は( ベスト・オブ・ベストの選択肢とは言えないかな・・・)って、思ってしまいました。

いつも、お世話になってるのに、こんなコメントしかできなくて、ほんとうにごめんなさい。

それからこれは、僕の PC で起きてしまったことなので、書かせていただきますが、僕のような素人には何が原因なのか・・・さっぱりわからないのですが、何らかの設定変更のあと?でしょうか・・・ タイトルバーが以前よりすごく大きく(逆に文字はほとんど読み取れないくらい小さく)表示されるようになることが(時として)あり、さらにこうなった時は、タイトルバー部分をクリックしてアクティブにして、画面上の位置を変えようとドラッグしても微動だにしないという不思議な現象に遭遇・・・ たしか、以前はそんなことは、なかった・・・ ような気がするのですが・・・

その際は、次のように操作してコトなきを得ましたが・・・。

( ただ、この方法でも「動かない」こともありました・・・ )

左上のアイコンを右クリックすると表示される
サブメニューの移動をクリックしてタイトルバー部分を
ドラッグすると移動できたり・できなかったり・・・

あと、表示されるタイトルバーも文字も、なぜかすごく大きい。
(逆にアプリの文字は、ものすごく小さい)

この状態で、マウスカーソルの形状を含めて画面キャプチャする機能を試そうと思ったのですが、今度は、設定?画面の文字が小さすぎて読めません・・・。

いったい何が原因でこうなったのか、それがわかりません。
私の使い方が間違っていると思うのですが、その間違いがわかりません。


exe を右クリックして、プロパティを表示し、「高 DPI 設定の変更」も試しましたが、効果がありません。これにはどう対応していいのか、まったく手も足も出ず、ほんとうに困ってしまいました。

【これは Windows 側の問題ではないかと?】

Lightscreen の名誉のために追記します。この Lightscreen の見た目の問題ですが OS を再起動したら元の姿に戻りました。再起動前に自分がナニをやったのか、よくよく考えてみると、解像度の変更を数回繰り返したように思います。もしかしたら、そのへんに何か、原因があったのかもしれません。

OS を再起動したら元の姿に戻ってくれました!

拝啓 Lightscreen 様

ご心配をおかけしましたこと、心より深くお詫び申し上げます。
どうか、これまで通り、仲よくしてください!!

いずれにしても、ここまで来てしまった以上、他人様に頼って問題を解決しようという、

甘え切った姿勢に
問題の真の原因がある

のは、火を見るよりも明らかです。それならば、取るべき道はただ一つ。

そう、自前で・・・
なんとかするしか、
ありません。

・・・

そうです。自前でなんとかするしか、ないのですが・・・

なんか、最近、そんなコトばっかり で・・・

それって、やっぱり・・・

前から、ちょっとは、思ってたケド。

僕は能力が低くて、適応力がなくて、ノロくて、グズだから、

もっと アリテー に言えば、ク〇ク〇パーだから

みんなが使ってる、一般的なアプリにすら馴染めないんだ。T_T

僕はやっぱりダメなんだ・・・。

ダメ人間なんだ!!

ひー(心の声)

でも、いいんだ。

僕には Delphi がいてくれる。

そう・・・ いつも Delphi がいてくれる。

Delphi があれば、なんにも心配なんかない。

汎用のアプリなんか使えなくても、自力で自分専用のを作るカラ☆

Delphi だけがトモダチさぁ♪

(この変わり身の早さだけが身上です)

こうして深い悲しみを、無上のよろこびに変えるべく、「その気になればマウスカーソルの形状も含めて画面キャプチャが可能なプログラム作り」がはじまりました。

あぁ アプリの名前、なんにしよー☆ みたいな・・・

みんなは、思ったように使えるアプリがないとき、いったいどーしてるのかなー? みたいな・・・

2.使い方

マウスのカーソルを含めてPCの画面をキャプチャする核心部分のコードは次の通りです。参考資料として、「ホットスポット」に対する考慮がないため、IDE のエディタ上などでカーソルが I ビームとなった状態でPC画面をキャプチャすると、マウスカーソルの位置がズレるコード( Version_01 )もコメント化した状態で載せています。試用される場合、コメント化してある下記 Version_01 のコードは、期待通りに動作しないコードであることに、どうかご注意ください。

また、キャプチャした画像のスクロールに関しても(画像関連のプログラムを書くときは、いつもこれが問題になるのですが)、なめらかにスクロールできるように処理を追加しています。Windows11の設定がデフォルト設定のままであれば、マウスのホイールを回転させれば上下方向にスクロール、Shift キーを押しながら、マウスのホイールを回転させれば、左右方向にスクロールします。

procedure TForm1.CaptureDesktop(ABitmap: TBitmap; IncludeCursor: Boolean);
var
  DC: HDC;
  R: TRect;
  CursorInfo: TCursorInfo;
  Pt: TPoint;
  //マウスのカーソル位置を正しくキャプチャするために追加
  IconInfo: TIconInfo;
begin

  //Version_01(マウスカーソルの位置がずれる)
  {
  if not Assigned(ABitmap) then Exit;
  R := Rect(0, 0,
    GetSystemMetrics(SM_CXSCREEN),
    GetSystemMetrics(SM_CYSCREEN));
  DC := GetDC(0);
  try
    ABitmap.PixelFormat := pf24bit;
    ABitmap.Width := R.Right;
    ABitmap.Height := R.Bottom;
    BitBlt(ABitmap.Canvas.Handle, 0, 0, R.Right, R.Bottom,
           DC, 0, 0, SRCCOPY);
    if IncludeCursor then
    begin
      CursorInfo.cbSize := SizeOf(CursorInfo);
      if GetCursorInfo(CursorInfo) and (CursorInfo.Flags = CURSOR_SHOWING) then
      begin
        GetCursorPos(Pt);
        DrawIcon(ABitmap.Canvas.Handle, Pt.X, Pt.Y, CursorInfo.hCursor);
      end;
    end;
  finally
    ReleaseDC(0, DC);
  end;
  }

  //上のコードを実行するとキャプチャした画像の「マウスカーソルの位置がずれる」
  //これは( Delphiの? )スクリーンキャプチャでよく起きる現象のようだ

  //【問題の原因】

  //GetCursorPos(Pt);
  //DrawIcon(ABitmap.Canvas.Handle, Pt.X, Pt.Y, CursorInfo.hCursor);

  //「カーソルのホットスポット(実際のクリック位置)」を考慮せずに
  //アイコンの左上を (Pt.X, Pt.Y) に描画している。

  //マウスカーソルは単なるアイコンではなく、
  //「ホットスポット」という基準点(通常は左上から数ピクセルずれた位置)が存在する。

  //Delphi のコードではその補正をしていないため、
  //キャプチャしたカーソルが右下に数ピクセルずれてしまう。

  //特に Delphi IDE のエディタ上では I ビームカーソルなど、
  //ホットスポットが左上から大きくずれているものを使うので、ズレが目立つ。

  //一方 GUI デザイン画面では標準の矢印カーソル(ホットスポットが左上 0,0 のもの)
  //が使われるのでズレが目立たない。

  //【修正方法】

  //GetIconInfo を使い、カーソルのホットスポットを考慮して描画位置を補正する。
  }

  //スクロールに関する問題の解決方法
  //1. AutoScroll := True (デフォルト)でスクロールバーは自動表示される
  //2. 手続き内で ScrollBox1.HorzScrollBar.Visible := False を設定
  //3. 以降、スクロールバーが表示されなくなる(領域を超えても)
  //いったん Visible を False にすると
  //Delphi が「このスクロールバーは使わない」と判断してしまい、
  //AutoScroll の制御対象から外れてしまう。

  ScrlPreview.AutoScroll:= True;
  ScrlPreview.HorzScrollBar.Visible := True;
  ScrlPreview.VertScrollBar.Visible := True;
  //ScrlPreview.Realign; // ← 状況によってはこれも必要?

  //Version_02(マウスカーソルの位置も正しく取得できる)
  if not Assigned(ABitmap) then Exit;

  R := Rect(0, 0,
    GetSystemMetrics(SM_CXSCREEN),
    GetSystemMetrics(SM_CYSCREEN));

  DC := GetDC(0);
  try
    ABitmap.PixelFormat := pf24bit;
    ABitmap.Width := R.Right;
    ABitmap.Height := R.Bottom;
    BitBlt(ABitmap.Canvas.Handle, 0, 0, R.Right, R.Bottom,
           DC, 0, 0, SRCCOPY);

    if IncludeCursor then
    begin
      CursorInfo.cbSize := SizeOf(CursorInfo);
      if GetCursorInfo(CursorInfo) and (CursorInfo.Flags = CURSOR_SHOWING) then
      begin
        GetCursorPos(Pt);
        if GetIconInfo(CursorInfo.hCursor, IconInfo) then
        try
          //ホットスポットを考慮してカーソルを描画
          DrawIcon(ABitmap.Canvas.Handle,
            Pt.X - Integer(IconInfo.xHotspot),
            Pt.Y - Integer(IconInfo.yHotspot),
            CursorInfo.hCursor);
        finally
          if IconInfo.hbmMask <> 0 then DeleteObject(IconInfo.hbmMask);
          if IconInfo.hbmColor <> 0 then DeleteObject(IconInfo.hbmColor);
        end;
      end;
    end;
  finally
    ReleaseDC(0, DC);
  end;
end;

プログラムの名称は、AI に相談したら即 ” Capity ” がいいよ☆との提案がありまして、それに決めました。なお、この ” Capity ” という名称は、特許情報プラットフォーム「 J-PlatPat 」のキーワード検索で「特許・実用新案、意匠、商標」の四法について過去の登録・申請の有無を調査し、2025年9月27日現在、この名称に該当する登録・申請が0件であることを確認済みです。

Capity – Lightweight Screen Capture Tool

【使い方】

上記コードを核心部分に据えて、(作者が)最低限必要と思われるアレやコレを GUI として付け足して、完成した気がする Capity.exe をダブルクリックして起動すると・・・


画面右下に操作方法の案内(トースト通知:Toast Notification?)だけを表示して、プログラム本体は非表示状態(タスクバーには表示されます)で起動します。

プログラム本体は「非表示」状態で起動します!


プログラム本体を表示するには、タスクバーの Capity (読めないかもしれません!)アイコンをクリックしてください。


タスクバーの Capity アイコンをクリックすると本体が表示されます。

プログラム本体を表示(起動時の画面をこのアプリでキャプチャしました)
なお、メイン画面の画像は、右下隅へスクロールした状態を取得(キャプチャ)しています。


マウスのカーソルの形状を含めて PC の画面をキャプチャする設定であった場合には、起動時に画面右下の通知領域に次の案内が表示されます。ただし、高DPI環境では、ヒントの位置がずれて表示されないことがあるようです。ところで、この表示の名称は「トースト通知」でいいのでしょうか?

起動時の表示は、ヒントの表示が無効化されている場合も、当然、出ません!


上記案内にある通り、Shift + Ctrl + C ( これは Shift キーと Ctrl キーと C キーを同時に押すことを意味します → 実際の操作では、左手でキーボード左下にある Shift キーと Ctrl キーを同時に押しつつ、右手で C キーを押します)のキー操作で現在表示されている PC 画面が、マウスのカーソルの形状も含めてキャプチャされます。

マウスのカーソルの形状を含めずに PC の画面をキャプチャする設定であった場合には、起動時に次の確認画面が表示されます。


「はい」・「いいえ」どちらを選択しても、画面右下の通知領域に上記案内を出して、プログラム本体は非表示(タスクバーには表示)で起動します。

「はい」を選んだ場合は、IncludeCursor という名前のチェックボックスにチェックが入ります。このチェックボックスの状態(チェックの有無)やその他の VCL コントロールの選択(設定)状態は、プログラム終了時に自動的に保存され、次回はこの自動保存された設定に基づいて起動します。

・設定の保存先フォルダ:C:\Users\ユーザー名\AppData\Roaming\Capity
・設定の保存ファイル:settings.dat

「EN」のチェックを外すと各 VCL コントロールのキャプションは日本語表記になります。


日本語表記の状態です。

「EN」をチェックすれば英語表記に戻ります。


あとは、キャプチャしたい画面を最前面に表示して、Shift + Ctrl + C のショートカットキーで PC 画面をキャプチャしてください。上の画像にある「キャプチャ」ボタンをクリックした場合は、このアプリの作業画面がキャプチャされます。

「キャプチャ」ボタンをクリックするか、アプリを最小化して Shift + Ctrl + C する度に、新しいキャプチャデータが作られます。新しくつくられたキャプチャデータ(画像)は、アプリの右側にサムネイル表示されます。このサムネイル表示を左クリックすると、そのキャプチャデータがアクティブになり、メイン画面に表示されます。右クリックすると削除の可否を問うメッセージが表示されます。キャプチャしたデータを保存していない場合は、削除されたデータを復活させることはできません。

このアプリは本格的な画像編集に使用するための素材、もしくは、操作方法の解説を作成するために必要な情報画像(部分的な切り抜き画像)を簡単に作成したいという目的を実現するために開発しました。ですので「現在、表示されている画面の全部、もしくは一部を、必要であればマウスカーソルの形状を含めた画像データとして取得する」ことしかできません。保存した画像データを再度読み込んで表示したり、キャプチャした画像を加工する(例えば、ぼかす・モザイクをかけるといったような)機能はありません。ただし、画像の指定範囲を「ぼかす・モザイクをかける」機能は、後日、追加できたら、追加したいと考えています。

また、範囲を指定してキャプチャする機能はありませんが、アプリ側でキャプチャした画像上に矩形を描画し、この矩形で囲んだ範囲を任意の名称を付けて保存したり、クリップボードへ送信することができます(方法は後述)。※ このアプリで、本当に実現したかった機能は、この機能です!

キャプチャした画像上に描く矩形の枠線の太さと色は変更可能です。アプリは終了時の設定内容を自動的に記憶しますので、次回起動時は前回終了時の設定を復元して起動します。矩形で囲った範囲内部を右クリックすると表示されるサブメニューから、その範囲のみを「名前を付けて保存」したり、「クリップボードへ送信」したりすることができますが、その際、保存したり、送信されるのは、矩形下の指定した範囲のみです。矩形そのものは保存・送信されません。

範囲を指定する際に描かれる矩形の枠線の太さと色は変更可能です。


何らかの理由で矩形も含めて保存・送信したい場合は、「矩形が表示された状態そのものをキャプチャ」して、その矩形全体を囲むように範囲を指定(範囲選択後も、矩形の上下左右と四隅に表示されるグラブハンドルをドラッグして選択範囲の微調整が可能です)し、保存・送信を行ってください。

なお、アプリの仕様として、単に画面をキャプチャしただけでは画像データとして保存されません。画像データとして利用したい場合は、必ず保存の作業を行ってください。保存していないキャプチャデータはアプリ終了時に自動的に破棄されますので、この点には十分ご注意ください。

キャプチャした画面を画像データとして保存する方法は3つあります。

(1)ショートカットキー( Shift + Ctrl + S )で保存。
(2)アプリ本体の「Save」ボタンをクリックして保存。
(3)アプリ本体で範囲を指定して保存。

(1)ショートカットキー( Shift + Ctrl + S )で保存

まず、(1)のショートカットキーによる保存は、最後に Shift + Ctrl + C を行ったデータに対して有効です。画像データの保存先 Path は、デフォルト設定はデスクトップですが、アプリを起動後、ユーザーが保存先を任意の場所に変更した場合は、ユーザーが選んだ任意の場所となります(アプリは終了時にデータの保存先 Path を記録して終了、次回起動時はその Path の存在を確認し、Path が存在すればそこを、存在しなければユーザーが新しく指定した任意のフォルダ(もしくはデスクトップ)を画像データの保存先として自動選択します)。

ショートカットキー( Shift + Ctrl + S )で保存した場合は、SaveDialog は表示されません。ショートカットキー押し下げと同時に上記指定フォルダに保存後、次のメッセージが表示されます。

画像ファイルの名称は「Screenshot_西暦年月日_時分秒」+「.拡張子」となります。
(SaveDialog 使用時は、任意の名称に変更可能です)

画像データの保存形式は、デフォルト「PNG」形式です。アプリ本体側で 予め指定 すれば「BMP」や「JPEG」形式で保存することもできます。先ほどの Path と同様、アプリは終了時に指定されていた画像ファイルの保存形式を記憶しますので、次回は前回終了時の保存形式が自動的に選択された状態で起動します。

(2)アプリ本体の「Save」ボタンをクリックして保存

2つ目の保存方法が、アプリ本体の「Save」ボタンをクリックして保存する方法です。初めて起動した際には、保存形式はデフォルトで PNG 形式になっていると思いますが、PNG 形式の他に BMP や JPEG でも保存可能です。ですので、アプリの「Save」ボタンを利用する保存の流れとしては、タスクバーのアプリをクリック → 画像の保存形式を指定 → 画像の保存( Save ボタンをクリック)が基本です。

キャプチャした画像の保存形式を指定した後、「Save」ボタンをクリックしてください。

このアプリでキャプチャした画像です!


即時保存のショートカットキー( Shift + Ctrl + S )を使わず、タスクバーに待機していたアプリのアイコンをクリックしてプログラムの本体を表示し、画面右下の「Save」ボタンをクリックする(2)の場合は、(1)の場合とは異なり、「名前を付けて保存」のダイアログが表示されます。必要に応じて保存場所・ファイル名を変更し、「保存」ボタンをクリックすれば、キャプチャした画像をそのまま、任意に指定したディレクトリ(フォルダ)に保存できます。

キャプチャした画像ファイルの保存先とファイル名は Windows 側の機能を利用して変更が可能です。
必要に応じてダイアログを操作し、「任意の場所・任意の名称」に変更してください。

(3)アプリ本体で範囲を指定して保存

キャプチャした画像の一部を範囲指定して保存する方法です。例えば、操作方法の案内で「最小化ボタンをクリックしてください」という趣旨を解説する際に使用したい画像を作成するには、次のように操作します。

まず、マウスのカーソルを最小化ボタンに乗せて(最小化ボタンをポイントして)、画面全体をキャプチャ( Shift + Ctrl + C )して、必要な範囲を矩形で範囲指定します。範囲指定は、必ず、指定したい範囲の左上から右下へドラッグする形で行ってください(右下から左上へドラッグする操作は無効となります)。

解説に使用したい範囲を矩形で囲みます。


矩形で囲んだ範囲の内部へマウスのカーソルを移動させる(矩形内部をポイントする)と、マウスのカーソルの形状が上下・左右の矢印( SizeAll ・「全方向サイズ変更カーソル」or「移動カーソル」に変化します(この状態で範囲そのものを移動させることもできますが、移動はあまりスムースではありません)。この状態でマウスを右クリックするとサブメニューが表示されます。

機能を追加して、上記リンク先で公開している Capity Plus では、ショートカットキー( Shift + Ctrl + 矢印キー)で選択範囲の移動が可能です。ちなみに選択範囲の微調整は Shift + 矢印キーに割り当ててあります。自分で使ってみた感想ですが、1ピクセル単位での制御が可能ですので、とても便利に使えています!

実行したいコマンドをクリックしてください。


指定範囲をそのまま無加工で使用する場合は「名前を付けて保存」、画像編集ソフトでさらに加工して使いたい場合は「クリップボードへ送る」を選んでください。

この解説そのものも、このアプリで作成しましたが、キャプチャした画像をさらにキャプチャするので、なんだか、すごく混乱しました。解説の解説を作るって、難しいです・・・

【終了方法】

アプリを終了するには、操作画面右下隅にある「Close」or「終了」ボタンをクリックしてください。確認メッセージが表示されます。

「はい」をクリックすると終了します。

3.☆ここではまりました☆

「こんなのカンタンさぁ・すぐ出来るー」みたいな軽いノリで始めたこのアプリの制作でしたが、思わぬ落とし穴にはまり、3日間ほど停滞しました。それは何かというと、範囲を指定する矩形を描画した後の、矩形のリサイズ時の挙動の制御です。

矩形の新規描画の際には、キャプチャした画像上の任意の位置をドラッグするわけですが、この時、ドラッグした範囲が画像の右や下、または右下隅までくると、次の画像のようにドラッグそのものが自動的に停止します。

新規に矩形を描画する際は、画像の端までドラッグしたら、もうそれ以上ドラッグできません。


ところが、この範囲の選択後、矩形の上下左右と四隅に表示されるグラブハンドルをクリックして(掴んで)リサイズしようとすると、選択範囲(上の画像の赤い点線部分:業界用語では「ラバーバンド」と言うようです)が画像の幅・高さを超えて右へ・下へ、大きくずれて描画されてしまうのです。

範囲選択後、矩形をリサイズしようとするとキャプチャした画像の範囲をオーバーしてしまう・・・


上の例はわかりやすさのため、画面の右下隅で新規矩形描画時にカーソルが自動停止した後、さらに右下側へリサイズするという、現実にはあり得ない設定で説明しましたが、実際の場面では、キャプチャした画像の右下方向を範囲選択して切り抜きたいとき、この現象が発生するわけです。

この選択範囲が画像の端をはるかにオーバーした状態で「名前を付けて保存」・「クリップボードへ送信」しても、保存・送信されるのは余計な余白のない(取得したかった)画像データのみなので、『問題がない』と言えば『問題ない』のですが、気分がよくないです。

プログラム的には、キャプチャした画像は imgPreview という名前の TImage に表示し、さらにその上に plImage1という名前の TImage を乗せ、これをラバーバンドとして利用しています。ですので、手続きは、imgPreview の MouseDown 、MouseMove 、MouseUp と、plImage1 の MouseDown 、MouseMove 、MouseUp があるわけです。

plImage1 は、Mr.XRAY さんが公開していらっしゃる plResizeImage.pas を利用させていただき、作成したものです。改変可能とのことでしたので、元の plResizeImage.pas に必要な機能を追加して使わせていただきました。素晴らしいプログラムを公開してくださっている Mr.XRAY さんに心より厚く御礼申し上げます。ほんとうに、ありがとうございました。

157_移動リサイズ可能な TImage ラバーバンドとグラブハンドル

さて、どうやってこの問題を解決したか? ですが・・・

私は、当初、imgPreview 上での動作なので、imgPreview の MouseDown 、MouseMove 、MouseUp の各手続きに必要な制御を記述すればイイとハナから思い込み(しかも、その思い込みが原因で、これまでにもさんざん苦労して、痛い思いを味わい続けてきたことすら完全に忘れ)、手を変え、品を変え、コードを変えて、imgPreview の MouseDown 、MouseMove 、MouseUp の各手続きに、ドラッグ操作が imgPreview の右下隅で自動停止するコードを書き続けたわけですが、どんなにコードを尽くしても、上の画像で示した画像の境界を超えてラバーバンドが描画される現象を改善することが出来ず、つまり、ラバーバンドは画像の端の先々まで伸び続けるという、またまた、この「他に誰一人として悩まないこと」で、さんざんに悩み、まるまる2日間をこの問題の解決に費やしました。今思えば、見方によっては、それは途方もなく無駄で無意味な時間であったわけですが・・・。この悩んでいる状態を経験したくてプログラムを書いているように思えてならないフシもあり、そうなるともう、これは救いようがありません・・・

ともあれ、七転八倒し続けた挙句、(オレは根本的に間違えているのではなかろうか?)と、ようやくそこに思いが至りました。時にして遅すぎですが、これが悩み続けて3日目のことです。

で、気づいた根本的な間違いとはナニか? というと、確実に呼ばれていると思い込んでいた上記イベントは実は呼ばれていないんじゃないか? という至極(「非常に」・「とても」の上位語として、強調の意味を持ちます)当然のことなのですが、3日めにしてようやくというか、やっとそこに思いが至ったわけです。

一緒に暮らしているヒトに『悔い改めましたか?』とよく問われる私ですが、どんなに悔い改めても同じような誤りをひたすら繰り返すのは、懲りてないと言うか、何と言うか、やっぱり、バカなんじゃないかと・・・、今に始まったことではありませんが、そう思えてなりません。

神さま
ボクには信じることと
思い込むことの違いが
よくわかりません。

信じるものは救われると
聞きましたが、
思い込むものは
救われないのでしょうか?

父からは、いつか・・・
『大丈夫。天国の黒板にはあなたの名前が書いてある。』と

ものすごいことを聞きましたが、
あっ。いえ、その・・・
決して「ついでに」という訳ではありませんが、
それも ほんとう でしょうか?

そのように悩みつつ、書いたコードがコレです。

  {$IFDEF DEBUG}
  OutputDebugString('imgPreviewMouseDown called');
  {$ENDIF}

実際の画面上では、こう見えます。(この文字のうすさが、なんかステキで・・・ ♪)


本来ならば、コレをいちばん最初に確認すべきだったのですが、後悔と反省の狭間で目を瞬きながら「いつ・どのイベントが呼ばれているのか」確かめて見ると・・・ 矩形のリサイズ時に呼ばれているのは、


当たり前ですが、(本人は、そのような手続きを書いたことすら忘れていた )plImageResized 手続きでありました!

しばし、唖然としましたが、わかってしまえばコトはカンタンです。

procedure TForm1.plImage1Resized(Sender: TObject; ARect: TRect; ALeft, ATop,
  AWidth, AHeight: Integer);
var
  ・・・ 省略 ・・・
  clipRect: TRect;
  topLeft, bottomRight: TPoint;
begin

  {$IFDEF DEBUG}
  OutputDebugString('plImageResized called');
  {$ENDIF}

  ・・・ 省略 ・・・

  //マウスのカーソルの移動範囲を制限

  //imgPreview のクライアント領域をスクリーン座標に変換してカーソルの移動を制限
  clipRect := imgPreview.ClientRect;
  topLeft := imgPreview.ClientToScreen(Point(clipRect.Left, clipRect.Top));
  bottomRight := imgPreview.ClientToScreen(Point(clipRect.Right, clipRect.Bottom));

  clipRect.Left := topLeft.X;
  clipRect.Top := topLeft.Y;
  clipRect.Right := bottomRight.X;
  clipRect.Bottom := bottomRight.Y;

  // カーソル移動範囲を制限
  ClipCursor(@clipRect);

end;

で、このままだと、マウスのカーソルは imgPreview の外へ出れなくなってしまうので、plImage1 の MouseUp 手続きで ClipCursor に nil を代入してカーソルの移動制限を解除します。

procedure TForm1.plImage1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  screenPt: TPoint;
  localPt: TPoint;
begin

  //フラグをリセット
  DragFlag := False;
  MoveFlag := False;

  //ドロップ後の plImage1 のスクリーン座標を取得
  screenPt := plImage1.ClientToScreen(Point(0, 0));

  //imgPreview のローカル座標に変換
  localPt := imgPreview.ScreenToClient(screenPt);

  //キャプチャ範囲を更新
  rbX := localPt.X;
  rbY := localPt.Y;
  rbW := plImage1.Width;
  rbH := plImage1.Height;

  //カーソルの移動制限を解除
  ClipCursor(nil);

end;

これでリサイズ時でも、キャプチャした画像の端でカーソルが停止するようになったはずです。そしてこれは思い込みではないはずです。

結果を信じながら、キャプチャした画像上を範囲選択します。

範囲選択後、いったん指をマウスから離し、さらにリサイズします。


神さまに祈りながら、グラブハンドルをクリックして(掴んで)右下隅の方へドラッグし(引っ張り)ます。はたして結果は如何に・・・

☆キャプチャ画像の右下隅でドラッグは自動停止しました☆

やった! やった!!

これでようやく
夢が全部叶いました!

4.ダウンロードはこちらから

今回の記事で紹介した PC の画面キャプチャを実行するプログラム一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt をご覧ください。

【注意!】
ここで公開しているのは、私的に必要であった最小限の機能のみを搭載した初期バージョンです。
(新機能を追加し、不具合を改善した新しいバージョンがこちらにあります。下記リンクからダウンロードできる旧バージョンは、新しいバージョンに向かってソフトウェアがどのように進化したか、確認するためのものとお考えください。万一、試用される場合は、新しいバージョンで改善した不具合を含んだままのプログラムであることに十分ご注意の上、自己責任の下でお試しくださいますよう、お願い申し上げます)

なお、プログラムの初回起動時には、Windows Defender SmartScreen による警告画面が表示されます。この警告画面に関する詳細は、当 Blog の次の過去記事をご参照ください。

【追記_20251109】

新しいバージョンのプログラムの修正に合わせ、初期バージョンのサムネイル表示部分の不具合も修正しました。

5.お願いとお断り

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

解答欄(矩形)検出器を改良しました!

AnswerAreaLocator 実行時の画面


この Blog の過去記事で紹介している手書き答案の採点補助プログラム AC_Reader では、スキャナーでスキャンして Jpeg 形式で保存した試験の解答用紙画像から、解答欄の座標を取得するプログラムを外部的に呼び出して利用しています。

この解答欄の座標を取得するプログラムは、OpenCV の矩形検出機能を使って、その目的を実現しているのですが(掲載しておいてこんなことを言うのもナンですが)、必ずしも意図した通りに動かない場合がありました。

今回、その「いつか直そうと思っていた部分を手直し」して、前よりは少しは意図した通りに動くかな? みたいなプログラムが出来た気がするので、「デジタル採点 All in One」なる大それた名前を付けて世に出してしまったプログラム集のバージョンアップ版として公開させていただきます。

プログラムの名前も、よりわかりやすいものに変更( AnswerAreaLocator.exe )しました(が、単体での使用は事故防止のため非推奨です)。あくまでも AC_Reader.exe から呼び出しての動作が基本ですが、たぶん、以前のモノより、期待通りに動作するものと思われます。万一にでも、バージョンアップしてくれないかなーと思われていた方が「もし、いてくださったら」のお話ではありますが・・・

今回の記事では、そのバージョンアップ内容(正しくは不具合のお詫びとその修正内容)をご紹介させていただきます。

【もくじ】

1.GUIが使いやすくなりました!
2.画像の傾きに強くなりました!
3.ほぼ採点する順番に解答欄を検出できるようになりました!
4.マウスのアイコンがデフォルト状態に戻るようになりました!
5.必要なフォルダがない場合には警告を表示するようになりました!
6.常に最大化して実行する設定にしてやっぱりやめました!
7.最大化から非最大化した際に画面中央にフォームを表示します!
8.ダウンロードのご案内
9.まとめ
10.お願いとお断り

1.GUIが使いやすくなりました!

以前のユーザーインターフェイスは、次のようなものでした。

「画像の傾きを補正する機能を重視」した結果、操作方法がわかりにくい設計となっておりました。


新しいプログラムのユーザーインターフェイスです。基本的に、左から右へ操作していただければ作業がスムースに進むように改良しました。

画像の傾きの補正機能は残しましたが、使用しなくても動作するように修正しました。


スキャナーでスキャンした画像のすべてが、目視状態で明らかに傾いて(左右いずれかの方向に回転して)いる場合がありますので、画像の回転を行って、傾きを補正する機能は残しましたが、機能の実装方法そのものを見直し、負の数で左へ回転/正の数で右へ回転、Prev ボタンで効果を確認、UnDo ボタンでやり直し、実行ボタンで全画像に修正を適用というように簡略化しました。

また、矩形検出「する/しない」の境界を決める閾値も、以前は面積を利用するようにしていましたが、新しいプログラムでは、検出限界とする幅もしくは高さをピクセル単位で指定できるように変更しました。使っていただければ、お分かりいただけると思うのですが、例えば以下のような場合、自動的に小さな矩形を最初から無視しますので、より解答欄の矩形だけを検出する方向に進化できたのではないかと思います。

矩形検出を実行した段階で設問番号の「1」を囲む矩形は検出対象から自動的に外されます。


また、これは以前と同じですが、「6文字で答えよ」と文字数を指定して解答させたい場合は、検出したい矩形の外枠を実線で、内部を点線として予め解答欄を作成(描画)しておくことで、後から手動で範囲指定をやり直さなくとも、取得したい解答欄そのものを自動的に取得できます。

点線は、目視で線がつながっていないことが確認できれば大丈夫(検出されません!)。


ちなみに、点線は、誰もが使っているであろう「あのソフトウェア」で描いたものです。上の図の点線は、下の赤枠内の点線を利用して描画しました。

ちなみに実線は、赤枠の4つ下の実線です。


それから、間違っていたらごめんなさい。赤枠の1つ上の細かな(?)点線は、要注意の線です。私の見間違いかもしれませんが、以前、この線を利用する中で「とても不思議な現象に出会った」ことがあり、それ以来、この線は簡易的な利用にとどめ、本格的な何か(色を変えたりみたいな)には利用しないようにしています。詳しくは書きませんが、いろいろワケありの線のようです・・・

また、GUI で、ブロックと表現している部分の考え方ですが、これは採点する順番に解答欄座標を並べることができるように、解答用紙を幾つかのブロックに分けて、そのブロック内で横書き答案であれば「左 → 右」かつ「上 → 下」へ、国語で使われる縦書き答案であれば「上 → 下」かつ「右 → 左」へ、解答欄の座標を検出します。

上のような場合には、「2」ブロックを指定します。

2.画像の傾きに強くなりました!

以前のバージョンで、最も対応困難であったのが「スキャンした画像の傾き」です。

以前の勤務先で使用していた複合機では、気になるほどスキャンした画像が傾くことなどなかったのですが、今の勤務先で使用している複合機のスキャナーは(同じメーカーさんの同じ型番の製品ですが)、スキャンすると画像がことごとく右肩上がりになるこの固体特有のクセがあり、サービスマンの方に修正を依頼しても「これはちょっと難しいですね・・・」と断られてしまった経緯もあって、自分では紙送りローラーのクリーニングくらいしかできませんので、たいへん困っておりました。

もちろん、傾きと言っても、わずか 0.1° 程度の傾きですので、私以外に誰一人、問題にする人なんていませんが・・・

私はものすごく気になるのです!

なぜか? というと・・・ 私の神経が細やかとか、そんな問題ではなく、どちらかと言えば、私は神経が少し足りないんじゃないかと思うことの方が多いくらいです・・・。その証拠に『点くのが遅い蛍光灯のようなお子さんですね!』と小学校時代、担任の先生から言われたと母親が語っておりましたし、私はその時、多分言葉の意味そのものが理解できず、おそらく褒められたに違いないと勘違いして、むしろ、喜んでいたのではないか? とも思います。ヽ(=´▽`=)ノ

とにかく、これまでの矩形検出プログラムで解答欄矩形の座標を検出して、採点する順番になるように並び替える際、横書き答案であれば「 Y 座標の値が小さいものから順に、左から右へ並べ替える」アルゴリズムを採用しているため、解答用紙の画像が左へ傾いていると、座標原点 0,0 が左上であるため、右側の解答欄ほど Y 座標の値が小さくなり、検出した座標を並び替える際に「上から下へ」の順番はなんとか守れても、「左から右へ」が「右から左へ」と、「一部の解答欄座標の並びが逆転」してしまうわけです。

この修正が大変な手間で・・・

( AC_Reader を使ってくださる方のお手伝いをする際に、いつも、そう感じ・・・ )

ほんとうに、申し訳なく・・・

私自身の心情など、この際、極めて、どうだっていいコト・・・ では、ありますが・・・

私といっしょに暮らしているヒトは、とてもやさしくて、かわいい、イイひとなのですが、極々稀に、ブチ切れると、ながーい間、沈黙した挙句・・・ 私は、完全に悪くないと思えてならないときでも・・・

おまえが、わるい。

必ず、そう言います。

控えめに「そうなの?」と尋ねることにしているのですが、返事は決まって

だって、そうじゃん!

この言葉を聞いた時の心境が、まさに、この場合のそれで・・・。みなさんに、どうにかして、ご理解いただきたい私自身の偽りのない心情なのです・・・

ほんとうに、良かれと信じて、精一杯、その時の自分にある、全身全霊の、すべての力を使って書いた・・・と、そう信じて疑わないプログラム。・・・なのですが・・・

それは・・・悪気なんて全然なく、きみのために良かれと思ってやったこと・・・

でも、その プログラム には、不具合があった・・・。

でも、きみは、なぜか、怒っている・・・。

悪いのは ・・・。

そう、ほんとうに、精一杯、がんばって、「書いたのは間違いない」んだけど・・・ 。

そう、ほんとうに、きみとケ〇カなんて、したくなかった・・・。

だけど・・・ だから・・・

間違ったのは僕なんです。
みんな僕が悪いんです。

一緒に暮らしているヒトと、ケ〇カするといつも、そんな気持ちになります。

で、そんな時、「今日の晩御飯、なぁに?」って彼女に尋ねると・・・

へびとカエル

彼女は、必ずそう答えるのです・・・ 実際に、それが出てきたことは幸いにしてありませんが。

そのように悪いのは私だと理解していますので、以前のプログラムでは、解答欄座標を検出する前に、全画像を傾きがなくなる方向に回転させて、傾きを修正してから解答欄矩形の座標を検出するように手順を工夫していたのです。それは、それで「ない」知恵を絞って考えた自分的には限界とも思える方法だったのですが、この修正を行っても 100 %スムースに作業できるとは到底思えず、(あのプログラム、検出して並び変えた結果の一部は必ず修正が必要な状態なのではなかろうか・・・ある程度はちゃんと動くと思うんだけど・・・、ちょっとでも傾きがあると・・・うーん、困ったぁ)と、思い出す度に同じ思いが込み上げてきて、日々、後悔と、反省と、絶大なる心配とを、交互に繰り返しておりました。

いつか、なんとかしなければ・・・

そう思いながらも、よい方法が思いつかず、更新が先延ばしになってしまいました。お使いいただけた皆さまに、伏してお詫び申し上げます。ほんとうに、すみませんでした。

今回の更新では、正直に言いますと当初、得られた解答欄矩形の座標から、水平方向の直線を複数本検出して、その傾きの平均値を計算し、全自動で画像の傾きを修正する方向でプログラムの修正作業を進めたのです・・・が、残念ながら、現在の私の力では、自分自身が満足できる結果を出すことは出来ませんでした。

そこで、全自動での修正を断念し、画像の傾きを補正する部分と解答欄矩形を検出する部分、両方のアルゴリズムを(自分の力の及ぶ限りの範囲ではありますが)全面的に見直すことにしました。

そこで思いついたのが、横並びの解答欄を「行」のように見なし、Y 方向に「多少のマージン」を設定することで、検出した解答欄座標を理想通りに並べ替えて表示できるのではないかということです。さらに、これが出来れば、解答用紙画像の多少の傾きなど問題ではなくなるはずです。この考えを基にしてスクリプトを書き替えること2度、3度、ようやく思った通りに解答欄矩形の座標を並べ替えて出力できるようになりました。少なくても、私のテストした範囲では、採点する順番で解答欄座標の並び替え出力に成功するようになった・・・と思えるプログラムに改良することが出来ました☆

もちろん、国語の試験で利用される縦書き答案についても、縦並びの解答欄を「列」のように見なし、やはり「多少のマージン」を設定することで、画像が少しくらい傾いていても基本「右から左」かつ「上から下」へという順番で検出した解答欄座標を並べ替えて表示できるように、こちらもプログラムを修正できました。

以下は、Delphi に埋め込んで使用している「横書き答案の解答欄座標を検出して、採点順に並べ替える」 Python Script です(ダウンロードしていただいた Zip ファイルのサイズが大きいのも、展開に時間がかかるのも、Python 用の OpenCV をバックグラウンドで動作させているためです)。

  import cv2
  import numpy as np

  def imread_unicode(path):
      with open(path, "rb") as f:
          data = f.read()
      img_array = np.frombuffer(data, np.uint8)
      return cv2.imdecode(img_array, cv2.IMREAD_COLOR)

  def deskew_image(gray):
      edges = cv2.Canny(gray, 50, 150, apertureSize=3)
      lines = cv2.HoughLines(edges, 1, np.pi / 180, 150)
      if lines is None:
          return gray
      horizontal_angles = []
      for rho, theta in lines[:, 0]:
          angle_deg = (theta * 180 / np.pi)
          if (angle_deg < 10) or (angle_deg > 170):
              adjusted_angle = angle_deg if angle_deg < 90 else angle_deg - 180
              horizontal_angles.append(adjusted_angle)
      if len(horizontal_angles) < 5:
          return gray
      mean_angle = np.mean(horizontal_angles)
      if abs(mean_angle) < 0.3:
          return gray
      (h, w) = gray.shape
      center = (w // 2, h // 2)
      M = cv2.getRotationMatrix2D(center, mean_angle, 1.0)
      rotated = cv2.warpAffine(gray, M, (w, h), flags=cv2.INTER_LINEAR, borderValue=255)
      return rotated

  def detect_inner_boxes(image_path):
      img_color = imread_unicode(image_path)
      if img_color is None:
          raise FileNotFoundError(f"画像が見つかりません: {image_path}")
      img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
      thresh = cv2.adaptiveThreshold(
          img_gray, 255,
          cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
          cv2.THRESH_BINARY_INV,
          15, 10
      )
      contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
      boxes = []
      for cnt in contours:
          x, y, w, h = cv2.boundingRect(cnt)
          if w > ' + cmbThreshold.Text + ' and h > ' + cmbThreshold.Text + ':
              boxes.append((x, y, w, h))
      inner_boxes = []
      for i, box in enumerate(boxes):
          x1, y1, w1, h1 = box
          rect1 = (x1, y1, x1 + w1, y1 + h1)
          contains_other = False
          for j, other in enumerate(boxes):
              if i == j:
                  continue
              x2, y2, w2, h2 = other
              rect2 = (x2, y2, x2 + w2, y2 + h2)
              if rect1[0] <= rect2[0] and rect1[1] <= rect2[1] and rect1[2] >= rect2[2] and rect1[3] >= rect2[3]:
                  contains_other = True
                  break
          if not contains_other:
              inner_boxes.append(box)

      if not inner_boxes:
          return []
      y_tolerance = max(5, int(np.median([h for (_,_,_,h) in inner_boxes]) * 0.5))
      inner_boxes.sort(key=lambda b: b[1])
      sorted_boxes = []
      current_row = []
      current_y = None
      for b in inner_boxes:
          x, y, w, h = b
          if current_y is None:
              current_y = y
              current_row.append(b)
          elif abs(y - current_y) <= y_tolerance:
              current_row.append(b)
          else:
              current_row.sort(key=lambda b: b[0])
              sorted_boxes.extend(current_row)
              current_row = [b]
              current_y = y
      if current_row:
          current_row.sort(key=lambda b: b[0])
          sorted_boxes.extend(current_row)
      inner_boxes = sorted_boxes
    
      for idx, (x, y, w, h) in enumerate(inner_boxes, start=1):
          var1.Value = str(x) + "," + str(y) + "," + str(x + w) + "," + str(y + h)
      return inner_boxes

  if __name__ == "__main__":
      image_path = r"' + 'CutImage0' + IntToStr(i) + '.jpg' + '"
      boxes = detect_inner_boxes(image_path)

横書き答案で、ブロックの指定が2以上である場合がありますので、この処理を for ループの中に埋め込んでいます。また、この横書きでブロックの指定が2以上である答案の場合には、2ブロック目に検出した座標の値のx座標を一律補正するような処理も Delphi 側で必要ですが、核心部分はなんと言っても、上のスクリプトです。思えば、ここに至るまで、はや幾年月・・・

横書き用が出来てしまえば、あとはそれを縦書き用に書き換えるだけです。「縦書き答案用のスクリプト」は次の通りです。

  import cv2
  import numpy as np

  def imread_unicode(path):
      with open(path, "rb") as f:
          data = f.read()
      img_array = np.frombuffer(data, np.uint8)
      return cv2.imdecode(img_array, cv2.IMREAD_COLOR)

  def detect_inner_boxes(image_path):
      img_color = imread_unicode(image_path)
      if img_color is None:
          raise FileNotFoundError(f"画像が見つかりません: {image_path}")
      img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
      thresh = cv2.adaptiveThreshold(
          img_gray, 255,
          cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
          cv2.THRESH_BINARY_INV,
          15, 10
      )

      contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

      boxes = []
      for cnt in contours:
          x, y, w, h = cv2.boundingRect(cnt)
          if w > ' + cmbThreshold.Text + ' and h > ' + cmbThreshold.Text + ':
              boxes.append((x, y, w, h))

      inner_boxes = []
      for i, box in enumerate(boxes):
          x1, y1, w1, h1 = box
          rect1 = (x1, y1, x1 + w1, y1 + h1)
          contains_other = False
          for j, other in enumerate(boxes):
              if i == j:
                  continue
              x2, y2, w2, h2 = other
              rect2 = (x2, y2, x2 + w2, y2 + h2)
              if rect1[0] <= rect2[0] and rect1[1] <= rect2[1] and rect1[2] >= rect2[2] and rect1[3] >= rect2[3]:
                  contains_other = True
                  break
          if not contains_other:
              inner_boxes.append(box)

      if not inner_boxes:
          return []
      x_tolerance = max(5, int(np.median([w for (_,_,w,_) in inner_boxes]) * 0.5))
      inner_boxes.sort(key=lambda b: b[0], reverse=True)
      sorted_boxes = []
      current_col = []
      current_x = None
      for b in inner_boxes:
          x, y, w, h = b
          if current_x is None:
              current_x = x
              current_col.append(b)
          elif abs(x - current_x) <= x_tolerance:
              current_col.append(b)
          else:
              current_col.sort(key=lambda b: b[1])
              sorted_boxes.extend(current_col)
              current_col = [b]
              current_x = x
      if current_col:
          current_col.sort(key=lambda b: b[1])
          sorted_boxes.extend(current_col)
      inner_boxes = sorted_boxes

      for (x, y, w, h) in inner_boxes:
          var1.Value = str(x) + "," + str(y) + "," + str(x + w) + "," + str(y + h)

      return inner_boxes

  if __name__ == "__main__":
      image_path = r"CutImage01.jpg"
      detect_inner_boxes(image_path)

こちらについては「横書き答案」とは異なり、私が想定した範囲では「現状」ブロックに分割しての処理の必要性が感じられませんでしたので、Loop での処理は考慮しておりません。

3.ほぼ採点する順番に解答欄を検出できるようになりました!

極端な例として(いくらなんでも、これはないと思いますが)-1.00° ほど故意に画像を傾けて実験してみました。

「回転」に負の数を指定すると画像は左へ回転します。


左へ -1.00° 故意に回転させた画像に対して、解答欄座標の検出を行ってみた結果です。画像がどういう状態であろうと、傾きがあろうと、なかろうと、それに関係なく、プログラムが解答欄矩形の座標を左から右へ、そして上から下へ認識してくれたなら、夢はほんとうになります。

この夢は・・・

他の誰かが、既に実現した夢でもかまいません。私にとっては、まだ、成し遂げていない夢ですから。たとえ、だれひとり、この夢の成就を待っていてくれる人など、いなくても・・・

人の夢と書いて、「儚い」と読むそうですが、これほど、私の思いに重なる言葉はありません・・・。

僕の書いた・・・ プログラムは、僕の夢の結晶。

だから・・・ 人の夢の結晶が、たとえ「儚い」ものであっても・・・

うん。「儚い」ものでしか、なくても・・・

そこに込めた様々な願いと祈りは・・・

僕にとっての「ほんとう」です。

だから僕は、心から、それを いとしく おもいます。

プログラムは修正なしで、「解答欄 エ」よりY座標が大きい「解答欄 ア」を最初の座標に選んでいます。
設問番号「1」部分の矩形は、閾値の設定により検出対象外となっています。
この場合の座標原点は、左上が(0,0)です。


これだけの傾きがあっても、今回修正したプログラムは、ようやく長い間この胸に思い描き続けた夢の通りに動いてくれるようになりました。今回、全自動での補正(修正)処理は実現できませんでしたが、自分的には、この結果から見て・・・おそらく、今後、手動での傾きの補正処理はほぼ不要になるのではないか? と考えます。この実験結果より、「My 解答欄矩形の検出プログラムは、これまで内在していた不具合を一掃できるレベルに到達できた」と判断していいかも・・・と、ようやく思えた次第です。

ものすごく、遠いむかしに、断層を解析し、それを形成した応力場を描くプログラムを書いたことがあります。その際に非常に苦しんだのが PC の座標設定と、中学・高校以来慣れ親しんだ数学的な座標設定の相違でした。

当初、私は「座標原点は数学で学んだのと同じ X 軸と Y 軸の交点の位置にある」というように思い込んで、先人の書いたコードを読んでいましたので・・・

( 座標原点は、いったい、どこなんだ? )

と、大混乱。ようやく「座標原点は左上にある」と理解してからも、なお・・・

( 原点を移動して、解析図を描画するためには、ナニを、どう修正すればいい? )

あの時、大いに悩んだ経験が今回大いに役立ちました☆

ただ・・・、余弦定理の力を初めて知って、私に魂が震えるような感動の経験を与えてくれた・・・

あの断層解析プログラムは、
まだ1度も使っていません☆

が。

まぁ、作るのが楽しかった ♪ から、全然、自分的には「いい」のでありますが・・・

今後、断層解析の科学論文、書くことも、あり得ないし・・・

今はただ・・・ 青春を「理科」に賭けた思い出だけが、懐かしい。

*(^_^)* ♪

4.マウスのアイコンがデフォルト状態に戻るようになりました!

「当たり前のことじゃないか? なにバカなことを言ってるんだ」

そう言われても仕方がないことなので、こちらについても心からお詫びするしかないのですが・・・

実は、これも前から気になっていたことなのですが・・・、これまでのプログラムでは解答欄矩形の座標を取得後、時々、マウスのカーソルの形状が「上下左右の四方を向いた矢印」になり、デフォルトの「左斜め上を向いた白い矢印」に戻らなくなってしまう現象が、時々発生しておりました。

もちろん、ずっと気にはしていたのですが、でも、「何とかしなきゃ」と思いながらも、気づけばこちらも放置したままになってしまいました。理由は2つあって、1つはカーソルの形状が変化するだけで機能的な部分には(実用上何も)問題が生じなかった(つまり、見た目だけの問題であると認識していた)こと、2つめはそもそも「どこをどうしたら直せるのか」それがよくわからなかった・・・というのが私の中での、ほんとうです。

こんな不出来なプログラムを、耐え難きを・・・堪えて、それでもお使い下さった皆さま、ほんとうに、ありがとうございます。この件につきましても、ここであらためて、こころからお詫び申し上げます。重ね重ねではありますが、誠に、誠に申し訳ありませんでした。

今回の見直しにあたって、ようやく本気で「このままではいけない!」と思い、まず、その原因を探るところから修正作業を始めることにしました。まず、「いつ・どこで・何をするとカーソルの形状が変化したまま、元に戻らなくなるのか」それを明らかにする必要があります。私は、問題を再現すべくプログラムを様々に操作してみました。なかなか思った通りに問題が再現できず、ちょっと時間がかかりましたが、ようやく(変な言い方ですが)思った通りに問題を再現することが出来るようになりました。明らかになった問題発生に至るまでの操作は、次の通りです。

解答欄矩形の座標を取得すると表示されるラバーバンドの中をポイントすると、マウスカーソルの形状が次のように変化します。

この上下左右の四方を向いた矢印から成るカーソルは、
「サイズ変更カーソル」(Resize Cursor)という名前のようです。


この状態で、下向きの矢印キーを押し下げると TMemo 内のカーソルが次の座標に移動し、それに合わせてラバーバンドの位置が次の解答欄矩形上に移動します。

TMemo にフォーカスがあり、カーソルは2行目に移動します。
プログラムは2行目の座標を読み取り、その位置に赤い矩形を表示します。


この時、困ったことが起きます。ラバーバンドの外に出たらデフォルト状態に戻るはずのマウスカーソルの形状変化が起きず、その形状は「サイズ変更カーソル状態のまま」になってしまいます。下図はその状態をハードコピーしたものです。

マウスカーソルがデフォルトの白い矢印に戻りません!


ただし、機能的には何の問題もなく、このままの状態でボタンクリック等、通常通りの操作が可能です。このことが、この問題への対応がここまで遅れた原因の1つとなりました。

通常通り操作できますが、あまり気分がよくありません。


ここで、マウスカーソルをもう一度ラバーバンド内に戻してあげると、マウスカーソルの形状はデフォルトの白い矢印に戻るのですが、いちいちそんな操作はやってられません。

ラバーバンド内の左側領域をポイントするとデフォルト状態に戻ります。


この後、マウスカーソルをラバーバンド内から再度外に出します。カーソルの形状はデフォルト状態のままですが、再度、ラバーバンド内にカーソルを戻すとその形状は「サイズ変更」状態に変化し、ラバーバンドの外へ出すとデフォルト状態に戻ります。つまり、カーソルを動かすのではなく、カーソルを固定したまま、ラバーバンドの方を動かすと問題が発生することがわかりました。

マウスカーソルの形状は、デフォルト状態に戻りました。


詳しい原因はまだわかりませんが、とにかく、問題の核心部分がマウスカーソルの形状の制御にあることは明らかですので、次にそれがどのように実現されているのか、確認してみることにしました。

ラバーバンドを表示する部分のプログラムは、Mr.XRAYさんの TplResizeImage クラス(コンポーネント)を使わせていただき、Pen の太さと色を私が追加で指定しています。

この TplResizeImage.pas を開いて、じっくり読んでみます。すると、マウスの形状の制御は FSelected という Bool 型の変数で行われていて、これが True のとき、形状がサイズ変更カーソル(crSizeAll)になり、False のとき、デフォルト(crDefault)になることがわかりました。以下、その制御部分の抜粋です。

  TplResizeImage = class(TImage)
  private
    FSelected        : Boolean;

  ・・・

      if FSelected then begin
        Screen.Cursor := crSizeAll;
      end else begin
        Screen.Cursor := crDefault;
      end;

で、次の手続きで、マウスがコントロールから離れたらカーソルの形状をデフォルトに戻す設定になっていることもわかりました。

//=============================================================================
//  TplResizeImageクラス
//  CM_MOUSELEAVEメッセージ処理
//  マウスがコントロールから離れたらカーソルの形をデフォルトに戻す
//=============================================================================
procedure TplResizeImage.CMMouseleave(var Message: TMessage);
begin
  inherited;
  if not FSelected then exit;
  Screen.Cursor := crDefault;
  FResizeState  := irsNone;
end;

これより「マウスがコントロールから離れた」ことが確認できないところから問題が起きているのではないかと、ようやく、問題の原因らしきものが見えてきました。

どうしたらいいか、ひたすら考えます。すると、コメント文の中に次の一文が・・・

  //SetBoundsを実行すると,Resizeメソッドが自動実行される
  SetBounds(ALeft, ATop, Width, Height);

で、その Resize 部分を読んでみると・・・

procedure TplResizeImage.Resize;
var
  ALeft        : Integer;
  ・・・
begin
  ・・・
  SetBounds(ALeft, ATop, AWidth, AHeight);
  ・・・
end;

解答欄矩形の幅や高さが変わった場合は、必ず Resize が呼ばれます。そこで、ここに保険のような感じで、マウスのカーソルを元に戻す処理を追加しました。

//=============================================================================
//  TplResizeImageクラス
//  TImageのResizeメソッド
//  リサイズが発生すると自動的に呼ばれる
//=============================================================================
procedure TplResizeImage.Resize;
var
  ALeft        : Integer;
  ・・・ 省略 ・・・
begin
  ・・・ 省略 ・・・

  //サイズ変更後も必ずカーソルを戻す
  Screen.Cursor := crDefault;

  inherited Resize;
end;

これで解答欄矩形の幅や高さが変わった場合には、Resize 手続きが呼ばれ、マウスカーソルの形状が必ずデフォルト状態に戻ります。ただ、問題は幅や高さが変わらない場合です。幅や高さが変わらない解答欄は実際たくさんありますから、ここは手抜きをせず絶対にきちんと対応しなくてはなりません。

ただ、上に示したように Resize 手続きの中で SetBounds しているので、Resize 手続きは座標を入れ替える度に必ず呼ばれるような気もするのですが、より確実な方法を設定しておきたいと思い、カーソルの移動に使用している矢印キーの OnKeyDown イベントが使えないかと考えました。

考えました・・・が ・・・、よくよくコードを見ると、

TplResizeImage = class(TImage)

KeyDown は TWinControl 由来のイベントですが、TplResizeImage は TImage( = TGraphicControl )で親が違います。結論だけ言えば、 TImage はフォーカスを受け取れません。したがって KeyDown イベントは書いても無駄です・・・

と、ここで・・・

それなら、逆に、Form の方で KeyDown イベントを拾えばいいのではないか? と、ようやく気づき、

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key in [VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN] then
    Screen.Cursor := crDefault;
end;

さらに、より確実に動作するよう OnKeyUp イベントにも同じ処理を記述します。

procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key in [VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN] then
    Screen.Cursor := crDefault;
end;

で、Form が他のコントロールより先にキーボードイベントを取得できるように FormCreate 手続きで、KeyPreview: = True を設定しておきます。

procedure TForm1.FormCreate(Sender: TObject);
begin
  KeyPreview:=True;
end;

これで完璧かと思いましたが、Application.OnMessage を使ってグローバルに押されたキーを監視し、矢印キーが押された場合にはマウスのカーソルをデフォルトに戻す処理も追加しておくことにしました。こちらは構造的な意味でも、保守性を高める意味でも Form のメンバーとして記述します。こうしておけば、何年か経って今日の作業内容を完全に忘れてしまった場合でも、Private 宣言部分を参照すれば、何を設定したのかがわかり、メンテナンスしやすいコードにすることができます。

私は、そのような意味から、手続きだけでなく関数も Form のメンバーとして記述するようにしています。むかしは何でもかんでも Form のメンバーにしていたのですが、この Blog を書くようになってから、他から呼び出す必要のない手続きや関数は、「ネストされた手続き(Nested Procedure)」 または 「ネストされた関数(Nested Function)」 として記述することも多くなりました。コードを読むのと、( Blog の記事用に)コピペするのが楽だからというのが、その主な理由です。

type
  TForm1 = class(TForm)
    ...
  private
    //Application.OnMessage を使ったグローバルキー監視
    procedure AppMessageHandler(var Msg: TMsg; var Handled: Boolean);
  end;

で、Shift + Ctrl + C で手続きを作成し、実装します。

procedure TForm1.AppMessageHandler(var Msg: TMsg; var Handled: Boolean);
begin
  case Msg.message of
    WM_KEYDOWN, WM_KEYUP:
      case Msg.wParam of
        VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
          Screen.Cursor := crDefault;
      end;
  end;
end;

最後に、FormCreate で登録しました。

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := AppMessageHandler;
end;

これで Form がアクティブ(フォーカスがある)な時も、非アクティブ(フォーカスがない)な時も、常にマウスのカーソルをリセットできるようになったはずです。

実行して確認しました!

ラバーバンドの内部をポイントして、カーソルを「サイズ変更型」に変えます。


下向きの矢印キーを押し下げます。ラバーバンドは次の解答欄へ移動します。マウスカーソルの位置はそのままですが、その形状は・・・

マウスカーソルはデフォルトの白い矢印に無事戻るようになりました☆


余談ですが、このマウスカーソルの形状も含めて画面のハードコピーを取るのはどうしたらいいものかと、今回、少し悩んでしまいました。Windows11の機能のみで行うなら、拡大鏡を固定(?)にして PrintScreen を実行すれば出来るみたいなことを AI が言ってましたが、せっかく Delphi があるんだし、ヒマもあったので、マウスカーソルの形状も含めて画面のハードコピーを取るプログラムを自分で書いてしまいました。後日、機会がありましたら、この Blog でご紹介したいと思います。

5.必要なフォルダがない場合には警告を表示するようになりました!

この解答欄矩形の座標検出プログラムは、ユーザー目線で見ると、ただ1枚の画像だけを扱うプログラムのように見えると思うのですが、実はそうではありません。

画像の傾きを補正して解答欄矩形の座標を取得する機能を追加した段階で、傾きの補正を行った場合には、すべての画像に対して傾き補正を行って上書き保存する処理がどうしても必要になり、採点作業に必要な全画像を処理できるように(1つ前のバージョンで)プログラムを修正しました。

詳しく説明すると、手書き答案の採点補助プログラム( AC_Reader )側では、採点前の真っ新な解答用紙画像と、採点データ(採点記号や得点等)を書き込んだ採点済み解答用紙画像の2種類の画像を使用していますので、傾き補正を行った場合は、両方の画像データを補正して上書き保存する必要が生じるわけです。

今回、全面的にプログラムの見直しを行ったわけですが、その中で、あろうことか、採点済み解答用紙画像を保存しておくフォルダ(フォルダ名: MarkedAnswerSheet )が必ず存在しているという前提でコードを書いていることが判明しました。

もちろん、AC_Reader 側で(正規の・・・というか、私が決めた流れで)画像変換を行って、AC_Reader からこの解答欄矩形の座標を検出するプログラムを呼び出して作業を行う場合は何の問題も生じませんが、単にスキャンした画像を1枚だけ保存した任意のフォルダを指定して、このプログラムを直接単体で実行した場合、採点済み解答用紙画像を保存するフォルダがそもそもありませんから、最初に行う解答用紙画像の選択段階で「確実にエラーが発生」します。

このエラー(というか、正しくは不幸な事故)を防止するために、修正する前のバージョンでも、このプログラムを単体で起動した場合にはパスワードの入力を求めるように設定して、事故を防止する方策としていたわけですが、今回の見直し作業の中で、テスト用の解答用紙画像を作成し、種々の確認作業を行ったところ、作った本人が採点済み解答用紙画像を保存するフォルダの準備を失念してしまい、初めて内在していたこの欠陥に気づいた次第です。

どぉしてこんなにバカなのか・・・

そこで次のように、採点済み解答用紙画像を保存するフォルダがなかった場合には警告を表示するようにプログラムを修正しました。

  //読み込むデータのあるフォルダへのPathを取得して表示
  SrcPath:=ExtractFilePath(imgPath)+'MarkedAnswerSheet';

  //フォルダの存在を確認 -> ない場合は警告してExitする
  if not System.SysUtils.DirectoryExists(SrcPath) then
  begin

    strMsg:='動作に必要なフォルダがありません!'+#13#10+
      'AC_Readerで「画像変換」を行ってから、再度実行してください。'+#13#10+#13#10+
      '処理を中止します。';
    Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);

    ・・・ 省略 ・・・
    Image1.Picture.Assign(nil);
    Exit;
  end;

5.の記事を書いた真意ですが、このプログラムを単体で起動するとパスワード入力を求められますので、「フリーソフトと言いながら、ふざけんな!」と気分を悪くされた方も、もしかしたらいらっしゃるかもしれないと思い、なぜ、パスワード入力が必要なのか、その本当の理由を記した次第です。

6.常に最大化して実行する設定にしてやっぱりやめました!

解答欄矩形を示すラバーバンドの位置を解答欄上に正しく表示するには、画面は常に最大化して表示する必要があります(最大化表示していないと解答欄矩形とラバーバンドがずれて表示されます)。

こちらの問題も修正しようかとも思いましたが、このプログラムを実行する場合、画面は最大化して作業するのが最も効率がよく、何か他の画面と並べて作業する必要性もないので、Form は常に最大化して表示する設定とし、通常 Form の右上にある最大化及び最小化ボタンは表示しないようにプログラムを変更しました。

また、通常の場合、最大化状態で Form のタイトルバーをクリックしてアクティブにし、そのままタイトルバーをドラッグ&ドロップすると Window 内の任意の位置へ、設計時の大きさになった Form を移動できますが、上記の理由から、この時やはりラバーバンド位置が解答欄矩形からズレます。これを防止するため、Form のタイトルバーをクリックしてドラッグ&ドロップする機能は無効化しました。

この Form の設定に使用したコードは、以下の通りです。

  private
    //最初に1回だけ設定を実行するための確認フラグ
    F_FormActivated: Boolean;

    //タイトルバーは残したまま「最大化解除できない」ように設定
    procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
    procedure WMNCLButtonDown(var Msg: TWMNCLButtonDown); message WM_NCLBUTTONDOWN;

procedure TForm1.FormCreate(Sender: TObject);
begin

  //Formの最大化ボタン及びドラッグ&ドロップを制御(禁止)する
  F_FormActivated := False;
  //最大化して表示する
  Form1.WindowState := wsMaximized;
  //ここで実行するとFormがタスクバーを覆い隠してしまう -> FormActiveで実行する
  //BorderIcons := [biSystemMenu, biMinimize];

end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  if not F_FormActivated then
  begin
    BorderIcons := [biSystemMenu];
    F_FormActivated := True;
  end;
end;

procedure TForm1.WMNCLButtonDown(var Msg: TWMNCLButtonDown);
begin
  if Msg.HitTest = HTCAPTION then
    Exit; // タイトルバーをドラッグしても動かせない
  inherited;
end;

procedure TForm1.WMSysCommand(var Msg: TWMSysCommand);
begin
  // 「元に戻す」「サイズ変更」を禁止
  if (Msg.CmdType = SC_RESTORE) or (Msg.CmdType = SC_SIZE) then
  begin
    Exit;
  end;
  inherited;
end;

・・・と、ここまで修正(?)したのですが。

ちょっと待て!
おまえ、逃げてない?

・・・ みたいな声が聴こえた気がして。(。>__<。)

「このプログラムを実行する場合、画面は最大化して作業するのが最も効率がよく、何か他の画面と並べて作業する必要性もない」

それって、言い訳じゃない?

なので、上のような現実逃避的「逃げの一手」みたいな卑怯な方法を取らず、やっぱり、ここも

ちゃんとする!

ことにしました。

取りあえず、上で行った設定を全部解除して・・・

画面を最大化せずに、プログラムを実行してみます。これまで、そのようなことをしたことがなかった(してみようとも思わなかった)ので、こんな欠陥が内在していることに、やはり気づかなかったのです。画面を非最大化した状態で、このプログラムを実行されました皆々さまには、大変なご迷惑をお掛けしたことと思います。こちらにつきましても、心より、こころよりお詫び申し上げます。

1つ前のバージョンを「非最大化」して実行すると・・・(後ろは Delphi の IDE です)

ラバーバンドの描画位置がズレてしまいます・・・


そこで最大化した場合でも、非最大化した場合でも、ラバーバンドが同じ位置に描画されるようにコードを修正しました。次がその修正したつもりのコードです。

procedure TForm1.Memo2Click(Sender: TObject);
var
  i:integer;
  p1,p2:TPoint;

  function RemoveToken(var s:string;delimiter:string):string;
  var
    p:Integer;
  begin
    p:=Pos(delimiter,s);
    if p=0 then Result:=s
    else Result:=Copy(s,1,p-1);
    s:=Copy(s,Length(Result)+Length(delimiter)+1,Length(s));
  end;

  function GetTokenIndex(s:string;delimiter:string;index:Integer):string;
  var
    i:Integer;
  begin
    Result:='';
    for i:=0 to index do
      Result:=RemoveToken(s,delimiter);
  end;

begin

  if not EditTF then
  begin

    //座標を取得
    i:=Memo2.Perform(EM_LINEFROMCHAR, Memo2.SelStart, 0);

    //エラー対策
    if Memo2.Lines[i]='' then Exit;

    x1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',0));
    y1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',1));
    x2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',2));
    y2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',3));

    if Assigned(plImage1) then begin
      FreeAndNil(plImage1);
    end;

    //コンポーネントを生成し,イベントを定義し,位置を指定して画像を表示
    plImage1:= TplResizeImage.Create(Self);
    plImage1.Parent:= ScrollBox1;
    plImage1.TransEvent:= True;

    //画像内座標
    p1 := Point(x1, y1);
    p2 := Point(x2, y2);
    //クライアント座標 -> スクリーン座標(Image1基準)
    p1 := Image1.ClientToScreen(p1);
    p2 := Image1.ClientToScreen(p2);
    //スクリーン座標 -> フォームのクライアント座標(Form基準)
    p1 := Form1.ScreenToClient(p1);
    p2 := Form1.ScreenToClient(p2);
    //ラバーバンドの座標を設定(フォームのクライアント座標で配置)
    plImage1.SetBounds(p1.X, p1.Y, p2.X - p1.X, p2.Y - p1.Y);

    //SelectedプロパティをTrueにするとラバーバンドとグラブハンドルが表示される
    plImage1.Selected := True;
    plImage1.BringToFront;

  end;

end;

上記コードを実行してみた結果です。最初に画面を最大化して表示した場合・・・

解答欄矩形に対して、ラバーバンドがわずかではありますが右下へズレています。


続けて、非最大化( Window 右上の「最大化ボタン」をクリック)した場合です。最大化ボタンを押して非最大化するというのも、なんともおかしな表現ですが、再度、このボタンをクリックすれば最大化されるので、やはりこれは最大化ボタンでいいのかな?

やはり微妙にズレてはいますが、位置的には先ほどと同じところに描画されています。
なので、半歩前進というところでしょうか?
背景は Delphi の IDE です。


この微妙なズレは、なぜ生じたのでしょうか? コードを追いかけてみます。

(1)OpenCV の矩形検出機能で読み取った解答欄矩形の座標を TMemo から読み込む。
(2)座標をカンマで切り分けて変数に代入。
(3)Image1.ClientToScreen( ) で、TImage の画像内座標をスクリーン座標に変換。
(4)Form1.ScreenToClient( ) で、スクリーン座標をフォームのクライアント座標に変換。
(5)plImage1.SetBounds( )で、ラバーバンドの描画位置を指定。
(6)plImage1.Selected := True で、ラバーバンドを描画。

どこにもおかしなところはない気がします。OpenCV が正しく読み取って保存したはずの解答欄矩形の座標の数値が間違っているとは到底思えませんし・・・

何が原因かと言えば、SetBounds 関数に渡した値がズレの原因であることは間違いありません。

SetBounds 関数に渡した値がズレの原因・・・

SetBounds 関数に渡した値がズレの原因・・・

SetBounds 関数に渡した値がズレの原因・・・

では、SetBounds 関数は、何の座標系に基づいてラバーバンドを表示しているのか・・・というと、座標系をフォームのクライアント座標に変換して渡しているから、フォームのクライアント座標で描画・・・ した結果・・・ それがちょっとズレてしまう・・・

・・・ってコトは、もしかして、僕は・・・変換すべき座標系を間違えて・・・渡して・・・ る?

    //コンポーネントを生成し,イベントを定義し,位置を指定して画像を表示
    plImage1:= TplResizeImage.Create(Self);
    plImage1.Parent:= ScrollBox1;
    plImage1.TransEvent:= True;

あ”!

そうだ! 解答用紙の画像はデカいから絶対にスクロールが必要で・・・

スクロールの設定でも、いつか、さんざん悩んだけれど。

plImage1 の親は、Form1 じゃなくて・・・

ScrollBox1 ・・・

plImage1 を Image1 の上に重ねて表示したいわけだから、この場合、plImage1.Parent := Image1; とするのが最も自然・・・なんだけれど、Image1 は TGraphicControl なので、子コントロールを持てないから、plImage1 の親は Image1 の親、つまり、ScrollBox1 にしてたんだ・・・。

だから、ラバーバンド( plImage1 )は ScrollBox の座標系で描画しないといけない・・・

ここまでわかれば、もう、必要ない気がするけど、念のため、確認。

    //親を確認
    ShowMessage(plImage1.Parent.Name);

表示されたのは・・・(当たり前ですが)


これでズレた原因がはっきりしました。ScrollBox のクライアント座標でラバーバンドを描画すれば、先ほどの微妙なズレは解消されるはずです。

    p1 := Point(x1, y1);
    p2 := Point(x2, y2);
    // クライアント座標 -> スクリーン座標(Image1基準)
    p1 := Image1.ClientToScreen(p1);
    p2 := Image1.ClientToScreen(p2);
    // スクリーン座標 -> plImage1 の親(ScrollBox1)のクライアント座標に変換
    p1 := plImage1.Parent.ScreenToClient(p1);
    p2 := plImage1.Parent.ScreenToClient(p2);
    // ラバーバンド表示(親のクライアント座標系で配置)
    plImage1.SetBounds(p1.X, p1.Y, p2.X - p1.X, p2.Y - p1.Y);

コードを修正して、実行してみました。最初に、全画面表示の場合です。

ラバーバンドは、解答欄矩形の上に完全に重なって描画されました!


続いて、非全画面表示の場合です。

こちらもOK!


たったひとつ、だけ・・・ ですが、今回も、よくなれた気がします!

7.最大化から非最大化した際に画面中央にフォームを表示します!

上の6.の記事を書いている時に、もうひとつ気になることが出来てしまいました。それは何かというと、最大化状態から非最大化した際に、Form の右側が画面の外にはみ出した状態で表示されてしまうことです。

「最大化」ボタンはちらっと見えていますが、「閉じる」ボタンは完全に見えません・・・


詳しいことはわかりませんが、この表示位置は私の方で何かした覚えがありませんので、おそらく OS 側で決めているのではないか・・・と思うのですが、やはり、これは何とかしたいところです。

私は普段は「1366×768」サイズに設定したモニターを使ってプログラムを書いています。職場ではもっと高解像度のモニターを与えられていますが、もともと大きさ的に限界のあるノート PC のモニターに必要以上の解像度設定は不要だと思います。若い方ならいざ知らず、年寄りには小さな画面&高解像度のモニター環境は厳しすぎる気がします。

ちなみに、このプログラムを書くために使用している Panasonic CF-QV は「2880×1920」の解像度が「推奨」設定されています。この高解像度モニターを「1366×768」という「低」解像度に落として使う私は、何か、もったいないコトをしているのでしょうか?

Word や Excel の使用が主、つまりビジネス用途である場合、コストパフォーマンス的にも、バッテリー効率の面から見ても、文字サイズや視認性の点でも、「普通に使いやすい・無理してない」という感覚的な面からも、汎用モニターにおける最適な画面解像度はやはり「1366×768」であると私的には思えてなりませんので、あくまでも独断ですが、私はこのサイズで収まるように GUI を作成しています。

ですので、この解答欄矩形の座標を検出するプログラムも、設計時の Form の幅は・・・

「1364」ピクセル!


Windows がその気になれば、ギリ! 幅1366 ピクセルの画面内に収めて、全体が見えるように表示できるはずなのですが、現実には右側が切れて表示されてしまいます。

自分でなんとかするしか、なさそうです。

で、どうしたか、というと・・・

  private
    { Private 宣言 }

    //「最大化->元に戻す」で画面の中央に表示
    FPrevWindowState: TWindowState;  //Window の状態を取得する
    procedure AdjustFormPosition;  //Form の表示位置を設定

グローバル変数と手続きをひとつずつ宣言して、Shift + Ctrl + C で手続きを実装。

で、通常状態に戻ったときに Form を中央に表示する AdjustFormPosition 手続きは・・・

procedure TForm1.AdjustFormPosition;
var
  WorkArea: TRect;
begin
  //フォームが属しているモニタのワークエリアを取得(マルチモニタ対応)
  WorkArea := Monitor.WorkareaRect;

  //横方向の調整
  if Width < (WorkArea.Right - WorkArea.Left) then
    Left := WorkArea.Left + ((WorkArea.Right - WorkArea.Left) - Width) div 2
  else
    //はみ出す場合は左端に寄せる
    Left := WorkArea.Left;

  //縦方向の調整
  if Height < (WorkArea.Bottom - WorkArea.Top) then
    Top := WorkArea.Top + ((WorkArea.Bottom - WorkArea.Top) - Height) div 2
  else
    //はみ出す場合は上端に寄せる
    Top := WorkArea.Top;

end;

FormCreate 時に、Form の状態を取得しておきます。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //「最大化->元に戻す」で画面の中央に表示
  FPrevWindowState := WindowState;

あとは、Form の OnResize イベントで、前回が最大化で、今回が通常状態なら、Form を中央に表示する処理を行うように設定。

procedure TForm1.FormResize(Sender: TObject);
var
  //for 高さの調整
  MemoHight, btnHight:integer;
begin
  //VCLの高さを調整
  ・・・ 省略 ・・・

  //「最大化->元に戻す」で画面の中央に表示
  //ユーザーが普通にフォームをドラッグして幅や高さを変えた場合を除外
  if (FPrevWindowState = wsMaximized) and (WindowState = wsNormal) then
    AdjustFormPosition;
  FPrevWindowState := WindowState;  //最新の状態を保存
end;

実行して、非最大化時の動作を確認します。

Form は、画面の中央に表示されました!


できたー☆

予定した(と言うか、気がついた)修正作業は、全部、無事完了しました!

どなた様も、お待ちになってないことと思いますが・・・

8.ダウンロードのご案内

今回、全面的に不具合を修正しました、この「解答欄矩形の座標を検出するプログラム」と、先日この Blog でご紹介した「自動採点機能みたいなモノを搭載した手書き答案の採点補助プログラム(こちらも様々に内在していた不具合を修正し、Version 3.1.0 としました)」及び「マークシートリーダー」、「採点結果通知表並びに成績一覧表作成プログラム」他を1つにまとめた zip ファイルを下記リンク先からダウンロードすることができます。

使用方法につきましては、下記リンク先の過去記事をご参照ください。

高解像度ディスプレイで、プログラムを実行される場合は、次のリンク先の記事の内容も必要に応じてご参照ください。

解答欄矩形の座標を検出するプログラムの操作方法の詳しい解説は(旧版用ですが基本的な操作方法はほとんど同じです)次のリンク先記事をご参照ください。

9.まとめ

あらためて今回の記事の内容を振り返り、これほど多くの不具合が内在していたことに気づかないまま、解答欄矩形の座標検出プログラムを掲載してしまっていたことを、心より深くお詫び申し上げます。

今回の見直しによって多くの問題点を洗い出し、修正することができましたが、もしかすると、まだ発見できていない不具合が残っている可能性も否定できません。

今後、不具合が判明した際には、速やかにこのブログ上でご報告し、修正済みのプログラムが整い次第、あらためてご案内させていただく所存です。今後とも何卒よろしくお願いいたします。

10.お願いとお断り

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