2024,江端さんの技術メモ

Gstreamer とにかく動いたコマンドは、かたっぱしから残しておく

mp4ファイルの繰り返し送信をするコマンドの例(ただガタガタする)
ffmpeg -stream_loop -1 -i 192.168.1.8.mp4 -vf "scale=640:360, fps=0.1" -c:v libx264 -preset ultrafast -tune zerolatency -x264-params "keyint=10:scenecut=0" -maxrate 1M -bufsize 2M -c:a copy -f mpegts "srt://192.168.1.217:38089?latency=5000000"
(今後、色々改善を試みる)

 

良い参考→ https://notes.yh1224.com/tech/gstreamer/gstreamer-examples/

(文字を大きく表示)
gst-launch-1.0 -e videotestsrc pattern=snow  ! video/x-raw,width=640,height=480,framerate=30/1 ! textoverlay text="おわかりいただけただろうか...?" font-desc="Sans 48" ! autovideosink

gst-launch-1.0 -e videotestsrc pattern=snow num-buffers=300 ! video/x-raw,width=640,height=480,framerate=30/1 ! textoverlay text="<span foreground='#0000BB'>おわかりいただけただろうか...?</span>" ! autovideosink
(https://qiita.com/araki-yzrh/items/2bd1fc899ea157f704e4)

gst-launch-1.0 -e videotestsrc pattern=snow num-buffers=300 ! video/x-raw,width=640,height=480,framerate=30/1 ! textoverlay text="おわかりいただけただろうか...?" ! autovideosink

gst-launch-1.0 videotestsrc pattern=ball do-timestamp="true" ! videoconvert ! autovideosink (超高速移動ボール)  動きのあるものとしては snow, blink

gst-launch-1.0.exe -v filesrc location=file.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink

gst-launch-1.0 rtspsrc location=rtsp://192.168.0.2/ONVIF/Streaming/channels/0 latency=0 buffer-mode=0 drop-on-latency=true ! rtph264depay ! h264parse ! queue ! avdec_h264 ! videoscale ! video/x-raw, width=640, height=360 ! videorate ! video/x-raw, framerate=5/1 ! x264enc speed-preset=ultrafast tune=zerolatency key-int-max=10 ! mpegtsmux ! srtserversink uri="srt://192.168.1.217:38089" latency=5

gst-launch-1.0 filesrc location=192.168.1.1.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! videoscale ! video/x-raw, width=640, height=360 ! videorate ! video/x-raw, framerate=5/1 ! x264enc speed-preset=ultrafast tune=zerolatency key-int-max=10 ! mpegtsmux ! srtserversink uri="srt://192.168.1.217:38089" latency=5

 

2024,江端さんの技術メモ

VSCode の Auto Markdownで目次や章番号が出てこなくなったら、とにかく、拡張機能の全部にチェックをつける

2024,江端さんの技術メモ

以下のGo言語プログラムで、"small2_bus_data.csv"が1行のみの
1, 93, 139.62957005198, 35.36604342344, 12:55:00
を使って、を読み込ませたのですが、その結果が
1 0 0 12:55:00 [{1 0001-01-01 00:00:00 +0000 UTC {0 0}}]
となってしまいます。

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
	"time"
)

// 緯度経度の型定義
type LatLng struct {
	Lat, Lng float64
}

// 時間と緯度経度の情報を持つ構造体
type BusData struct {
	NodeID   int
	Time     time.Time
	Location LatLng
}

func main() {
	// CSVファイルを開く
	file, err := os.Open("small2_bus_data.csv")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer file.Close()

	// CSVファイルの内容をパースする
	reader := csv.NewReader(file)
	records, err := reader.ReadAll()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// データを格納するためのスライス
	var busData []BusData

	// CSVの各行を処理する
	for _, record := range records {
		nodeID, _ := strconv.Atoi(record[0])
		lng, _ := strconv.ParseFloat(record[2], 64)
		lat, _ := strconv.ParseFloat(record[3], 64)
		timeStr := record[4]

		fmt.Println(nodeID, lng, lat, timeStr)

		// 時間のパース
		var parsedTime time.Time
		if timeStr != "" {
			parsedTime, _ = time.Parse("15:04:05", timeStr)
		}

		// データを構造体に格納
		data := BusData{
			NodeID: nodeID,
			Time:   parsedTime,
			Location: LatLng{
				Lat: lat,
				Lng: lng,
			},
		}
		busData = append(busData, data)
	}

	fmt.Println(busData)
}

