距離データから速度を計算|接近・停止・離反をリアルタイム表示【超音波センサー#4】

超音波センサー

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

概要

今回は、HC-SR04で測定した距離データから、対象物の速度を計算します。

距離データを使って、対象物が近づいているのか、止まっているのか、遠ざかっているのかをリアルタイムに判定します。

距離は「今どこにあるか」、速度は「どちらに動いているか」を表します。

今回は、距離と速度をリアルタイムグラフに表示し、接近・停止・離反の状態を判定します。

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

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

距離データから速度を計算|接近・停止・離反をリアルタイム表示【超音波センサー#4】

プログラムコード

マイコン(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ファイルから距離データを読み込み
  • 時間と距離データを数値化し、欠損値や異常な時間差を除外
  • 距離データを移動平均で平滑化
  • 距離の変化量と時間差から速度を計算
  • 速度データを移動平均で平滑化
  • 速度の大きさと向きから、5段階の動きに分類
  • 解析結果をCSVファイルに保存
  • グラフ表示
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


# =========================================================
# ▼ ユーザー設定:ここだけ変更すればOK
# =========================================================

CSV_FILE = "ultrasonic_filter_export.csv"

# 速度計算に使う距離列
# raw_cm / outlier_removed_cm / moving_average_cm / median_cm
DISTANCE_COLUMN = "moving_average_cm"

EXPORT_CSV_FILE = "ultrasonic_motion_analysis.csv"

MIN_DT = 0.01

DISTANCE_SMOOTH_WINDOW = 5
VELOCITY_SMOOTH_WINDOW = 9

# 停止とみなす速度 [cm/s]
VELOCITY_DEAD_ZONE = 5.0

# 高速とみなす速度 [cm/s]
FAST_SPEED_THRESHOLD = 30.0


# =========================================================
# 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 = df.dropna(
    subset=["elapsed_time_s", DISTANCE_COLUMN]
).reset_index(drop=True)

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


# =========================================================
# 時間重複・異常な時間差を除外
# =========================================================

dt_check = df["elapsed_time_s"].diff()

df = df[
    (dt_check.isna()) |
    (dt_check > MIN_DT)
].reset_index(drop=True)


# =========================================================
# 距離データ
# =========================================================

time_s = df["elapsed_time_s"].values
distance_cm = df[DISTANCE_COLUMN].values


# =========================================================
# 距離の平滑化
# =========================================================

distance_smooth = (
    pd.Series(distance_cm)
    .rolling(
        window=DISTANCE_SMOOTH_WINDOW,
        min_periods=1
    )
    .mean()
    .values
)


# =========================================================
# 速度計算
# 速度 = 距離変化量 ÷ 時間変化量
#
# 速度がマイナス:距離が小さくなる → 接近
# 速度がプラス:距離が大きくなる → 離反
# =========================================================

dt = np.diff(time_s)
dd = np.diff(distance_smooth)

velocity_cm_s = np.zeros(len(time_s))

valid = dt > MIN_DT
velocity_cm_s[1:][valid] = dd[valid] / dt[valid]


# =========================================================
# 速度の平滑化
# =========================================================

velocity_smooth = (
    pd.Series(velocity_cm_s)
    .rolling(
        window=VELOCITY_SMOOTH_WINDOW,
        min_periods=1
    )
    .mean()
    .values
)


# =========================================================
# 5段階判定
#
# -2 : 高速接近
# -1 : 低速接近
#  0 : 停止
#  1 : 低速離反
#  2 : 高速離反
# =========================================================

motion_state = []
motion_state_num = []

for v in velocity_smooth:

    if abs(v) < VELOCITY_DEAD_ZONE:
        motion_state.append("Stop")
        motion_state_num.append(0)

    elif v < 0 and abs(v) >= FAST_SPEED_THRESHOLD:
        motion_state.append("Fast approaching")
        motion_state_num.append(-2)

    elif v < 0:
        motion_state.append("Slow approaching")
        motion_state_num.append(-1)

    elif v > 0 and abs(v) >= FAST_SPEED_THRESHOLD:
        motion_state.append("Fast moving away")
        motion_state_num.append(2)

    else:
        motion_state.append("Slow moving away")
        motion_state_num.append(1)


