距離と速度をカラーマップ化|超音波センサーの動きを見える化してみた【超音波センサー#5】

超音波センサー

※当サイトは、アフィリエイト広告を利用しています

概要

今回は、HC-SR04で取得した距離と速度のデータを、カラーマップで見える化します。

前回は、距離データから速度を計算し、接近・停止・離反をリアルタイムに表示しました。
第5回では、その距離と速度の変化を色で表現し、対象物の動きを直感的に確認できるようにします。

折れ線グラフでは細かい変化を確認できますが、カラーマップにすると、距離や速度の変化パターンを一目で見やすくなります。

ちょっとしたデジタル化のトライアルをお考えの方に、少しでも参考になればうれしいです。

詳しくは、以下のYouTube動画をご覧ください。

プログラムコード

マイコン(ESP32)

  • 超音波を発射
  • 対象物で反射
  • 戻ってくるまでの時間を測定
  • 音速から距離を計算
#define TRIG_PIN 13
#define ECHO_PIN 14

// 最大測定距離[cm]
#define MAX_DISTANCE 700

// タイムアウト時間[μs]
float timeOut = MAX_DISTANCE * 60;

// 音速[m/s]
const int SOUND_VELOCITY = 340;

void setup()
{
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);

    Serial.begin(115200);
}

void loop()
{
    float distance = getDistance();

    Serial.print("Distance: ");
    Serial.print(distance);
    Serial.println(" cm");

    delay(100);
}


/*************************************************
 * 距離測定関数
 *************************************************/
float getDistance()
{
    unsigned long pingTime;

    // 超音波パルス送信
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);

    // 反射波の時間測定
    pingTime = pulseIn(
        ECHO_PIN,
        HIGH,
        timeOut
    );

    // 距離計算
    float distance =
        (float)pingTime *
        SOUND_VELOCITY /
        2 /
        10000;

    return distance;
}

Python(CSV読み込み版)

  • CSVファイルから、時間・距離・速度データを読み込み
  • 数値化できないデータや欠損値を除外
  • 時間順にデータを並べ替え
  • 指定した時間幅ごとにデータを分割
  • 各時間区間ごとに、距離の平均値を計算
  • 各時間区間ごとに、速度の平均値を計算
  • グラフ表示
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


# ============================================
# ▼ここだけ設定すればOK
# ============================================

CSV_FILE = "ultrasonic_motion_analysis.csv"

DISTANCE_COLUMN = "distance_smooth_cm"
VELOCITY_COLUMN = "velocity_smooth_cm_s"

TIME_BIN = 0.2

DISTANCE_MIN = 0
DISTANCE_MAX = 100

VELOCITY_MIN = -50
VELOCITY_MAX = 50


# ============================================
# CSV読込
# ============================================

df = pd.read_csv(CSV_FILE)

df["elapsed_time_s"] = pd.to_numeric(df["elapsed_time_s"], errors="coerce")
df[DISTANCE_COLUMN] = pd.to_numeric(df[DISTANCE_COLUMN], errors="coerce")
df[VELOCITY_COLUMN] = pd.to_numeric(df[VELOCITY_COLUMN], errors="coerce")

df = df.dropna(
    subset=["elapsed_time_s", DISTANCE_COLUMN, VELOCITY_COLUMN]
).reset_index(drop=True)

df = df.sort_values("elapsed_time_s").reset_index(drop=True)

time_s = df["elapsed_time_s"].values
distance = df[DISTANCE_COLUMN].values
velocity = df[VELOCITY_COLUMN].values


# ============================================
# 時間ビン作成
# ============================================

t_start = np.min(time_s)
t_end = np.max(time_s)

time_bins = np.arange(t_start, t_end + TIME_BIN, TIME_BIN)

if time_bins[-1] < t_end:
    time_bins = np.append(time_bins, t_end)

n_time = len(time_bins) - 1


# ============================================
# 時間ビンごとの平均値を作る関数
# ============================================