で、かなり、すったもんだした結果、文字列に余分なスペースが含まれていたため であることが分かりました(このくらい自動で対処して欲しいが)。

strings.TrimSpace がキモだったようです。

修正後のプログラムは以下の通り。

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

// 緯度経度の型定義
type LatLng struct {
	Lat, Lng float64
}

// 時間と緯度経度の情報を持つ構造体
type BusData struct {
	NodeID   int
	Time     time.Time
	Location LatLng
}

func main() {
	// CSVファイルを開く
	file, err := os.Open("small2_bus_data.csv")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer file.Close()

	// CSVファイルの内容をパースする
	reader := csv.NewReader(file)
	records, err := reader.ReadAll()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// データを格納するためのスライス
	var busData []BusData

	// CSVの各行を処理する
	for _, record := range records {
		nodeID, _ := strconv.Atoi(record[0])

		// スペースをトリムしてから実数に変換
		lng, err := strconv.ParseFloat(strings.TrimSpace(record[2]), 64)
		if err != nil {
			fmt.Println("Error parsing lng:", err)
			return
		}
		lat, err := strconv.ParseFloat(strings.TrimSpace(record[3]), 64)
		if err != nil {
			fmt.Println("Error parsing lat:", err)
			return
		}
		timeStr := strings.TrimSpace(record[4]) // スペースをトリム

		fmt.Println(nodeID, lng, lat, timeStr)

		// 時間のパース
		var parsedTime time.Time
		if timeStr != "" {
			parsedTime, err = time.Parse("15:04:05", timeStr)
			if err != nil {
				fmt.Println("Error parsing time:", err)
				return
			}
		}

		// データを構造体に格納
		data := BusData{
			NodeID: nodeID,
			Time:   parsedTime,
			Location: LatLng{
				Lat: lat,
				Lng: lng,
			},
		}
		busData = append(busData, data)
	}

	fmt.Println(busData)
}

出力結果は
>go run main28.go
1 139.62957005198 35.36604342344 12:55:00
[{1 0000-01-01 12:55:00 +0000 UTC {35.36604342344 139.62957005198}}]
となり、一安心です。

2024,江端さんの技術メモ

3次元を取り扱うDBSCANプログラム

このプログラムでは、同一座標は1つの座標として纏められてしまって、異なるユーザの座標として取り扱ってくれません。

これに対応するために修正したプログラムは以下の通りです。

// ~/tomioka_school/src/trip_school/dbscan_3d_2.go



package main

import (
	"fmt"
	"math"
)

// Point represents a 3D point with coordinates x, y, and t
type Point struct {
	User string
	X, Y, T float64
}

// DistanceTo calculates the Euclidean distance between two 3D points
func (p Point) DistanceTo(other Point) float64 {
	dx := p.X - other.X
	dy := p.Y - other.Y
	dt := p.T - other.T
	return math.Sqrt(dx*dx + dy*dy + dt*dt)
}

// Cluster represents a cluster of points
type Cluster struct {
	Points []Point
}

// DBSCAN performs density-based clustering of 3D points
func DBSCAN(points []Point, epsilon float64, minPts int) []Cluster {
	var clusters []Cluster
	var visited = make(map[string]bool)

	for _, point := range points {
		pointKey := fmt.Sprintf("%s,%f,%f,%f", point.User, point.X, point.Y, point.T)
		if visited[pointKey] {
			continue
		}
		visited[pointKey] = true

		neighbours := getNeighbours(points, point, epsilon)
		if len(neighbours) < minPts {
			continue
		}

		var clusterPoints []Point
		expandCluster(&clusterPoints, points, visited, point, neighbours, epsilon, minPts)
		clusters = append(clusters, Cluster{Points: clusterPoints})
	}

	return clusters
}

