🚙

画像の中に好きなエリアを作って座標ファイルを書き出す方法

に公開

📌はじめに

本記事では、画像の上にプロットされたデータを効率よくカウント・集計する方法をご紹介します。

せっかくユーザーに「好き/嫌い」をポチポチ入力してもらっても、
集計を手作業でやるのは現実的ではありません。
だったら、画像上のエリアごとに自動で集計できた方が便利ですよね。

そのためには、評価対象のタッチ座標が画像のどの範囲に属するのか、
あらかじめ「エリア」を定義しておく必要があります。

まずはGUIを使って、画像上でエリアをポチポチ選んでいきましょう。

🥅今回のゴール:エリア定義ファイル

エリア定義は CSVファイル に保存されます。
このCSVには、画像上で指定した各エリアの座標が記録されていきます。

xy_points.csv


📌分析の流れ

  1. 画像の読み込みとGUIによるラベリング(✅ 今回)
  2. ラベル情報の CSV 保存(✅ 今回)
  3. ラベル領域の色分け・描画(✅ 今回)
  4. 評価データの集計(like / dislike / none)
  5. Excel ファイルへの出力
  6. 散布図による可視化(参考)

📌調査内容(仮定)

エリア定義しやすそうな車の画像を使用しました。

  • 調査対象者には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ライブラリ Tkintersimpledialog を使っています。

  1. tk.Tk() — ウィンドウを作成
  2. root.withdraw() — ウィンドウを非表示にする
  3. simpledialog.askstring() — 入力用ポップアップを表示
  4. root.destroy() — ウィンドウを破棄
  5. 入力文字列を返す(未入力の場合はデフォルト値)

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
      • 各エリアの座標を順番に書き込み
    • 出力
      • 保存成功 → 「保存完了」と表示
      • 保存失敗 → エラーメッセージを表示

8. メイン処理

  • main: 画像を読み込み、ウィンドウ作成とマウス操作を受ける準備を行います。その後、ユーザー操作をループで受け付けます。

  • キー操作

    • 左クリック: 点追加
    • 右クリック: 点削除
    • スペース: 現在作業中の点を確定し、新しいグループとして追加 → 名前入力
    • Enter: 作業中のグループも含めて最終保存
    • ESC: キャンセル
❓エリア定義の操作方法❓

表示された画像上にマウスで点をクリックしてポリゴン(多角形)を作成し、部位名(ラベル)を入力していきます。

Animation_XY_UP.gif

操作 内容
左クリック 点を追加(ポリゴンの頂点として座標を登録)
右クリック 直前に追加した点を削除(取り消し)
スペースキー 現在までの点で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


📌まとめ

ここまでで、以下のことができました。

  • ✔️ 画像上に自由に点を打ってエリアを定義
  • ✔️ グループごとに名前をつけて保存

これで、次回のタッチデータ集計や分析の準備が整いました。

次回は、このエリア定義を使って「好き」「嫌い」のタッチデータを集計・分析する方法を解説します。

https://zenn.dev/swatchp/articles/abc121f3a6134e


参考リンク・素材について

GitHubリポジトリ

本記事で紹介したコードやサンプルデータはこちらのリポジトリで公開しています。
 https://github.com/iwakazusuwa/area-labeler-mit-license

画像素材

掲載している画像素材は「いらすとや」さんのものを加工して使っています。

Discussion