# =========================================================
# 解析結果をCSV出力
# =========================================================

result_df = pd.DataFrame({
    "elapsed_time_s": time_s,
    "distance_cm": distance_cm,
    "distance_smooth_cm": distance_smooth,
    "velocity_cm_s": velocity_cm_s,
    "velocity_smooth_cm_s": velocity_smooth,
    "motion_state": motion_state,
    "motion_state_num": motion_state_num
})

result_df.to_csv(
    EXPORT_CSV_FILE,
    index=False,
    encoding="utf-8-sig"
)

print(f"解析結果を保存しました: {EXPORT_CSV_FILE}")


# =========================================================
# グラフ①:距離
# =========================================================

plt.figure(figsize=(12, 5))

plt.plot(time_s, distance_cm, label="Distance")
plt.plot(time_s, distance_smooth, label="Smoothed distance")

plt.title("Distance Time Series")
plt.xlabel("Time [s]")
plt.ylabel("Distance [cm]")
plt.grid(True)
plt.legend()

plt.show()


# =========================================================
# グラフ②:速度
# ゾーンを色分け
# =========================================================

plt.figure(figsize=(12, 5))

# 接近側:赤系
plt.axhspan(
    -max(abs(velocity_smooth)) - 10,
    -FAST_SPEED_THRESHOLD,
    alpha=0.15,
    color="red",
    label="Fast approaching zone"
)

plt.axhspan(
    -FAST_SPEED_THRESHOLD,
    -VELOCITY_DEAD_ZONE,
    alpha=0.08,
    color="red",
    label="Slow approaching zone"
)

# 停止ゾーン
plt.axhspan(
    -VELOCITY_DEAD_ZONE,
    VELOCITY_DEAD_ZONE,
    alpha=0.15,
    color="gray",
    label="Stop zone"
)

# 離反側:緑系
plt.axhspan(
    VELOCITY_DEAD_ZONE,
    FAST_SPEED_THRESHOLD,
    alpha=0.08,
    color="green",
    label="Slow moving away zone"
)

plt.axhspan(
    FAST_SPEED_THRESHOLD,
    max(abs(velocity_smooth)) + 10,
    alpha=0.15,
    color="green",
    label="Fast moving away zone"
)

plt.plot(
    time_s,
    velocity_smooth,
    linewidth=2,
    label="Smoothed velocity"
)

plt.axhline(0, linestyle="--")
plt.axhline(VELOCITY_DEAD_ZONE, linestyle=":")
plt.axhline(-VELOCITY_DEAD_ZONE, linestyle=":")
plt.axhline(FAST_SPEED_THRESHOLD, linestyle=":")
plt.axhline(-FAST_SPEED_THRESHOLD, linestyle=":")

plt.title("Velocity State Zones")
plt.xlabel("Time [s]")
plt.ylabel("Velocity [cm/s]")
plt.grid(True)
plt.legend(loc="upper right")

plt.show()


# =========================================================
# グラフ③:距離と速度
# =========================================================

fig, axes = plt.subplots(
    2,
    1,
    figsize=(12, 8),
    sharex=True
)

axes[0].plot(
    time_s,
    distance_smooth,
    label="Smoothed distance"
)

axes[0].set_title("Distance")
axes[0].set_ylabel("Distance [cm]")
axes[0].grid(True)
axes[0].legend()


# 速度ゾーン色分け
y_abs_max = max(abs(velocity_smooth)) + 10

axes[1].axhspan(
    -y_abs_max,
    -FAST_SPEED_THRESHOLD,
    alpha=0.15,
    color="red"
)

axes[1].axhspan(
    -FAST_SPEED_THRESHOLD,
    -VELOCITY_DEAD_ZONE,
    alpha=0.08,
    color="red"
)

axes[1].axhspan(
    -VELOCITY_DEAD_ZONE,
    VELOCITY_DEAD_ZONE,
    alpha=0.15,
    color="gray"
)

