175 lines
4.7 KiB
Go
175 lines
4.7 KiB
Go
// Package render converts video frames into annotated display/evidence images.
|
|||
|
|
package render
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"image"
|
||
|
|
"image/color"
|
||
|
|
"math"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"golang.org/x/image/font"
|
||
|
|
"golang.org/x/image/font/basicfont"
|
||
|
|
"golang.org/x/image/math/fixed"
|
||
|
|
|
||
|
|
"silverpose/v2/internal/fall"
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
normalColor = color.RGBA{R: 21, G: 128, B: 61, A: 255}
|
||
|
|
cautionColor = color.RGBA{R: 180, G: 83, B: 9, A: 255}
|
||
|
|
criticalColor = color.RGBA{R: 198, G: 40, B: 40, A: 255}
|
||
|
|
)
|
||
|
|
|
||
|
|
var skeletonEdges = [][2]int{
|
||
|
|
{0, 1}, {0, 2}, {1, 3}, {2, 4}, {5, 6}, {5, 7}, {7, 9}, {6, 8}, {8, 10},
|
||
|
|
{5, 11}, {6, 12}, {11, 12}, {11, 13}, {13, 15}, {12, 14}, {14, 16},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render returns a stand-alone RGBA frame. The renderer accepts no source URL
|
||
|
|
// or credential and can therefore be used both for the UI and screenshot evidence.
|
||
|
|
func Render(bgr []byte, width, height int, result fall.FrameResult, capturedAt time.Time) (*image.RGBA, error) {
|
||
|
|
if width <= 0 || height <= 0 || len(bgr) != width*height*3 {
|
||
|
|
return nil, fmt.Errorf("BGR frame length = %d, want %d", len(bgr), width*height*3)
|
||
|
|
}
|
||
|
|
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
|
|
for y := 0; y < height; y++ {
|
||
|
|
for x := 0; x < width; x++ {
|
||
|
|
source := (y*width + x) * 3
|
||
|
|
destination := canvas.PixOffset(x, y)
|
||
|
|
canvas.Pix[destination] = bgr[source+2]
|
||
|
|
canvas.Pix[destination+1] = bgr[source+1]
|
||
|
|
canvas.Pix[destination+2] = bgr[source]
|
||
|
|
canvas.Pix[destination+3] = 255
|
||
|
|
}
|
||
|
|
}
|
||
|
|
confirmed := false
|
||
|
|
for _, person := range result.People {
|
||
|
|
personColor := colorForState(person.State)
|
||
|
|
if person.State == fall.Confirmed {
|
||
|
|
confirmed = true
|
||
|
|
}
|
||
|
|
drawPerson(canvas, person, personColor, capturedAt)
|
||
|
|
}
|
||
|
|
if confirmed {
|
||
|
|
strokeRectangle(canvas, 0, 0, width-1, height-1, criticalColor, 6)
|
||
|
|
}
|
||
|
|
return canvas, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func drawPerson(canvas *image.RGBA, person fall.PersonAnalysis, lineColor color.RGBA, capturedAt time.Time) {
|
||
|
|
pose := person.TrackedPose.Pose
|
||
|
|
box := pose.Box
|
||
|
|
strokeRectangle(canvas, round(box.Left), round(box.Top), round(box.Right), round(box.Bottom), lineColor, 2)
|
||
|
|
for _, edge := range skeletonEdges {
|
||
|
|
first := pose.Keypoints[edge[0]]
|
||
|
|
second := pose.Keypoints[edge[1]]
|
||
|
|
if first.Confidence > 0 && second.Confidence > 0 {
|
||
|
|
drawLine(canvas, round(first.X), round(first.Y), round(second.X), round(second.Y), lineColor)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, point := range pose.Keypoints {
|
||
|
|
if point.Confidence > 0 {
|
||
|
|
drawDot(canvas, round(point.X), round(point.Y), lineColor)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
label := fmt.Sprintf("%s %s", person.TrackedPose.TrackID, person.State)
|
||
|
|
if !capturedAt.IsZero() {
|
||
|
|
label += " " + capturedAt.UTC().Format("15:04:05")
|
||
|
|
}
|
||
|
|
drawText(canvas, round(box.Left), max(12, round(box.Top)-4), label, lineColor)
|
||
|
|
}
|
||
|
|
|
||
|
|
func colorForState(state fall.State) color.RGBA {
|
||
|
|
switch state {
|
||
|
|
case fall.Confirmed:
|
||
|
|
return criticalColor
|
||
|
|
case fall.Suspect, fall.Recovering:
|
||
|
|
return cautionColor
|
||
|
|
default:
|
||
|
|
return normalColor
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func drawText(canvas *image.RGBA, x, y int, text string, textColor color.RGBA) {
|
||
|
|
drawer := &font.Drawer{
|
||
|
|
Dst: canvas,
|
||
|
|
Src: image.NewUniform(textColor),
|
||
|
|
Face: basicfont.Face7x13,
|
||
|
|
Dot: fixed.P(x, y),
|
||
|
|
}
|
||
|
|
drawer.DrawString(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
func strokeRectangle(canvas *image.RGBA, left, top, right, bottom int, lineColor color.RGBA, thickness int) {
|
||
|
|
if thickness < 1 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
for offset := 0; offset < thickness; offset++ {
|
||
|
|
drawLine(canvas, left+offset, top+offset, right-offset, top+offset, lineColor)
|
||
|
|
drawLine(canvas, left+offset, bottom-offset, right-offset, bottom-offset, lineColor)
|
||
|
|
drawLine(canvas, left+offset, top+offset, left+offset, bottom-offset, lineColor)
|
||
|
|
drawLine(canvas, right-offset, top+offset, right-offset, bottom-offset, lineColor)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func drawLine(canvas *image.RGBA, x0, y0, x1, y1 int, lineColor color.RGBA) {
|
||
|
|
dx := abs(x1 - x0)
|
||
|
|
sx := -1
|
||
|
|
if x0 < x1 {
|
||
|
|
sx = 1
|
||
|
|
}
|
||
|
|
dy := -abs(y1 - y0)
|
||
|
|
sy := -1
|
||
|
|
if y0 < y1 {
|
||
|
|
sy = 1
|
||
|
|
}
|
||
|
|
err := dx + dy
|
||
|
|
for {
|
||
|
|
setPixel(canvas, x0, y0, lineColor)
|
||
|
|
if x0 == x1 && y0 == y1 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
twiceError := 2 * err
|
||
|
|
if twiceError >= dy {
|
||
|
|
err += dy
|
||
|
|
x0 += sx
|
||
|
|
}
|
||
|
|
if twiceError <= dx {
|
||
|
|
err += dx
|
||
|
|
y0 += sy
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func drawDot(canvas *image.RGBA, x, y int, dotColor color.RGBA) {
|
||
|
|
for vertical := -2; vertical <= 2; vertical++ {
|
||
|
|
for horizontal := -2; horizontal <= 2; horizontal++ {
|
||
|
|
if horizontal*horizontal+vertical*vertical <= 4 {
|
||
|
|
setPixel(canvas, x+horizontal, y+vertical, dotColor)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func setPixel(canvas *image.RGBA, x, y int, pixel color.RGBA) {
|
||
|
|
if !image.Pt(x, y).In(canvas.Bounds()) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
canvas.SetRGBA(x, y, pixel)
|
||
|
|
}
|
||
|
|
|
||
|
|
func round(value float32) int { return int(math.Round(float64(value))) }
|
||
|
|
func abs(value int) int {
|
||
|
|
if value < 0 {
|
||
|
|
return -value
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
func max(left, right int) int {
|
||
|
|
if left > right {
|
||
|
|
return left
|
||
|
|
}
|
||
|
|
return right
|
||
|
|
}
|