def make_time_map(time_s, value, time_bins):
    value_map = np.full((1, len(time_bins) - 1), np.nan)

    for i in range(len(time_bins) - 1):
        if i == len(time_bins) - 2:
            mask = (time_s >= time_bins[i]) & (time_s <= time_bins[i + 1])
        else:
            mask = (time_s >= time_bins[i]) & (time_s < time_bins[i + 1])

        if np.any(mask):
            value_map[0, i] = np.mean(value[mask])

    return value_map


distance_map = make_time_map(time_s, distance, time_bins)
velocity_map = make_time_map(time_s, velocity, time_bins)


# ============================================
# 距離マップ表示
# ============================================

fig = plt.figure(figsize=(14, 6))

gs = fig.add_gridspec(
    2,
    2,
    width_ratios=[30, 1],
    height_ratios=[1, 3],
    hspace=0.25,
    wspace=0.08
)

ax_map = fig.add_subplot(gs[0, 0])
ax_trend = fig.add_subplot(gs[1, 0], sharex=ax_map)
cax = fig.add_subplot(gs[0, 1])

mesh = ax_map.pcolormesh(
    time_bins,
    [0, 1],
    distance_map,
    cmap="turbo_r",
    vmin=DISTANCE_MIN,
    vmax=DISTANCE_MAX,
    shading="flat"
)

ax_map.set_title("Distance Map")
ax_map.set_yticks([])
ax_map.set_xlim(t_start, t_end)
ax_map.tick_params(labelbottom=False)

cbar = fig.colorbar(mesh, cax=cax)
cbar.set_label("Distance [cm]")

ax_trend.plot(
    time_s,
    distance,
    linewidth=2,
    label="Distance"
)

ax_trend.set_title("Distance Trend")
ax_trend.set_xlabel("Time [s]")
ax_trend.set_ylabel("Distance [cm]")
ax_trend.set_xlim(t_start, t_end)
ax_trend.set_ylim(DISTANCE_MIN, DISTANCE_MAX)
ax_trend.grid(True)
ax_trend.legend(loc="upper right")

plt.show()


# ============================================
# 速度マップ表示
# ============================================

fig = plt.figure(figsize=(14, 6))

gs = fig.add_gridspec(
    2,
    2,
    width_ratios=[30, 1],
    height_ratios=[1, 3],
    hspace=0.25,
    wspace=0.08
)

ax_map = fig.add_subplot(gs[0, 0])
ax_trend = fig.add_subplot(gs[1, 0], sharex=ax_map)
cax = fig.add_subplot(gs[0, 1])

mesh = ax_map.pcolormesh(
    time_bins,
    [0, 1],
    velocity_map,
    cmap="RdYlGn",
    vmin=VELOCITY_MIN,
    vmax=VELOCITY_MAX,
    shading="flat"
)

ax_map.set_title("Velocity Map")
ax_map.set_yticks([])
ax_map.set_xlim(t_start, t_end)
ax_map.tick_params(labelbottom=False)

cbar = fig.colorbar(mesh, cax=cax)
cbar.set_label("Velocity [cm/s]")

ax_trend.plot(
    time_s,
    velocity,
    linewidth=2,
    label="Velocity"
)

ax_trend.axhline(0, linestyle="--")

ax_trend.set_title("Velocity Trend")
ax_trend.set_xlabel("Time [s]")
ax_trend.set_ylabel("Velocity [cm/s]")
ax_trend.set_xlim(t_start, t_end)
ax_trend.set_ylim(VELOCITY_MIN, VELOCITY_MAX)
ax_trend.grid(True)
ax_trend.legend(loc="upper right")

plt.show()

Python(リアルタイム通信版)

  • ESP32から距離データをリアルタイム受信
  • 受信文字列から距離値を抽出
  • 距離データを移動平均で平滑化
  • 平滑化した距離の変化から速度を計算
  • 速度データも移動平均で平滑化
  • 距離と速度のデータをCSVに保存
  • 最新の一定時間分のデータだけを表示対象にグラフ化
import serial
import time
import re
import csv
from collections import deque

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


# ============================================
# ▼ここだけ設定すればOK
# ============================================

