192 lines
5.4 KiB
Go
192 lines
5.4 KiB
Go
// Package monitor composes source, pose, fall and evidence work off the UI thread.
|
|
package monitor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"sync"
|
|
"time"
|
|
|
|
"silverpose/v2/internal/alert"
|
|
"silverpose/v2/internal/fall"
|
|
"silverpose/v2/internal/pose"
|
|
"silverpose/v2/internal/render"
|
|
"silverpose/v2/internal/source"
|
|
)
|
|
|
|
type FrameSource interface {
|
|
Frames() <-chan source.Frame
|
|
Statuses() <-chan source.Update
|
|
}
|
|
|
|
type PoseRuntime interface {
|
|
Run([]float32) ([]float32, error)
|
|
Close()
|
|
}
|
|
|
|
type EventProcessor interface {
|
|
Process(fall.Frame) fall.FrameResult
|
|
}
|
|
|
|
type Update struct {
|
|
Image *image.RGBA
|
|
Result fall.FrameResult
|
|
Events []alert.Record
|
|
SourceStatus source.Status
|
|
SourceMessage string
|
|
Sequence uint64
|
|
}
|
|
|
|
type Monitor struct {
|
|
source FrameSource
|
|
runtime PoseRuntime
|
|
processor EventProcessor
|
|
confidence float32
|
|
dispatcher *alert.Dispatcher
|
|
clock func() time.Time
|
|
updates chan Update
|
|
alerts chan alert.Record
|
|
done chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func New(
|
|
stream FrameSource,
|
|
runtime PoseRuntime,
|
|
processor EventProcessor,
|
|
confidence float32,
|
|
dispatcher *alert.Dispatcher,
|
|
clock func() time.Time,
|
|
) (*Monitor, error) {
|
|
if stream == nil || runtime == nil || processor == nil || dispatcher == nil {
|
|
return nil, fmt.Errorf("monitor dependencies must be non-nil")
|
|
}
|
|
if confidence < 0 || confidence > 1 {
|
|
return nil, fmt.Errorf("pose confidence must be between 0 and 1")
|
|
}
|
|
if clock == nil {
|
|
clock = time.Now
|
|
}
|
|
return &Monitor{
|
|
source: stream, runtime: runtime, processor: processor, confidence: confidence,
|
|
dispatcher: dispatcher, clock: clock, updates: make(chan Update, 1), alerts: make(chan alert.Record, 16), done: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func (monitor *Monitor) Updates() <-chan Update { return monitor.updates }
|
|
func (monitor *Monitor) Done() <-chan struct{} { return monitor.done }
|
|
func (monitor *Monitor) Alerts() <-chan alert.Record { return monitor.alerts }
|
|
|
|
// Run blocks until the source closes or the supplied context is cancelled.
|
|
// The caller owns the goroutine; all expensive work stays out of Walk's UI thread.
|
|
func (monitor *Monitor) Run(ctx context.Context) {
|
|
defer monitor.close()
|
|
defer monitor.runtime.Close()
|
|
frames := monitor.source.Frames()
|
|
statuses := monitor.source.Statuses()
|
|
currentStatus := source.Connecting
|
|
currentMessage := "正在连接视频流"
|
|
for frames != nil || statuses != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case status, open := <-statuses:
|
|
if !open {
|
|
statuses = nil
|
|
continue
|
|
}
|
|
currentStatus, currentMessage = status.Status, status.Message
|
|
monitor.publish(Update{SourceStatus: currentStatus, SourceMessage: currentMessage})
|
|
case frame, open := <-frames:
|
|
if !open {
|
|
frames = nil
|
|
continue
|
|
}
|
|
statuses, currentStatus, currentMessage = monitor.drainStatuses(statuses, currentStatus, currentMessage)
|
|
monitor.processFrame(frame, currentStatus, currentMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (monitor *Monitor) drainStatuses(statuses <-chan source.Update, current source.Status, message string) (<-chan source.Update, source.Status, string) {
|
|
for statuses != nil {
|
|
select {
|
|
case status, open := <-statuses:
|
|
if !open {
|
|
return nil, current, message
|
|
}
|
|
current, message = status.Status, status.Message
|
|
default:
|
|
return statuses, current, message
|
|
}
|
|
}
|
|
return nil, current, message
|
|
}
|
|
|
|
func (monitor *Monitor) processFrame(frame source.Frame, status source.Status, message string) {
|
|
input, transform, err := pose.PreprocessBGR(frame.BGR, frame.Width, frame.Height, 640)
|
|
if err != nil {
|
|
monitor.publish(Update{SourceStatus: status, SourceMessage: "视频帧格式无效", Sequence: frame.Sequence})
|
|
return
|
|
}
|
|
output, err := monitor.runtime.Run(input)
|
|
if err != nil {
|
|
monitor.publish(Update{SourceStatus: status, SourceMessage: "姿态推理失败", Sequence: frame.Sequence})
|
|
return
|
|
}
|
|
people, err := pose.ParseYOLOv8Pose(output, transform, monitor.confidence, 0.70)
|
|
if err != nil {
|
|
monitor.publish(Update{SourceStatus: status, SourceMessage: "姿态结果无效", Sequence: frame.Sequence})
|
|
return
|
|
}
|
|
result := monitor.processor.Process(fall.Frame{
|
|
Timestamp: frame.Timestamp.Seconds(), Width: frame.Width, Height: frame.Height, Poses: people,
|
|
})
|
|
capturedAt := monitor.clock().UTC()
|
|
canvas, err := render.Render(frame.BGR, frame.Width, frame.Height, result, capturedAt)
|
|
if err != nil {
|
|
monitor.publish(Update{Result: result, SourceStatus: status, SourceMessage: "画面渲染失败", Sequence: frame.Sequence})
|
|
return
|
|
}
|
|
records := make([]alert.Record, 0, len(result.Events))
|
|
for _, event := range result.Events {
|
|
record, err := monitor.dispatcher.Dispatch(event, canvas, capturedAt)
|
|
if err != nil {
|
|
monitor.publish(Update{Image: canvas, Result: result, SourceStatus: status, SourceMessage: "事件证据保存失败", Sequence: frame.Sequence})
|
|
return
|
|
}
|
|
if record.Written {
|
|
records = append(records, record)
|
|
monitor.alerts <- record
|
|
}
|
|
}
|
|
monitor.publish(Update{
|
|
Image: canvas, Result: result, Events: records, SourceStatus: status, SourceMessage: message, Sequence: frame.Sequence,
|
|
})
|
|
}
|
|
|
|
func (monitor *Monitor) publish(update Update) {
|
|
select {
|
|
case monitor.updates <- update:
|
|
return
|
|
default:
|
|
}
|
|
select {
|
|
case <-monitor.updates:
|
|
default:
|
|
}
|
|
select {
|
|
case monitor.updates <- update:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (monitor *Monitor) close() {
|
|
monitor.closeOnce.Do(func() {
|
|
close(monitor.updates)
|
|
close(monitor.alerts)
|
|
close(monitor.done)
|
|
})
|
|
}
|