axes[1].axhspan(
    VELOCITY_DEAD_ZONE,
    FAST_SPEED_THRESHOLD,
    alpha=0.08,
    color="green"
)

axes[1].axhspan(
    FAST_SPEED_THRESHOLD,
    y_abs_max,
    alpha=0.15,
    color="green"
)

axes[1].plot(
    time_s,
    velocity_smooth,
    linewidth=2,
    label="Smoothed velocity"
)

axes[1].axhline(0, linestyle="--")
axes[1].axhline(VELOCITY_DEAD_ZONE, linestyle=":")
axes[1].axhline(-VELOCITY_DEAD_ZONE, linestyle=":")
axes[1].axhline(FAST_SPEED_THRESHOLD, linestyle=":")
axes[1].axhline(-FAST_SPEED_THRESHOLD, linestyle=":")

axes[1].set_title("Velocity")
axes[1].set_xlabel("Time [s]")
axes[1].set_ylabel("Velocity [cm/s]")
axes[1].grid(True)
axes[1].legend()

plt.tight_layout()
plt.show()


# =========================================================
# グラフ④:5段階判定結果
# =========================================================

plt.figure(figsize=(12, 4))

plt.step(
    time_s,
    motion_state_num,
    where="post",
    linewidth=2
)

# 背景色
plt.axhspan(-2.5, -1.5, alpha=0.15, color="red")
plt.axhspan(-1.5, -0.5, alpha=0.08, color="red")
plt.axhspan(-0.5, 0.5, alpha=0.15, color="gray")
plt.axhspan(0.5, 1.5, alpha=0.08, color="green")
plt.axhspan(1.5, 2.5, alpha=0.15, color="green")

plt.yticks(
    [-2, -1, 0, 1, 2],
    [
        "Fast approaching",
        "Slow approaching",
        "Stop",
        "Slow moving away",
        "Fast moving away"
    ]
)

plt.ylim(-2.5, 2.5)

plt.title("5-Level Motion State")
plt.xlabel("Time [s]")
plt.ylabel("State")
plt.grid(True)

plt.show()

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

  • ESP32から距離データをリアルタイム受信
  • 距離値を抽出し、移動平均で平滑化
  • 平滑化した距離の変化から速度を計算
  • 速度も移動平均で平滑化
  • 速度から、接近・停止・離反を5段階で判定
  • 距離と速度を上下2段のグラフでリアルタイム表示
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_motion_realtime.csv"

DISPLAY_POINTS = 100

MIN_DT = 0.01

DISTANCE_SMOOTH_WINDOW = 5
VELOCITY_SMOOTH_WINDOW = 9

VELOCITY_DEAD_ZONE = 5.0
FAST_SPEED_THRESHOLD = 30.0

UPDATE_INTERVAL_MS = 100

# 固定スケール
DISTANCE_Y_MIN = 0
DISTANCE_Y_MAX = 100

VELOCITY_Y_MIN = -50
VELOCITY_Y_MAX = 50


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

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",
    "motion_state",
    "motion_state_num",
    "raw_text"
])


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

time_plot = deque(maxlen=DISPLAY_POINTS)

raw_distance_plot = deque(maxlen=DISPLAY_POINTS)
distance_smooth_plot = deque(maxlen=DISPLAY_POINTS)

velocity_plot = deque(maxlen=DISPLAY_POINTS)
velocity_smooth_plot = deque(maxlen=DISPLAY_POINTS)

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 judge_motion_state(v):
    if abs(v) < VELOCITY_DEAD_ZONE:
        return "Stop", 0
    elif v < 0 and abs(v) >= FAST_SPEED_THRESHOLD:
        return "Fast approaching", -2
    elif v < 0:
        return "Slow approaching", -1
    elif v > 0 and abs(v) >= FAST_SPEED_THRESHOLD:
        return "Fast moving away", 2
    else:
        return "Slow moving away", 1


# =========================================================
# 速度グラフ背景・基準線描画
# =========================================================