// getNeighbours returns all points within distance epsilon of the given point
func getNeighbours(points []Point, point Point, epsilon float64) []Point {
	var neighbours []Point
	for _, other := range points {
		if point.DistanceTo(other) <= epsilon {
			neighbours = append(neighbours, other)
		}
	}
	return neighbours
}

// expandCluster expands the cluster from the given point
func expandCluster(cluster *[]Point, points []Point, visited map[string]bool, point Point, neighbours []Point, epsilon float64, minPts int) {
	*cluster = append(*cluster, point)
	for _, neighbour := range neighbours {
		neighbourKey := fmt.Sprintf("%s,%f,%f,%f", neighbour.User, neighbour.X, neighbour.Y, neighbour.T)
		if !visited[neighbourKey] {
			visited[neighbourKey] = true
			neighbourNeighbours := getNeighbours(points, neighbour, epsilon)
			if len(neighbourNeighbours) >= minPts {
				expandCluster(cluster, points, visited, neighbour, neighbourNeighbours, epsilon, minPts)
			}
		}
	}
}

func main() {
	points := []Point{
		{"A", 1, 2, 0},
		{"A", 1.5, 1.8, 1},
		{"A", 5, 8, 2},
		{"A", 8, 8, 3},
		{"A", 1, 0.6, 4},
		{"A", 9, 11, 5},
		{"A", 8, 2, 6},
		{"A", 10, 2, 7},
		{"A", 9, 3, 8},
		{"B", 1, 2, 0},
		{"B", 1.5, 1.8, 1},
		{"B", 5, 8, 2},
		{"B", 8, 8, 3},
		{"B", 1, 0.6, 4},
		{"B", 9, 11, 5},
		{"B", 8, 2, 6},
		{"B", 10, 2, 7},
		{"B", 9, 3, 8},
		{"C", 1, 2, 0},
		{"C", 1.5, 1.8, 1},
		{"C", 5, 8, 2},
		{"C", 8, 8, 3},
		{"C", 1, 0.6, 4},
		{"C", 9, 11, 5},
		{"C", 8, 2, 6},
		{"C", 10, 2, 7},
		{"C", 9, 3, 8},
	}

	// epsilon := 3.0
	// minPts := 5

	epsilon := 2.5
	minPts := 5


	clusters := DBSCAN(points, epsilon, minPts)
	fmt.Println("Combined Clusters:")
	for i, cluster := range clusters {
		fmt.Printf("Cluster %d:\n", i+1)
		for _, point := range cluster.Points {
			fmt.Printf("  (%s, %.2f, %.2f, %.2f)\n", point.User, point.X, point.Y, point.T)
		}
	}
}

出力結果は以下の通りです。
C:\Users\ebata\tomioka_school\src\trip_school>go run dbscan_3d_2.go
Combined Clusters:
Cluster 1:
(A, 1.00, 2.00, 0.00)
(A, 1.50, 1.80, 1.00)
(B, 1.00, 2.00, 0.00)
(B, 1.50, 1.80, 1.00)
(C, 1.00, 2.00, 0.00)
(C, 1.50, 1.80, 1.00)
Cluster 2:
(A, 8.00, 2.00, 6.00)
(A, 10.00, 2.00, 7.00)
(A, 9.00, 3.00, 8.00)
(B, 8.00, 2.00, 6.00)
(B, 10.00, 2.00, 7.00)
(B, 9.00, 3.00, 8.00)
(C, 8.00, 2.00, 6.00)
(C, 10.00, 2.00, 7.00)
(C, 9.00, 3.00, 8.00)

2024,江端さんの技術メモ

// NormalizeCoordinates 京急富岡駅を基準として正規化された緯度と経度を返す関数
// C:\Users\ebata\tomioka_school\src\trip_school\NormalizeCoordinates.go

package main

import (
	"fmt"
	"math"
)