PORT = "COM5"
BAUDRATE = 115200

EXPORT_CSV_FILE = "ultrasonic_realtime_map.csv"

# 画面に表示する時間幅 [s]
DISPLAY_SEC = 20.0

# カラーマップの時間分解能 [s]
TIME_BIN = 0.2

# 距離マップの表示範囲 [cm]
DISTANCE_MIN = 0
DISTANCE_MAX = 100

# 速度マップの表示範囲 [cm/s]
VELOCITY_MIN = -50
VELOCITY_MAX = 50

# 平滑化の窓幅
DISTANCE_SMOOTH_WINDOW = 5
VELOCITY_SMOOTH_WINDOW = 9

# 速度計算時に無視する最小時間差 [s]
MIN_DT = 0.01

# グラフ更新周期 [ms]
UPDATE_INTERVAL_MS = 100


# ============================================
# シリアル接続
# ============================================

ser = serial.Serial(
    PORT,
    BAUDRATE,
    timeout=1
)

time.sleep(2)


# ============================================
# CSV保存準備
# ============================================

csv_file = open(
    EXPORT_CSV_FILE,
    mode="w",
    newline="",
    encoding="utf-8-sig"
)

csv_writer = csv.writer(csv_file)

csv_writer.writerow([
    "elapsed_time_s",
    "raw_distance_cm",
    "distance_smooth_cm",
    "velocity_cm_s",
    "velocity_smooth_cm_s",
    "raw_text"
])


# ============================================
# データ保存用
# ============================================

time_data = deque()
distance_data = deque()
velocity_data = deque()

distance_buffer = deque(maxlen=DISTANCE_SMOOTH_WINDOW)
velocity_buffer = deque(maxlen=VELOCITY_SMOOTH_WINDOW)

last_time = None
last_distance_smooth = None

start_time = time.time()


# ============================================
# マップ作成関数
# ============================================

def make_time_map(time_values, value_values, t_start, t_end):
    time_bins = np.arange(
        t_start,
        t_end + TIME_BIN,
        TIME_BIN
    )

    if len(time_bins) < 2:
        time_bins = np.array([
            t_start,
            t_start + TIME_BIN
        ])

    n_time = len(time_bins) - 1

    value_map = np.full(
        (1, n_time),
        np.nan
    )

    time_array = np.array(time_values)
    value_array = np.array(value_values)

    for i in range(n_time):

        if i == n_time - 1:
            mask = (
                (time_array >= time_bins[i])
                &
                (time_array <= time_bins[i + 1])
            )
        else:
            mask = (
                (time_array >= time_bins[i])
                &
                (time_array < time_bins[i + 1])
            )

        if np.any(mask):
            value_map[0, i] = np.mean(
                value_array[mask]
            )

    return time_bins, value_map


# ============================================
# 1画面4分割レイアウト
# ============================================

fig = plt.figure(
    figsize=(16, 8)
)

fig.suptitle(
    "Ultrasonic Sensor Real-time Distance & Velocity Map",
    fontsize=14
)

gs = fig.add_gridspec(
    2,
    4,
    width_ratios=[30, 1, 30, 1],
    height_ratios=[1, 3],
    hspace=0.35,
    wspace=0.15
)

# 左上:距離マップ
ax_dist_map = fig.add_subplot(gs[0, 0])

# 左下:距離推移
ax_dist_trend = fig.add_subplot(
    gs[1, 0],
    sharex=ax_dist_map
)

# 距離カラーバー
cax_dist = fig.add_subplot(gs[0, 1])

# 右上:速度マップ
ax_vel_map = fig.add_subplot(gs[0, 2])

# 右下:速度推移
ax_vel_trend = fig.add_subplot(
    gs[1, 2],
    sharex=ax_vel_map
)

# 速度カラーバー
cax_vel = fig.add_subplot(gs[0, 3])


# ============================================
# 距離グラフ初期設定
# ============================================

dist_mesh = None

line_dist, = ax_dist_trend.plot(
    [],
    [],
    linewidth=2,
    label="Distance"
)

