emrp:ws2025:amt
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| emrp:ws2025:amt [2026/02/26 06:01] – 36502_students.hsrw | emrp:ws2025:amt [2026/02/26 21:31] (current) – 37554_students.hsrw | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Animal Movement Tracker ====== | ====== Animal Movement Tracker ====== | ||
| + | **Authors: | ||
| ===== 1. Introduction & Problem Statement ===== | ===== 1. Introduction & Problem Statement ===== | ||
| Line 42: | Line 43: | ||
| |**//Figure 3//** Interfacing of the MCU and MPU6050| | |**//Figure 3//** Interfacing of the MCU and MPU6050| | ||
| - | To facilitate easier normalisation for IMU measurements, | + | To facilitate easier normalisation for IMU measurements, |
| === 2.2.3 Power Management === | === 2.2.3 Power Management === | ||
| Line 123: | Line 124: | ||
| |**//Figure 11// | |**//Figure 11// | ||
| - | === 2.4.2 Gateway System (RPi) === | + | === 2.4.2 Gesture Model === |
| The motion classification model has a three-class structure: | The motion classification model has a three-class structure: | ||
| * **Move:** The animal' | * **Move:** The animal' | ||
| Line 355: | Line 356: | ||
| main() | main() | ||
| - | if _name_ == "_main_": | + | if _name_ == "__main__": |
| main() | main() | ||
| </ | </ | ||
| Line 449: | Line 450: | ||
| </ | </ | ||
| - | The controller then collected a fixed-length IMU data window consisting of a total of 96 samples at a sampling frequency of 100 Hz. Each sample contained a total of six 16-bit raw measurements: | + | The controller then collected a fixed-length IMU data window consisting of a total of 96 samples at a sampling frequency of 100 Hz on the gateway side (a bit lower considering the transmission latency) |
| The collected window data was not sent in a single piece but was divided into fixed-length frames, taking into account the maximum packet size of the LoRa module. Each frame consisted of a header and a payload section. The header section contained address information, | The collected window data was not sent in a single piece but was divided into fixed-length frames, taking into account the maximum packet size of the LoRa module. Each frame consisted of a header and a payload section. The header section contained address information, | ||
| Line 617: | Line 618: | ||
| uint8_t who = 0; | uint8_t who = 0; | ||
| if (mpu_read(hi2c, | if (mpu_read(hi2c, | ||
| - | if (who != 0x68) return -2; // WHO_AM_I beklenen | + | if (who != 0x68) return -2; |
| // Wake up (disable sleep bit) | // Wake up (disable sleep bit) | ||
| Line 624: | Line 625: | ||
| // Sample rate = Gyro output / (1 + SMPLRT_DIV). Gyro output default 8kHz (DLPF=0) or 1kHz (DLPF!=0) | // Sample rate = Gyro output / (1 + SMPLRT_DIV). Gyro output default 8kHz (DLPF=0) or 1kHz (DLPF!=0) | ||
| - | mpu_write(hi2c, | + | mpu_write(hi2c, |
| mpu_write(hi2c, | mpu_write(hi2c, | ||
| mpu_write(hi2c, | mpu_write(hi2c, | ||
| Line 1266: | Line 1267: | ||
| raise ValueError(f" | raise ValueError(f" | ||
| - | # Return the window | + | # Return the window |
| return out | return out | ||
| </ | </ | ||
| Line 1311: | Line 1312: | ||
| ==== 3.3 Gesture Model ==== | ==== 3.3 Gesture Model ==== | ||
| === 3.3.1 Dataset Creation === | === 3.3.1 Dataset Creation === | ||
| - | The dataset was collected directly on the Raspberry Pi via the IMU (MPU6050) for training the motion classification model. During the data collection process, a total of 6 channels were read from the sensor: 3-axis acceleration (AX, AY, AZ) and 3-axis gyroscope (GX, GY, GZ). The collection process was designed based on windows, and each window consisted of A=96 consecutive samples. The sampling frequency Fs=100 Hz was selected, so each window represented a time interval of approximately 0.96 s. To increase temporal continuity and enhance data diversity, inter-window stride was applied, and S=48 with 50% overlapping windows was implemented. This structure ensured better capture of short-term motion transitions and increased boundary examples between classes. | + | The dataset was collected directly on the Raspberry Pi via the IMU (MPU6050) for training the motion classification model. During the data collection process, a total of 6 channels were read from the sensor: 3-axis acceleration (AX, AY, AZ) and 3-axis gyroscope (GX, GY, GZ). The collection process was designed based on windows, and each window consisted of A=96 consecutive samples. The sampling frequency Fs=100 Hz was selected |
| <file python record.py> | <file python record.py> | ||
| Line 1754: | Line 1755: | ||
| print(f" | print(f" | ||
| - | if _name_ == "_main_": | + | if _name_ == "__main__": |
| main() | main() | ||
| </ | </ | ||
| Line 1881: | Line 1882: | ||
| **Model v2 Architecture** | **Model v2 Architecture** | ||
| - | The v2 architecture is an updated 1D-CNN designed for three-class classification and TFLite deployment on Raspberry Pi. Figure 21 shows the full architecture. | + | The v2 architecture is an updated 1D-CNN designed for three-class classification and TFLite deployment on Raspberry Pi One-dimensional CNNs have been shown to be effective for classifying temporal sensor signals with limited training data [R3]. Figure 21 shows the full architecture. |
| {{ : | {{ : | ||
| Line 1906: | Line 1907: | ||
| **Experiment 1 — Batch Normalization** | **Experiment 1 — Batch Normalization** | ||
| - | Batch Normalization (BN) layers were added immediately after each of the three Conv1D layers. | + | Batch Normalization (BN) layers were added immediately after each of the three Conv1D layers. |
| **Result:** EarlyStopping triggered at epoch ~37 compared to ~80 for the baseline — convergence speed was more than halved. Test accuracy remained unchanged at 95.7%. Validation loss showed slightly more stable behaviour. The significant reduction in training time with no accuracy cost made this an easy decision. | **Result:** EarlyStopping triggered at epoch ~37 compared to ~80 for the baseline — convergence speed was more than halved. Test accuracy remained unchanged at 95.7%. Validation loss showed slightly more stable behaviour. The significant reduction in training time with no accuracy cost made this an easy decision. | ||
| Line 1914: | Line 1915: | ||
| A learning rate callback was added: when validation loss does not improve for 10 consecutive epochs, the learning rate is multiplied by 0.5 (minimum: 1×10⁻⁶). This allows the model to take large gradient steps early in training and progressively finer steps as it approaches a local optimum. | A learning rate callback was added: when validation loss does not improve for 10 consecutive epochs, the learning rate is multiplied by 0.5 (minimum: 1×10⁻⁶). This allows the model to take large gradient steps early in training and progressively finer steps as it approaches a local optimum. | ||
| - | **Result:** Test accuracy improved from 95.7% to 96.6% (113/117 correct). Shake recall improved from 41/46 to 42/46. Validation loss reached a lower minimum and remained more stable than in Experiment 1. EarlyStopping triggered at ~40 epochs. The improvement was measurable and consistent. | + | **Result:** Test accuracy improved from 95.7% to 96.6% (113/117 correct). Shake recall improved from 41/46 to 42/46. Validation loss reached a lower minimum and remained more stable than in Experiment 1. EarlyStopping triggered at ~40 epochs. The improvement was measurable and consistent.This configuration was further improved by augmentation in the final step. |
| **Decision: | **Decision: | ||
| Line 1920: | Line 1921: | ||
| To further increase the effective training set size, each training window was duplicated with a perturbed copy: Gaussian noise (σ=0.05 in normalised space) was added, and a random amplitude scale factor drawn from U(0.9, 1.1) was applied. The training set grew from 348 to 696 windows. The validation and test sets were not augmented. | To further increase the effective training set size, each training window was duplicated with a perturbed copy: Gaussian noise (σ=0.05 in normalised space) was added, and a random amplitude scale factor drawn from U(0.9, 1.1) was applied. The training set grew from 348 to 696 windows. The validation and test sets were not augmented. | ||
| - | **Result: | + | **Result: |
| - | **Decision: | + | **Decision: |
| Figure 23 summarises all three experiments across the three key metrics: test accuracy, epochs to convergence, | Figure 23 summarises all three experiments across the three key metrics: test accuracy, epochs to convergence, | ||
| {{ : | {{ : | ||
| - | |**//Figure 23// | + | | **//Figure 23// |
| **Final Model Selection** | **Final Model Selection** | ||
| - | The final model is the v2 + BatchNorm + ReduceLROnPlateau configuration. It achieves | + | The final model is the v2 + BatchNorm + ReduceLROnPlateau |
| {{ : | {{ : | ||
| Line 1946: | Line 1947: | ||
| <file python train.py> | <file python train.py> | ||
| + | """ | ||
| + | Training script for a tiny 1D-CNN on (96 x 6) IMU windows. | ||
| + | Dataset format: pre-windowed CSVs with columns label, window_index, | ||
| + | |||
| + | - Classes | ||
| + | - Window size : 96 samples @ 100 Hz (~0.96 s) | ||
| + | - Features | ||
| + | - Preprocess | ||
| + | - Model : Conv1D(16, | ||
| + | -> GAP -> Dense(24) -> Dropout(0.30) -> Softmax(3) | ||
| + | + GaussianNoise(0.02) at input (training-time only) | ||
| + | - EarlyStopping: | ||
| + | - Export | ||
| + | """ | ||
| + | |||
| + | import os | ||
| + | import json | ||
| + | import numpy as np | ||
| + | import pandas as pd | ||
| + | import matplotlib.pyplot as plt | ||
| + | import tensorflow as tf | ||
| + | from tensorflow.keras import layers, models | ||
| + | |||
| + | try: | ||
| + | from sklearn.metrics import confusion_matrix, | ||
| + | SKLEARN_OK = True | ||
| + | except ImportError: | ||
| + | SKLEARN_OK = False | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 0) Reproducibility & constants | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | SEED = 1337 | ||
| + | np.random.seed(SEED) | ||
| + | tf.random.set_seed(SEED) | ||
| + | |||
| + | GESTURES | ||
| + | NUM_GESTURES | ||
| + | SAMPLES_PER_GESTURE = 96 | ||
| + | FEATS = 6 # aX, aY, aZ, gX, gY, gZ | ||
| + | |||
| + | # ── Paths ──────────────────────────────────────────────────────────────────── | ||
| + | # Put your three clean CSV files in DATA_DIR, or adjust paths below. | ||
| + | DATA_DIR = " | ||
| + | OUT_DIR | ||
| + | os.makedirs(OUT_DIR, | ||
| + | |||
| + | DATASET_FILES = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 1) Load pre-windowed CSVs | ||
| + | # Each row = one window. | ||
| + | # We reshape each row into (96, 6) = [aX, aY, aZ, gX, gY, gZ] per time step. | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | CHANNEL_SUFFIXES = [" | ||
| + | |||
| + | ONE_HOT | ||
| + | inputs_list, | ||
| + | |||
| + | for g_idx, gesture in enumerate(GESTURES): | ||
| + | path = DATASET_FILES[gesture] | ||
| + | df = pd.read_csv(path) | ||
| + | |||
| + | # Build ordered feature columns: x0_ax, x0_ay, ..., x95_gz | ||
| + | feat_cols = [] | ||
| + | for t in range(SAMPLES_PER_GESTURE): | ||
| + | for ch in CHANNEL_SUFFIXES: | ||
| + | feat_cols.append(f" | ||
| + | |||
| + | windows = df[feat_cols].values.astype(np.float32) | ||
| + | labels | ||
| + | |||
| + | inputs_list.append(windows) | ||
| + | outputs_list.append(labels) | ||
| + | print(f" | ||
| + | |||
| + | inputs | ||
| + | outputs = np.concatenate(outputs_list, | ||
| + | print(f" | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 2) Shuffle + split 60 / 20 / 20 | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | idx = np.random.permutation(len(inputs)) | ||
| + | inputs | ||
| + | outputs = outputs[idx] | ||
| + | |||
| + | n = len(inputs) | ||
| + | n_train = int(0.60 * n) | ||
| + | n_val = int(0.20 * n) | ||
| + | |||
| + | X_flat_train = inputs[: | ||
| + | X_flat_val | ||
| + | X_flat_test | ||
| + | |||
| + | y_train = outputs[: | ||
| + | y_val = outputs[n_train : n_train + n_val] | ||
| + | y_test | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 3) Per-channel z-score normalisation (fit on TRAIN only) | ||
| + | # Shape trick: flatten -> (N, 96, 6) -> compute mean/std per channel axis | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | train_ts = X_flat_train.reshape(-1, | ||
| + | ch_mean | ||
| + | ch_std | ||
| + | |||
| + | def zscore(x_flat: | ||
| + | """ | ||
| + | x_ts = x_flat.reshape(-1, | ||
| + | x_ts = (x_ts - ch_mean) / ch_std | ||
| + | x_ts = np.clip(x_ts, | ||
| + | return x_ts | ||
| + | |||
| + | X_train = zscore(X_flat_train) | ||
| + | X_val = zscore(X_flat_val) | ||
| + | X_test | ||
| + | |||
| + | print(f" | ||
| + | rng = np.random.default_rng(SEED) | ||
| + | |||
| + | noise = rng.normal(0, | ||
| + | scale = rng.uniform(0.9, | ||
| + | X_aug = np.clip(X_train * scale + noise, -5.0, 5.0) | ||
| + | y_aug = y_train.copy() | ||
| + | |||
| + | X_train = np.concatenate([X_train, | ||
| + | y_train = np.concatenate([y_train, | ||
| + | |||
| + | |||
| + | shuffle_idx = rng.permutation(len(X_train)) | ||
| + | X_train = X_train[shuffle_idx] | ||
| + | y_train = y_train[shuffle_idx] | ||
| + | |||
| + | print(f" | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 4) Model definition | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | def build_model(input_shape=(SAMPLES_PER_GESTURE, | ||
| + | inp = layers.Input(shape=input_shape, | ||
| + | |||
| + | # GaussianNoise: | ||
| + | x = layers.GaussianNoise(0.02)(inp) | ||
| + | |||
| + | x = layers.Conv1D(16, | ||
| + | x = layers.BatchNormalization()(x) | ||
| + | |||
| + | x = layers.Conv1D(16, | ||
| + | x = layers.BatchNormalization()(x) | ||
| + | |||
| + | x = layers.MaxPooling1D(2)(x) | ||
| + | |||
| + | x = layers.Conv1D(24, | ||
| + | x = layers.BatchNormalization()(x) | ||
| + | x = layers.GlobalAveragePooling1D()(x) | ||
| + | x = layers.Dense(24, | ||
| + | x = layers.Dropout(0.30)(x) | ||
| + | out = layers.Dense(num_classes, | ||
| + | |||
| + | model = models.Model(inp, | ||
| + | model.compile( | ||
| + | optimizer=tf.keras.optimizers.Adam(1e-3), | ||
| + | loss=" | ||
| + | metrics=[" | ||
| + | ) | ||
| + | return model | ||
| + | |||
| + | model = build_model() | ||
| + | model.summary() | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 5) Training | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | early = tf.keras.callbacks.EarlyStopping( | ||
| + | monitor=" | ||
| + | patience=25, | ||
| + | restore_best_weights=True | ||
| + | ) | ||
| + | |||
| + | # ReduceLROnPlateau | ||
| + | reduce_lr = tf.keras.callbacks.ReduceLROnPlateau( | ||
| + | monitor=" | ||
| + | factor=0.5, | ||
| + | patience=10, | ||
| + | min_lr=1e-6, | ||
| + | verbose=1 | ||
| + | ) | ||
| + | |||
| + | history = model.fit( | ||
| + | X_train, y_train, | ||
| + | validation_data=(X_val, | ||
| + | epochs=200, | ||
| + | batch_size=8, | ||
| + | callbacks=[early, | ||
| + | verbose=2 | ||
| + | ) | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 6) Learning curves | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | fig, axes = plt.subplots(1, | ||
| + | |||
| + | axes[0].plot(history.history[" | ||
| + | axes[0].plot(history.history[" | ||
| + | axes[0].set_title(" | ||
| + | axes[0].grid(True); | ||
| + | |||
| + | axes[1].plot(history.history[" | ||
| + | axes[1].plot(history.history[" | ||
| + | axes[1].set_title(" | ||
| + | axes[1].grid(True); | ||
| + | |||
| + | plt.tight_layout() | ||
| + | plt.savefig(os.path.join(OUT_DIR, | ||
| + | plt.show() | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 7) Test evaluation + confusion matrix | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | test_loss, test_acc = model.evaluate(X_test, | ||
| + | best_val_acc | ||
| + | best_val_loss = float(np.min(history.history.get(" | ||
| + | |||
| + | print(f" | ||
| + | print(f" | ||
| + | |||
| + | y_prob = model.predict(X_test, | ||
| + | y_pred = y_prob.argmax(axis=1) | ||
| + | y_true = y_test.argmax(axis=1) | ||
| + | |||
| + | if SKLEARN_OK: | ||
| + | cm = confusion_matrix(y_true, | ||
| + | disp = ConfusionMatrixDisplay(confusion_matrix=cm, | ||
| + | disp.plot(cmap=" | ||
| + | plt.title(" | ||
| + | plt.savefig(os.path.join(OUT_DIR, | ||
| + | plt.show() | ||
| + | print(" | ||
| + | print(cm) | ||
| + | else: | ||
| + | print(" | ||
| + | |||
| + | # Class probabilities over test windows (sanity check) | ||
| + | plt.figure(figsize=(14, | ||
| + | for c_idx, cname in enumerate(GESTURES): | ||
| + | plt.plot(y_prob[:, | ||
| + | plt.title(" | ||
| + | plt.xlabel(" | ||
| + | plt.grid(True); | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 8) Export: TFLite (float32) + normalization.json | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # --- TFLite --- | ||
| + | converter | ||
| + | tflite_bytes = converter.convert() | ||
| + | |||
| + | tflite_path = os.path.join(OUT_DIR, | ||
| + | with open(tflite_path, | ||
| + | f.write(tflite_bytes) | ||
| + | print(f" | ||
| + | |||
| + | # --- normalization.json --- | ||
| + | # The model server MUST apply the same per-channel z-score before inference. | ||
| + | # Load this file on the RPi and apply: | ||
| + | # | ||
| + | norm = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | norm_path = os.path.join(OUT_DIR, | ||
| + | with open(norm_path, | ||
| + | json.dump(norm, | ||
| + | print(f" | ||
| + | |||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | # 9) Quick inference sanity check | ||
| + | # Verifies the exported TFLite model gives identical outputs to Keras. | ||
| + | # ───────────────────────────────────────────────────────────────────────────── | ||
| + | interp = tf.lite.Interpreter(model_path=tflite_path) | ||
| + | interp.allocate_tensors() | ||
| + | inp_det | ||
| + | out_det | ||
| + | |||
| + | sample | ||
| + | keras_out = model.predict(sample, | ||
| + | |||
| + | interp.set_tensor(inp_det[" | ||
| + | interp.invoke() | ||
| + | tflite_out = interp.get_tensor(out_det[" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(" | ||
| </ | </ | ||
| Line 1967: | Line 2278: | ||
| <file python server.py> | <file python server.py> | ||
| # | # | ||
| + | import sys | ||
| + | import struct | ||
| import time | import time | ||
| import json | import json | ||
| Line 2307: | Line 2620: | ||
| #Initialize LoRa control ports (M0-M1, AUX) | #Initialize LoRa control ports (M0-M1, AUX) | ||
| GPIO.setmode(GPIO.BOARD) | GPIO.setmode(GPIO.BOARD) | ||
| - | GPIO.setwarnings(False) | + | |
| - | GPIO.setup(PIN_LORA_M0, | + | GPIO.setup(PIN_LORA_M0, |
| - | GPIO.setup(PIN_LORA_M1, | + | GPIO.setup(PIN_LORA_M1, |
| - | GPIO.setup(PIN_LORA_AUX, | + | GPIO.setup(PIN_LORA_AUX, |
| - | #Wait until AUX is high (Module is ready) | + | |
| - | if not wait_until_pin(PIN_LORA_AUX, | + | if not wait_until_pin(PIN_LORA_AUX, |
| - | raise_error(" | + | raise_error(" |
| - | else: | + | else: |
| - | print(" | + | print(" |
| - | #Set module mode (11) sleep | + | |
| - | GPIO.output(PIN_LORA_M0, | + | GPIO.output(PIN_LORA_M0, |
| - | GPIO.output(PIN_LORA_M1, | + | GPIO.output(PIN_LORA_M1, |
| - | if not wait_until_pin(PIN_LORA_AUX, | + | if not wait_until_pin(PIN_LORA_AUX, |
| - | raise_error(" | + | raise_error(" |
| - | else: | + | else: |
| - | print(" | + | print(" |
| - | #Send lora configuration parameters | + | |
| - | uart.reset_input_buffer() | + | uart.reset_input_buffer() |
| - | print(" | + | print(" |
| - | uart.write(lora_params) | + | uart.write(lora_params) |
| - | uart.flush() | + | uart.flush() |
| - | time.sleep(0.1) | + | time.sleep(0.1) |
| - | #Validate lora configuration parameters | + | |
| - | uart.reset_input_buffer() | + | uart.reset_input_buffer() |
| - | print(" | + | print(" |
| - | uart.write(cmd_params) | + | uart.write(cmd_params) |
| - | uart.flush() | + | uart.flush() |
| - | rx = uart.read(6) | + | rx = uart.read(6) |
| - | if rx != lora_params: | + | if rx != lora_params: |
| - | raise_error(" | + | raise_error(" |
| - | else: | + | else: |
| - | print(" | + | print(" |
| - | time.sleep(0.1) | + | time.sleep(0.1) |
| - | #Set module mode (00) transceiver | + | |
| - | if not wait_until_pin(PIN_LORA_AUX, | + | if not wait_until_pin(PIN_LORA_AUX, |
| - | raise_error(" | + | raise_error(" |
| - | else: | + | else: |
| - | print(" | + | print(" |
| - | GPIO.output(PIN_LORA_M0, | + | GPIO.output(PIN_LORA_M0, |
| - | GPIO.output(PIN_LORA_M1, | + | GPIO.output(PIN_LORA_M1, |
| - | time.sleep(0.01) | + | time.sleep(0.01) |
| #INIT FIREBASE SERVER | #INIT FIREBASE SERVER | ||
| Line 2394: | Line 2707: | ||
| fs = args.fs | fs = args.fs | ||
| period = 1.0 / fs | period = 1.0 / fs | ||
| + | | ||
| + | window_id = 0 | ||
| print(" | print(" | ||
| Line 2455: | Line 2770: | ||
| ts = time.time() | ts = time.time() | ||
| - | print(f" | + | print(f" |
| print(f" | print(f" | ||
| print(f" | print(f" | ||
| Line 2493: | Line 2808: | ||
| {{ : | {{ : | ||
| - | |Gateway & Server Test Video| | + | |**//Video 5//** Gateway & Server Test Video| |
| + | |||
| + | ===== 4.Discussion ===== | ||
| + | The implemented system has successfully demonstrated an end-to-end motion classification architecture. On the controller side, low-power mode standby, window-based data collection when motion is detected, and transmission to the gateway via LoRa; on the gateway side, real-time model execution and writing results to the cloud have all operated stably. | ||
| + | |||
| + | A clear distinction was observed between the move, rest, and shake classes in the model outputs. In particular, the model consistently distinguished between low-variance acceleration data in the rest state and high angular velocity values in the shake state. As the inference being performed on the gateway, raw IMU data was not sent to the cloud; only classification outputs were transmitted. This approach reduced bandwidth usage and increased the system' | ||
| + | |||
| + | The dataset created during the project process contains a limited number of windows. Saturation problems were observed, particularly in the shake class, and the sensor range settings were optimised. The small data set is a factor that could limit the model' | ||
| + | |||
| + | Furthermore, | ||
| + | |||
| + | The system functionality can be improved in various ways. For example, ZigBee, 2.4 GHz RF or Wi-Fi-based solutions can be used instead of LoRa. Particularly in short-range applications, | ||
| + | |||
| + | For model training, several directions could further improve the model in future iterations. The current dataset was recorded by a single person in a controlled desk environment. With a larger dataset covering multiple users, gesture intensities, | ||
| + | |||
| + | The current TFLite model uses float32 weights. Quantisation-aware training could produce an INT8 model, reducing memory footprint by approximately 4× and accelerating inference on the Raspberry Pi. This would also enable deployment on more constrained microcontrollers. The expected accuracy drop is 1–2 percentage points, which is acceptable given the current 96.6% baseline. | ||
| + | |||
| + | The current model is trained offline and deployed as a fixed artefact. For deployment on actual animal subjects, where motion dynamics differ from handheld recording, a lightweight fine-tuning step using a small number of animal-specific windows could substantially improve accuracy without full retraining. Transfer learning — freezing the convolutional layers and retraining only the dense head on new data — would be a practical approach given the limited compute available on the gateway. | ||
| + | |||
| + | The three-class model (Move, Rest, Shake) covers the primary behavioural states of interest. Future versions could extend to additional classes such as Eat, Play, or Sleep, provided sufficient labelled data is available. The modular architecture makes extension straightforward: | ||
| + | |||
| + | ===== 5. Conclusion & Outlook ===== | ||
| + | In this study, a low-power IMU-based motion classification system was designed and an end-to-end IoT architecture was implemented. The system integrates sensor measurement, | ||
| + | |||
| + | Future work plans include improving model performance with larger datasets, evaluating different communication technologies, | ||
| + | |||
| + | ===== 6. References ===== | ||
| + | European Telecommunications Standards Institute (ETSI). 2018. “Short Range Devices (SRD) operating in the frequency range 25 MHz to 1 000 MHz; Part 2: Harmonised Standard for access to radio spectrum for non specific radio equipment”. Visited: 09.02.2026. Available at: https:// | ||
| + | |||
| + | M. Alsaaod, J.J. Niederhauser, | ||
| + | “Development and validation of a novel pedometer algorithm to quantify extended characteristics of the locomotor behavior of dairy cows, Journal of Dairy Science”. Volume 98. Issue 9. | ||
| + | pp. 6236-6242. ISSN 0022-0302. DOI: https:// | ||
| + | |||
| + | Kiranyaz, S., Avci, O., Abdeljaber, O., Ince, T., Gabbouj, M., & Inman, D. J. (2021). 1D convolutional neural networks and applications: | ||
| + | |||
| + | Ioffe, S., & Szegedy, C. (2015). Batch normalization: | ||
| + | |||
| + | |||
| + | |||
| + | |||
emrp/ws2025/amt.1772082070.txt.gz · Last modified: 2026/02/26 06:01 by 36502_students.hsrw