func NormalizeCoordinates(referenceLat, referenceLng, lat, lng float64) (float64, float64) {
	// 1度あたりの緯度経度のメートル換算
	metersPerDegreeLat := 111319.9 // 緯度1度あたりのメートル数
	metersPerDegreeLng := 111319.9 * math.Cos(referenceLat*(math.Pi/180.0)) // 経度1度あたりのメートル数

	// 緯度と経度の差を計算
	deltaLat := lat - referenceLat
	deltaLng := lng - referenceLng

	// 正規化された緯度と経度を計算
	normalizedLat := deltaLat * metersPerDegreeLat / 100.0
	normalizedLng := deltaLng * metersPerDegreeLng / 100.0

	return normalizedLat, normalizedLng
}

func main() {
	// 京急富岡駅の緯度経度

	keikyuTomiokaLat := 35.367131726654705
	keikyuTomiokaLng := 139.62988318023088

	// 確認用の緯度経度(富岡駅のバス停)

	lat := 35.36605614545459
	lng := 139.6295094178281

	// 緯度経度の正規化
	normalizedLat, normalizedLng := NormalizeCoordinates(keikyuTomiokaLat, keikyuTomiokaLng, lat, lng)

	fmt.Printf("正規化された緯度: %.6f\n", normalizedLat)
	fmt.Printf("正規化された経度: %.6f\n", normalizedLng)
}

2024,江端さんの技術メモ

// NormalizeTime 2つの日時文字列の時間差を秒単位で計算し、600秒を1として正規化する関数
// C:\Users\ebata\tomioka_school\src\trip_school\NormalizeTime.go

package main

import (
	"fmt"
	"time"
)

func NormalizeTime(timeStr1, timeStr2 string) float64 {
	layout := "2006-01-02 15:04:05"
	t1, err := time.Parse(layout, timeStr1)
	if err != nil {
		fmt.Println("Error parsing time string 1:", err)
		return 0
	}

	t2, err := time.Parse(layout, timeStr2)
	if err != nil {
		fmt.Println("Error parsing time string 2:", err)
		return 0
	}

	duration := t2.Sub(t1).Seconds()
	normalizedDuration := duration / 600.0

	return normalizedDuration
}

func main() {
	timeStr1 := "2024-01-01 00:00:00"
	timeStr2 := "2024-02-11 23:45:00"

	normalized := NormalizeTime(timeStr1, timeStr2)
	fmt.Println("正規化された時間差:", normalized)
}

2024,江端さんの技術メモ

/*
	c:\users\ebata\dscan\dscan3.go

	DBSCANアルゴリズムを実装し、3次元空間のデータ(x、y、t)をクラスタリングするシンプルなプログラムです。
	このプログラムでは、各データポイントは3次元座標(x、y、t)で表され、距離の計算にはユークリッド距離を使用します。

	DBSCANをk-meansの違いで説明します。

	DBSCAN(Density-Based Spatial Clustering of Applications with Noise)とk-meansは、クラスタリングアルゴリズムですが、そのアプローチや動作原理にはいくつかの違いがあります。

	(1)クラスタリング方法:

	- DBSCAN: 密度ベースのクラスタリングアルゴリズムであり、データポイントの密度に基づいてクラスタを形成します。各点は、一定の距離(ε、epsilon)内に最小限の近傍点数(minPts)が存在する場合、その点を中心としたクラスタが形成されます。
	- k-means: 距離ベースのクラスタリングアルゴリズムであり、データポイントの距離に基づいてクラスタを形成します。クラスタの数(k)を事前に指定し、各点を最も近いセントロイド(クラスタの中心)に割り当てます。

	(2)クラスタの形状:

	- DBSCAN: クラスタの形状は任意であり、密度の高い領域に基づいて形成されます。したがって、DBSCANは非凸形状のクラスタを処理できます。
	- k-means: クラスタの形状は円形(球形)であり、各クラスタのセントロイドからの距離に基づいて決定されます。したがって、k-meansは凸形状のクラスタを前提としています。

	(3)ハイパーパラメータ:

	- DBSCAN: ε(epsilon)とminPtsの2つのハイパーパラメータを必要とします。εは近傍点の距離の閾値を定義し、minPtsはクラスタと見なすための最小の近傍点数を指定します
	- k-means: クラスタの数(k)を指定する必要があります。

	(4)ノイズの処理:

	- DBSCAN: ノイズポイントを自動的に検出し、外れ値として扱います。密度が低い領域に存在するポイントは、任意のクラスタに割り当てられず、ノイズとして扱われます。
	-k-means: 外れ値やノイズの処理を明示的に行いません。各点は必ずどれかのクラスタに割り当てられます。

	 要するに、DBSCANは密度ベースのアルゴリズムであり、任意の形状のクラスタを検出し、ノイズを処理する能力があります。一方、k-meansは距離ベースのアルゴリズムであり、クラスタの形状が円形であることを前提としています。


*/