ax_dist_map.set_title("Distance Map")
ax_dist_map.set_yticks([])
ax_dist_map.set_ylabel("(map)")

ax_dist_trend.set_title("Distance Trend")
ax_dist_trend.set_xlabel("Time [s]")
ax_dist_trend.set_ylabel("Distance [cm]")
ax_dist_trend.set_ylim(DISTANCE_MIN, DISTANCE_MAX)
ax_dist_trend.grid(True)
ax_dist_trend.legend(loc="upper right")


# ============================================
# 速度グラフ初期設定
# ============================================

vel_mesh = None

line_vel, = ax_vel_trend.plot(
    [],
    [],
    linewidth=2,
    label="Velocity"
)

ax_vel_map.set_title("Velocity Map")
ax_vel_map.set_yticks([])
ax_vel_map.set_ylabel("(map)")

ax_vel_trend.axhline(
    0,
    linestyle="--"
)

ax_vel_trend.set_title("Velocity Trend")
ax_vel_trend.set_xlabel("Time [s]")
ax_vel_trend.set_ylabel("Velocity [cm/s]")
ax_vel_trend.set_ylim(VELOCITY_MIN, VELOCITY_MAX)
ax_vel_trend.grid(True)
ax_vel_trend.legend(loc="upper right")


# ============================================
# カラーバー初期化用ダミー
# ============================================

dummy_dist = ax_dist_map.pcolormesh(
    [0, TIME_BIN],
    [0, 1],
    np.array([[np.nan]]),
    cmap="turbo_r",
    vmin=DISTANCE_MIN,
    vmax=DISTANCE_MAX,
    shading="flat"
)

cbar_dist = fig.colorbar(
    dummy_dist,
    cax=cax_dist
)

cbar_dist.set_label("Distance [cm]")


dummy_vel = ax_vel_map.pcolormesh(
    [0, TIME_BIN],
    [0, 1],
    np.array([[np.nan]]),
    cmap="RdYlGn",
    vmin=VELOCITY_MIN,
    vmax=VELOCITY_MAX,
    shading="flat"
)

cbar_vel = fig.colorbar(
    dummy_vel,
    cax=cax_vel
)

cbar_vel.set_label("Velocity [cm/s]")


# ============================================
# 更新処理
# ============================================