def draw_velocity_zones(ax):
    # 接近側:赤系
    ax.axhspan(
        VELOCITY_Y_MIN,
        -FAST_SPEED_THRESHOLD,
        alpha=0.15,
        color="red"
    )

    ax.axhspan(
        -FAST_SPEED_THRESHOLD,
        -VELOCITY_DEAD_ZONE,
        alpha=0.08,
        color="red"
    )

    # 停止ゾーン
    ax.axhspan(
        -VELOCITY_DEAD_ZONE,
        VELOCITY_DEAD_ZONE,
        alpha=0.15,
        color="gray"
    )

    # 離反側:緑系
    ax.axhspan(
        VELOCITY_DEAD_ZONE,
        FAST_SPEED_THRESHOLD,
        alpha=0.08,
        color="green"
    )

    ax.axhspan(
        FAST_SPEED_THRESHOLD,
        VELOCITY_Y_MAX,
        alpha=0.15,
        color="green"
    )

    # 基準線
    ax.axhline(0, linestyle="--")
    ax.axhline(VELOCITY_DEAD_ZONE, linestyle=":")
    ax.axhline(-VELOCITY_DEAD_ZONE, linestyle=":")
    ax.axhline(FAST_SPEED_THRESHOLD, linestyle=":")
    ax.axhline(-FAST_SPEED_THRESHOLD, linestyle=":")


# =========================================================
# グラフ準備
# =========================================================

fig, axes = plt.subplots(
    2,
    1,
    figsize=(12, 8),
    sharex=True
)

# 上段:距離
line_distance, = axes[0].plot(
    [],
    [],
    linewidth=2,
    label="Smoothed distance"
)

axes[0].set_title("Distance")
axes[0].set_ylabel("Distance [cm]")
axes[0].set_ylim(DISTANCE_Y_MIN, DISTANCE_Y_MAX)
axes[0].grid(True)
axes[0].legend(loc="upper right")


# 下段:速度
line_velocity, = axes[1].plot(
    [],
    [],
    linewidth=2,
    label="Smoothed velocity"
)

draw_velocity_zones(axes[1])

axes[1].set_title("Velocity")
axes[1].set_xlabel("Time [s]")
axes[1].set_ylabel("Velocity [cm/s]")
axes[1].set_ylim(VELOCITY_Y_MIN, VELOCITY_Y_MAX)
axes[1].grid(True)
axes[1].legend(loc="upper right")


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

def update(frame):
    global last_time
    global last_distance_smooth

    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:
                dd = distance_smooth - last_distance_smooth
                velocity = dd / dt

        last_time = elapsed_time
        last_distance_smooth = distance_smooth

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

        velocity_buffer.append(velocity)

        velocity_smooth = float(
            np.mean(velocity_buffer)
        )

        # ---------------------------
        # 5段階判定
        # ---------------------------

        motion_state, motion_state_num = judge_motion_state(
            velocity_smooth
        )

        # ---------------------------
        # 表示用データに追加
        # ---------------------------

        time_plot.append(elapsed_time)

        raw_distance_plot.append(raw_distance)
        distance_smooth_plot.append(distance_smooth)

        velocity_plot.append(velocity)
        velocity_smooth_plot.append(velocity_smooth)

        # ---------------------------
        # CSV保存
        # ---------------------------

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

        csv_file.flush()

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

    # ---------------------------
    # グラフ更新
    # ---------------------------

    if len(time_plot) > 0:

        line_distance.set_data(
            time_plot,
            distance_smooth_plot
        )

        line_velocity.set_data(
            time_plot,
            velocity_smooth_plot
        )

        axes[0].set_xlim(
            max(0, time_plot[0]),
            time_plot[-1] + 1
        )

        # 縦軸は固定
        axes[0].set_ylim(
            DISTANCE_Y_MIN,
            DISTANCE_Y_MAX
        )

        axes[1].set_ylim(
            VELOCITY_Y_MIN,
            VELOCITY_Y_MAX
        )

    return line_distance, line_velocity


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

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

    csv_file.close()
    ser.close()

    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.tight_layout()
plt.show()
スポンサーリンク
超音波センサー
Follow
この記事を書いた人

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

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

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

Follow
QCとらのまき

コメント