package main

import (
	"fmt"
	"math"
)

// Point represents a 3D point with coordinates x, y, and t
type Point struct {
	X, Y, T float64
}

// DistanceTo calculates the Euclidean distance between two 3D points
func (p Point) DistanceTo(other Point) float64 {
	dx := p.X - other.X
	dy := p.Y - other.Y
	dt := p.T - other.T
	return math.Sqrt(dx*dx + dy*dy + dt*dt)
}

// Cluster represents a cluster of points
type Cluster struct {
	Points []Point
}

// DBSCAN performs density-based clustering of 3D points
func DBSCAN(points []Point, epsilon float64, minPts int) []Cluster {
	var clusters []Cluster
	var visited = make(map[Point]bool)

	for _, point := range points {
		if visited[point] {
			continue
		}
		visited[point] = true

		neighbours := getNeighbours(points, point, epsilon)
		if len(neighbours) < minPts {
			continue
		}

		var clusterPoints []Point
		expandCluster(&clusterPoints, points, visited, point, neighbours, epsilon, minPts)
		clusters = append(clusters, Cluster{Points: clusterPoints})
	}

	return clusters
}

// getNeighbours returns all points within distance epsilon of the given point
func getNeighbours(points []Point, point Point, epsilon float64) []Point {
	var neighbours []Point
	for _, other := range points {
		if point.DistanceTo(other) <= epsilon {
			neighbours = append(neighbours, other)
		}
	}
	return neighbours
}

// expandCluster expands the cluster from the given point
func expandCluster(cluster *[]Point, points []Point, visited map[Point]bool, point Point, neighbours []Point, epsilon float64, minPts int) {
	*cluster = append(*cluster, point)
	for _, neighbour := range neighbours {
		if !visited[neighbour] {
			visited[neighbour] = true
			neighbourNeighbours := getNeighbours(points, neighbour, epsilon)
			if len(neighbourNeighbours) >= minPts {
				expandCluster(cluster, points, visited, neighbour, neighbourNeighbours, epsilon, minPts)
			}
		}
		// Add neighbour to cluster if not already in another cluster
		var isInCluster bool
		for _, c := range *cluster {
			if c == neighbour {
				isInCluster = true
				break
			}
		}
		if !isInCluster {
			*cluster = append(*cluster, neighbour)
		}
	}
}

func main() {
	// Example usage
	points := []Point{
		{X: 1, Y: 2, T: 0},
		{X: 1.5, Y: 1.8, T: 1},
		{X: 5, Y: 8, T: 2},
		{X: 8, Y: 8, T: 3},
		{X: 1, Y: 0.6, T: 4},
		{X: 9, Y: 11, T: 5},
		{X: 8, Y: 2, T: 6},
		{X: 10, Y: 2, T: 7},
		{X: 9, Y: 3, T: 8},
	}

	epsilon := 3.0
	minPts := 2

	clusters := DBSCAN(points, epsilon, minPts)
	for i, cluster := range clusters {
		fmt.Printf("Cluster %d:\n", i+1)
		for _, point := range cluster.Points {
			fmt.Printf("  (%.2f, %.2f, %.2f)\n", point.X, point.Y, point.T)
		}
	}
}

2024,江端さんの技術メモ

を起動したら、