def update(frame):
    global last_time
    global last_distance_smooth
    global dist_mesh
    global vel_mesh

    # --------------------------------
    # シリアルデータ受信
    # --------------------------------

    while ser.in_waiting > 0:

        line_text = (
            ser.readline()
            .decode("utf-8", errors="ignore")
            .strip()
        )

        # 例: Distance: 52.34cm
        match = re.search(
            r"Distance:\s*(\d+\.?\d*)cm",
            line_text
        )

        if not match:
            continue

        raw_distance = float(match.group(1))
        elapsed_time = time.time() - start_time

        # --------------------------------
        # 距離の平滑化
        # --------------------------------

        distance_buffer.append(raw_distance)

        distance_smooth = float(
            np.mean(distance_buffer)
        )

        # --------------------------------
        # 速度計算
        # --------------------------------

        if last_time is None:
            velocity = 0.0

        else:
            dt = elapsed_time - last_time

            if dt <= MIN_DT:
                velocity = 0.0
            else:
                velocity = (
                    distance_smooth
                    - last_distance_smooth
                ) / dt

        last_time = elapsed_time
        last_distance_smooth = distance_smooth

        # --------------------------------
        # 速度の平滑化
        # --------------------------------

        velocity_buffer.append(velocity)

        velocity_smooth = float(
            np.mean(velocity_buffer)
        )

        # --------------------------------
        # データ保存
        # --------------------------------

        time_data.append(elapsed_time)
        distance_data.append(distance_smooth)
        velocity_data.append(velocity_smooth)

        csv_writer.writerow([
            f"{elapsed_time:.3f}",
            f"{raw_distance:.2f}",
            f"{distance_smooth:.2f}",
            f"{velocity:.2f}",
            f"{velocity_smooth:.2f}",
            line_text
        ])

        csv_file.flush()

        print(
            f"{elapsed_time:.2f}s | "
            f"distance={distance_smooth:.2f}cm | "
            f"velocity={velocity_smooth:.2f}cm/s"
        )

    if len(time_data) < 2:
        return []

    # --------------------------------
    # 表示時間範囲
    # --------------------------------

    current_time = time_data[-1]
    t_start = max(0, current_time - DISPLAY_SEC)
    t_end = max(DISPLAY_SEC, current_time)

    time_array = np.array(time_data)
    distance_array = np.array(distance_data)
    velocity_array = np.array(velocity_data)

    mask = time_array >= t_start

    t_show = time_array[mask]
    d_show = distance_array[mask]
    v_show = velocity_array[mask]

    # ============================================
    # 距離マップ更新
    # ============================================

    time_bins, distance_map = make_time_map(
        t_show,
        d_show,
        t_start,
        t_end
    )

    if dist_mesh is not None:
        dist_mesh.remove()

    dist_mesh = ax_dist_map.pcolormesh(
        time_bins,
        [0, 1],
        distance_map,
        cmap="turbo_r",
        vmin=DISTANCE_MIN,
        vmax=DISTANCE_MAX,
        shading="flat"
    )

    ax_dist_map.set_xlim(t_start, t_end)
    ax_dist_map.set_yticks([])
    ax_dist_map.tick_params(labelbottom=False)

    line_dist.set_data(
        t_show,
        d_show
    )

    ax_dist_trend.set_xlim(t_start, t_end)
    ax_dist_trend.set_ylim(DISTANCE_MIN, DISTANCE_MAX)

    # ============================================
    # 速度マップ更新
    # ============================================

    time_bins, velocity_map = make_time_map(
        t_show,
        v_show,
        t_start,
        t_end
    )

    if vel_mesh is not None:
        vel_mesh.remove()

    vel_mesh = ax_vel_map.pcolormesh(
        time_bins,
        [0, 1],
        velocity_map,
        cmap="RdYlGn",
        vmin=VELOCITY_MIN,
        vmax=VELOCITY_MAX,
        shading="flat"
    )

    ax_vel_map.set_xlim(t_start, t_end)
    ax_vel_map.set_yticks([])
    ax_vel_map.tick_params(labelbottom=False)

    line_vel.set_data(
        t_show,
        v_show
    )

    ax_vel_trend.set_xlim(t_start, t_end)
    ax_vel_trend.set_ylim(VELOCITY_MIN, VELOCITY_MAX)

    return []


# ============================================
# 終了処理
# ============================================

def on_close(event):
    print("終了処理を実行します。")

    try:
        csv_file.close()
    except Exception:
        pass

    try:
        ser.close()
    except Exception:
        pass

    print(f"CSVを保存しました: {EXPORT_CSV_FILE}")


fig.canvas.mpl_connect(
    "close_event",
    on_close
)


# ============================================
# アニメーション開始
# ============================================

ani = FuncAnimation(
    fig,
    update,
    interval=UPDATE_INTERVAL_MS,
    cache_frame_data=False
)

plt.show()
スポンサーリンク
超音波センサー
Follow
この記事を書いた人

【経歴】
関東在住、40代、製造業(品質部門)。
これまで、研究開発、設計、生産技術、仕入先の品質管理を手掛ける。

【保有知識・技術分野】
統計学、信頼性工学、品質工学。
半導体、基板、有機材料、金属、セラミックスの材料、製造、加工技術。
部品加工(機械加工、化学処理)、組立・実装技術、分析・物理解析技術。
QC検定1級保有。

【当サイトについて】
品質・生産の基礎知識をテーマに、用語の解説、使い方(作り方)、メリット、考え方のポイントを分かりやすく解説しています。
某メーカ様の品質教育用の資料としてもご活用いただいております。
QC検定(品質管理検定)の試験対策、おすすめ勉強法も紹介しています。

Follow
QCとらのまき

コメント