画像の中に好きなエリアを作って座標ファイルを書き出す方法
📌はじめに
本記事では、画像の上にプロットされたデータを効率よくカウント・集計する方法をご紹介します。
せっかくユーザーに「好き/嫌い」をポチポチ入力してもらっても、
集計を手作業でやるのは現実的ではありません。
だったら、画像上のエリアごとに自動で集計できた方が便利ですよね。
そのためには、評価対象のタッチ座標が画像のどの範囲に属するのか、
あらかじめ「エリア」を定義しておく必要があります。
まずはGUIを使って、画像上でエリアをポチポチ選んでいきましょう。
🥅今回のゴール:エリア定義ファイル
エリア定義は CSVファイル に保存されます。
このCSVには、画像上で指定した各エリアの座標が記録されていきます。
xy_points.csv
📌分析の流れ
- 画像の読み込みとGUIによるラベリング(✅ 今回)
- ラベル情報の CSV 保存(✅ 今回)
- ラベル領域の色分け・描画(✅ 今回)
- 評価データの集計(like / dislike / none)
- Excel ファイルへの出力
- 散布図による可視化(参考)
📌調査内容(仮定)
エリア定義しやすそうな車の画像を使用しました。
- 調査対象者にはiPadで車の写真を見てもらい、
「好きなところ(最大2ヶ所)」と「嫌いなところ(最大2ヶ所)」をタッチしてもらいます。
※タッチした位置は画像上の座標 (x, y) として記録されます。
📌参考:調査データ(次回使用)
列名 | 内容 |
---|---|
ID~購買意思決定期間 | 調査対象者の属性(基本的なデモグラ情報など) |
like1_x, like1_y, like2_x, like2_y | 好きな場所(最大2点)の画像上の座標値 |
dislike1_x, dislike1_y, dislike2_x, dislike2_y | 嫌いな場所(最大2点)の画像上の座標値 |
📌ディレクトリ構成
project-root/
├─ area_labeling.py
# エリア定義・描画用スクリプト
├─ car.png
# 対象画像ファイル
├─ xy_points.csv
# エリア定義結果CSV(スクリプト実行後に生成)
└─ labeled_areas.png
# エリア描画済み画像(スクリプト実行後に生成)
📌環境
python3.x
📌使用する画像(例)
サイズ:1380 × 890の車画像(car.png
)を使用します。
📌コード解説(rea_labeling.py)
エリア定義を行います
#============================================
# 0. ライブラリとファイル名
#============================================
import cv2
import csv
import subprocess
import tkinter as tk
from tkinter import simpledialog
# 画像ファイル名、出力ファイル名
IMAGE_PATH = "car.png"
OUTPUT_CSV = "xy_points.csv"
#============================================
# 1.グループカラー設定
#============================================
GROUP_COLORS = [
(255, 0, 0), (0, 255, 0), (0, 0, 255),
(255, 255, 0), (0, 255, 255), (255, 0, 255),
(255, 165, 0), (128, 0, 128)
]
#============================================
# 2. 状態管理
#============================================
current_group = []
point_groups = [] # [[(x, y), ...], ...]
group_names = [] # ["左ドア", "ボンネット", ...]
0. ライブラリとファイル名
-
cv2
: 画像表示やマウスクリック操作のためのOpenCVライブラリ -
csv
: エリア座標をCSVに保存するため -
subprocess
: 外部コマンド実行用(今回はファイル保存などで使用) -
tkinter
: ポップアップでエリア名を入力するGUI用 -
IMAGE_PATH
: 調査対象の画像ファイル名 -
OUTPUT_CSV
: 定義したエリア座標を保存するCSVファイル名
1.グループカラー設定
-
GROUP_COLORS
:エリアごとに違う色で描画するためのRGB形式のカラーパレット(8色) - エリアが8個を超える場合は、先頭の色から順に再利用されます
2. 状態管理変数
-
current_group
: 現在ユーザーがクリックして描いているエリアの座標リスト- クリックするたびに
(x, y)
座標が追加されます
- クリックするたびに
-
point_groups
: すでに確定したエリアの座標リスト- 各エリアは1つの座標リストとして格納
- 全エリアは二重リストで管理されます
❓リスト❓
-
座標のリスト : 1つのエリアを構成する点の
(x, y)
集合
例:[(10, 20), (50, 20), (50, 60), (10, 60)]
-
二重リスト : 複数のエリア分の座標リストをまとめたもの
[
[(10, 20), (50, 20), (50, 60), (10, 60)], # エリア1
[(70, 30), (120, 30), (120, 80), (70, 80)], # エリア2
...
]
この二重リスト構造にすることで、
- 複数エリアを1つの変数で管理できる
- 描画やCSV保存の処理をループで簡単に回せる
- エリアごとの名前や色もIDに対応させやすい
要するに、プログラム上で複数のエリアを効率よく管理するための形です。
-
group_names
: 各エリアにつけられた名前を文字列リストで管理- ユーザーがポップアップで入力した名前が保存されます
#============================================
# 3. グループカラー
#============================================
def get_group_color(gid): return GROUP_COLORS[gid % len(GROUP_COLORS)]
#============================================
# 4. GUI用 名前入力
#============================================
def ask_group_name(default_name="group"):
root = tk.Tk()
root.withdraw()
name = simpledialog.askstring("グループ名を入力", "このエリアの名前は?", initialvalue=default_name)
root.destroy()
return name if name else default_name
#============================================
# 5. 描画関数
#============================================
def redraw(img, winname, groups, names, current, cursor=None):
canvas = img.copy()
for gid, group in enumerate(groups):
color = get_group_color(gid)
for i, pt in enumerate(group):
cv2.circle(canvas, pt, 4, color, -1)
if i > 0:
cv2.line(canvas, group[i - 1], pt, color, 2)
if len(group) > 2:
cv2.line(canvas, group[-1], group[0], color, 1)
if names and gid < len(names):
label_pos = group[0]
cv2.putText(canvas, names[gid], (label_pos[0] + 5, label_pos[1] - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
color = get_group_color(len(groups))
for i, pt in enumerate(current):
cv2.circle(canvas, pt, 4, color, -1)
if i > 0:
cv2.line(canvas, current[i - 1], pt, color, 2)
if current and cursor:
cv2.line(canvas, current[-1], cursor, (200, 200, 200), 1)
if cursor:
cv2.putText(canvas, f"({cursor[0]}, {cursor[1]})", (10, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (240, 240, 240), 1)
cv2.imshow(winname, canvas)
#============================================
# 6. マウス操作
#============================================
def on_mouse(event, x, y, flags, param):
global current_group
img, winname = param["img"], param["winname"]
if event == cv2.EVENT_LBUTTONDOWN:
current_group.append((x, y))
elif event == cv2.EVENT_RBUTTONDOWN:
if current_group:
current_group.pop()
redraw(img, winname, point_groups, group_names, current_group, (x, y))
#============================================
# 7. 保存
#============================================
def save_named_csv(groups, names, path):
try:
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["name", "id", "x", "y"])
for gid, group in enumerate(groups):
name = names[gid] if gid < len(names) else f"group_{gid+1}"
for idx, (x, y) in enumerate(group, start=1):
writer.writerow([name, idx, x, y])
print(f"✅ 保存完了: {path}")
return True
except Exception as e:
print(f"❌ 保存失敗: {e}")
return False
#============================================
# 8. メイン処理
#============================================
def main():
global current_group, point_groups, group_names
img = cv2.imread(IMAGE_PATH)
if img is None:
print(f"❌ 画像が読み込めません: {IMAGE_PATH}")
return
winname = "Labeling Tool"
cv2.namedWindow(winname)
cv2.setMouseCallback(winname, on_mouse, {"img": img, "winname": winname})
redraw(img, winname, point_groups, group_names, current_group)
print("🖱️ 左クリック: 点追加|右クリック: 削除|スペース: グループ確定|Enter: 保存|ESC: キャンセル")
while True:
key = cv2.waitKey(1)
if key == 27: # ESC
print("キャンセルされました。")
break
elif key == 32: # スペース → グループ確定 & 名前入力
if current_group:
point_groups.append(current_group)
default_name = f"group_{len(point_groups)}"
name = ask_group_name(default_name)
group_names.append(name)
current_group = []
redraw(img, winname, point_groups, group_names, current_group)
elif key == 13: # Enter → 最終保存
if current_group:
point_groups.append(current_group)
default_name = f"group_{len(point_groups)}"
name = ask_group_name(default_name)
group_names.append(name)
current_group = []
if not point_groups:
print("⚠️ グループがありません。")
continue
if save_named_csv(point_groups, group_names, OUTPUT_CSV):
subprocess.Popen(["start", OUTPUT_CSV], shell=True)
break
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
3. グループカラー
-
get_group_color(gid)
:エリアID(gid
)を入力として受け取り、GROUP_COLORS
の色を返します。
💡 エリアIDの説明
複数のエリアグループを区別するための番号(0からスタート)です。
例えば、最初に作ったエリアがID0、次がID1…と順に割り当てられます。
このIDを使って、各エリアに色を割り当てたり、名前や座標データを管理します。
4. GUIでの名前入力
-
ask_group_name(default_name="group")
:
エリアを確定すると、小さなポップアップが表示されて名前を入力できます。
補足
エリアを確定する時(スペースキー)にポップアップが表示されます。
初期値は group_連番
です。
-
tkinter.simpledialog.askstring
: 名前入力時にキャンセルしたり空欄の場合は、自動でデフォルト名(例:group_1
)が使われます。
出力例
💡 Tkinterポップアップの仕組み
このポップアップは、Python標準GUIライブラリ Tkinter の simpledialog
を使っています。
-
tk.Tk()
— ウィンドウを作成 -
root.withdraw()
— ウィンドウを非表示にする -
simpledialog.askstring()
— 入力用ポップアップを表示 -
root.destroy()
— ウィンドウを破棄 - 入力文字列を返す(未入力の場合はデフォルト値)
withdraw()
を使うことで、ポップアップだけを表示し、余計なウィンドウが出ないようにしています。
5. 描画関数
-
redraw
:画像上にエリアや現在作業中の座標を描く関数 - 描画内容のポイント:
- 確定済みのエリア:色付きの点と線で表示
- ラベル:各エリアの最初の点の近くに名前を表示
- 作業中のエリア:まだ確定していない点と線を描画
- カーソル座標:任意で画面左上に現在のマウス座標を表示
- 使用しているOpenCV関数:
-
cv2.circle
:点を描く -
cv2.line
:線を描く -
cv2.putText
:文字を描く
-
6. マウス操作
-
on_mouse
:マウス操作を検知してリアルタイムに反映- 左クリック:現在作業中のエリアに点を追加
- 右クリック:最後に追加した点を削除
- マウス移動:移動に合わせて線や点を描画
- 描画は
redraw
で常に更新され、作業状況がすぐに確認できます
7. 保存
-
save_named_csv
:確定したエリア情報をCSVファイルに保存-
入力
-
groups
:各エリアの座標リスト -
names
:エリア名のリスト -
path
:保存先CSVファイルのパス
-
-
内容
- CSVのヘッダは
name, id, x, y
- 各エリアの座標を順番に書き込み
- CSVのヘッダは
-
出力
- 保存成功 → 「保存完了」と表示
- 保存失敗 → エラーメッセージを表示
-
入力
8. メイン処理
-
main
: 画像を読み込み、ウィンドウ作成とマウス操作を受ける準備を行います。その後、ユーザー操作をループで受け付けます。 -
キー操作
- 左クリック: 点追加
- 右クリック: 点削除
- スペース: 現在作業中の点を確定し、新しいグループとして追加 → 名前入力
- Enter: 作業中のグループも含めて最終保存
- ESC: キャンセル
❓エリア定義の操作方法❓
表示された画像上にマウスで点をクリックしてポリゴン(多角形)を作成し、部位名(ラベル)を入力していきます。
操作 | 内容 |
---|---|
左クリック | 点を追加(ポリゴンの頂点として座標を登録) |
右クリック | 直前に追加した点を削除(取り消し) |
スペースキー | 現在までの点で1つのエリア(グループ)を確定し、ラベル名入力のポップアップを表示。OK押下後、新しいグループの入力へ移行 |
Enter | すべてのグループをCSVファイルとして保存し、終了 |
ESC | キャンセルして終了(保存されません) |
- 保存後は、OSの既定アプリでCSVを開く(Windowsの場合)
出力例
列名 | 内容 |
---|---|
name | エリア名。ラベル入力のポップアップでユーザーが設定。未入力時は自動的に group_連番 が付与されます。 |
id | 各エリア内の点に割り振られる連番ID(1から始まる整数)。 |
x, y | エリアを構成する各点の座標。複数行にわたって点の座標が記録されます。 |
-
cv2.destroyAllWindows()
: 終了時にウィンドウを閉じる
📌コード解説
エリア定義の色付けを行います
#============================================
# 9. ライブラリ
#============================================
import random
#============================================
# 10. ファイル読み込み
#============================================
points_df = pd.read_csv(OUTPUT_CSV)
img = cv2.imread(IMAGE_PATH)
if img is None:
raise FileNotFoundError(f"画像が読み込めません: {IMAGE_PATH}")
#============================================
# 11. 色の用意(エリア数に合わせて自動生成)
#============================================
random.seed(42)
area_names = points_df["name"].unique()
colors = {}
for area in area_names:
colors[area] = [random.randint(50, 255) for _ in range(3)]
#============================================
# 12. エリアごとにポリゴンを描く関数
#============================================
def draw_area(img, polygon_points, color, name, thickness=2):
pts = np.array(polygon_points, np.int32)
pts = pts.reshape((-1, 1, 2))
# ポリゴン塗りつぶし(半透明)
overlay = img.copy()
cv2.fillPoly(overlay, [pts], color)
alpha = 0.4
cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0, img)
# ポリゴン輪郭
cv2.polylines(img, [pts], isClosed=True, color=color, thickness=thickness)
# エリア名の表示(ポリゴン重心に)
M = cv2.moments(pts)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
cv2.putText(img, name, (cx - 30, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2, cv2.LINE_AA)
cv2.putText(img, name, (cx - 30, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA)
#============================================
# 13. エリアごとに描画
#============================================
for area, group in points_df.groupby("name"):
polygon_points = group[["x", "y"]].values.tolist()
draw_area(img, polygon_points, colors[area], area)
# 保存
cv2.imwrite("a_labeled_areas.png", img)
print("✅ エリア描画済み画像を 'labeled_areas.png' に保存しました。")
9. ライブラリ
-
random
:エリアごとの色をランダムに生成するために使用します。
同じ色を避けつつ、複数エリアを識別しやすくします。
10. ファイル読み込み
-
points_df = pd.read_csv()
: エリア定義したCSVファイルを読み込みます。 -
img = cv2.imread()
:描画対象の画像を読み込みます。- 画像が存在しない場合は、
FileNotFoundError
でエラーを通知。
- 画像が存在しない場合は、
11. 色の用意(エリア数に合わせて自動生成)
-
area_names = points_df["name"].unique()
: CSV内にあるエリア名を取得します。 - 各エリアに対して、RGB値をランダムに生成して色を割り当てます。
-
random.seed(42)
: 毎回同じ色が生成されるようにし、再現性を確保します。
12. エリアごとにポリゴンを描く関数
-
draw_area()
:ポリゴンの座標を受け取り、画像上に描画します。- 半透明で塗りつぶし、輪郭線を描画
- ポリゴンの重心にエリア名を表示
13. エリアごとに描画
-
points_df.groupby("name")
:CSVから読み込んだ座標をエリア名ごとにまとめて処理 -
draw_area()
:各エリアを色付きポリゴンとして画像に描画
beled_areas.png
📌まとめ
ここまでで、以下のことができました。
- ✔️ 画像上に自由に点を打ってエリアを定義
- ✔️ グループごとに名前をつけて保存
これで、次回のタッチデータ集計や分析の準備が整いました。
次回は、このエリア定義を使って「好き」「嫌い」のタッチデータを集計・分析する方法を解説します。
参考リンク・素材について
GitHubリポジトリ
本記事で紹介したコードやサンプルデータはこちらのリポジトリで公開しています。
https://github.com/iwakazusuwa/area-labeler-mit-license
画像素材
掲載している画像素材は「いらすとや」さんのものを加工して使っています。
Discussion