となることがあるので、この対応方法をメモしておきます。

https://app.mindmanager.com/ と入力(#my-filesはつけない)

ファイルが表示されるので、これを選択。

表示されるようになる。


のプロパティを開いて新しいURLをコピペする。

以上

2024,江端さんの技術メモ

本プログラムは、

Webサーバに繋っているブラウザが、全部いなくなったことを確認するプログラム

を拡張したものです。index.htmlは、このページに記載されているものを、そのまま、使って下さい。

こちらのプログラムでは、Webサーバに繋っているブラウザが、全部いなくなったと確認できた場合(30秒後)に、url = "http://localhost:8000/cancelAllVideoStreamGround"へメッセージを渡すプログラムです。

デバッグの為に使っていたコメントが残っていますので、適当に削除して下さい。

# c:\users\ebata\webMonitor.py
# https://wp.kobore.net/%e6%b1%9f%e7%ab%af%e3%81%95%e3%82%93%e3%81%ae%e6%8a%80%e8%a1%93%e3%83%a1%e3%83%a2/post-12475/を参照のこと

import threading
import time
import requests
import sys
from flask import Flask, request, jsonify
from requests.exceptions import Timeout
from flask_cors import CORS


app = Flask(__name__)

CORS(app)  # すべてのリクエストに対してCORSを有効にする

last_heartbeat_time = 0
lock = threading.Lock()

def send_notification():
    url = "http://localhost:8000/cancelAllVideoStreamGround"
    try:
        response = requests.post(url, timeout=3)
        response.raise_for_status()
        print("Notification sent successfully.")
        sys.stdout.flush()  # 標準出力をフラッシュする
    except Timeout:
        print("Error: Timeout while sending notification")
    except requests.exceptions.RequestException as e:
        print(f"Error sending notification: {e}")

def check_heartbeat():
    global last_heartbeat_time
    while True:
        current_time = time.time()
        print("ct:",current_time)
        print("lht:",last_heartbeat_time)
        diff = current_time - last_heartbeat_time
        print("diff:",diff)
        with lock:
            if current_time - last_heartbeat_time > 30:
                send_notification()
                last_heartbeat_time = current_time
        time.sleep(1)

@app.route('/heartbeat', methods=['POST'])
def receive_heartbeat():
    print("pass receive_heartbeat()")
    global last_heartbeat_time
    data = request.get_json()
    print(f"Received heartbeat: {data}")
    sys.stdout.flush()  # 標準出力をフラッシュする
    with lock:
        last_heartbeat_time = time.time()
        print("lht_2:",last_heartbeat_time)
    # data = request.get_json()
    # print(f"Received heartbeat: {data}")
    # sys.stdout.flush()  # 標準出力をフラッシュする
    return jsonify({"status": "OK"})

if __name__ == "__main__":
    heartbeat_thread = threading.Thread(target=check_heartbeat)
    heartbeat_thread.start()
    app.run(host='0.0.0.0', port=3000)

私用のコメント
(1)receive_heartbeat():と receive_heartbeat()を同時に走らせる為に、スレッド化しなければならなくなった → で Ctrl-Cで止められなくなったので、ちょっと困っている(が、シェル画面ごと落せばいいので、現在はそうしている)

(2)url = "http://localhost:8000/cancelAllVideoStreamGround" からの時間がもたつく場合には、Timeoutを設定すると良いかもしれない。send_notification()を丸ごと差し替えると、こんな感じ。

def send_notification():
    url = "http://localhost:8000/cancelAllVideoStreamGround"
    try:
        response = requests.post(url, timeout=3)
        response.raise_for_status()
        print("Notification sent successfully.")
        sys.stdout.flush()  # 標準出力をフラッシュする
    except Timeout:
        print("Error: Timeout while sending notification")
    except requests.exceptions.RequestException as e:
        print(f"Error sending notification: {e}")

(3)Webブラウザは、バックエンドに置いておくと、Heatbeatを出力しないことがあるので、常にアクティブにしておく必要があります(バックエンドのままにしておくと、cancelAllVideoStreamGroundが動き出します)

以上