package drawing import ( "fmt" "image" "image/color" "math" "time" "gocv.io/x/gocv" ) const ( CrossFlashFrames = 12 PopupLifetime = 20 LinePulseFrames = 12 CountPulseFrames = 15 BatchPulseFrames = 20 ) var skeleton = [][2]int{{0, 1}, {4, 3}, {1, 2}, {3, 2}, {2, 6}, {2, 5}, {2, 7}, {7, 8}} var skColors = []color.RGBA{ {0, 255, 255, 255}, {0, 255, 255, 255}, {255, 0, 255, 255}, {255, 0, 255, 255}, {0, 255, 0, 255}, {255, 255, 0, 255}, {0, 0, 255, 255}, {200, 200, 0, 255}, } var cPanel = color.RGBA{28, 24, 18, 255} var cBorder = color.RGBA{90, 85, 75, 255} var cAccent = color.RGBA{255, 200, 60, 255} var cGreen = color.RGBA{80, 220, 100, 255} var cText = color.RGBA{220, 220, 220, 255} var cMuted = color.RGBA{150, 150, 150, 255} var cAyamBox = color.RGBA{0, 165, 255, 255} var cTalenanBox = color.RGBA{220, 120, 60, 255} var cLineCore = color.RGBA{180, 220, 255, 255} var cLineGlow = color.RGBA{100, 160, 220, 255} type Popup struct { X, Y int Born int Text string } func ResolveLineX(w int, lineX *int, lineXFrac float64) int { if lineX != nil { return *lineX } if lineXFrac != 0.5 { return int(float64(w) * lineXFrac) } return w / 2 } func CrossedLine(prevCX, cx float64, lineX int, direction string) bool { lx := float64(lineX) switch direction { case "ltr": return prevCX < lx && lx <= cx case "both": return (prevCX > lx && lx >= cx) || (prevCX < lx && lx <= cx) default: return prevCX > lx && lx >= cx } } func OverlayRect(img *gocv.Mat, x1, y1, x2, y2 int, clr color.RGBA, alpha float64) { x1 = clampI(x1, 0, img.Cols()) y1 = clampI(y1, 0, img.Rows()) x2 = clampI(x2, 0, img.Cols()) y2 = clampI(y2, 0, img.Rows()) if x2 <= x1 || y2 <= y1 { return } roi := img.Region(image.Rect(x1, y1, x2, y2)) defer roi.Close() overlay := gocv.NewMatWithSize(y2-y1, x2-x1, gocv.MatTypeCV8UC3) defer overlay.Close() overlay.SetTo(gocv.NewScalar(float64(clr.B), float64(clr.G), float64(clr.R), 0)) gocv.AddWeighted(overlay, alpha, roi, 1-alpha, 0, &roi) } func DrawPill(img *gocv.Mat, text string, x, y int, bg color.RGBA) { fontScale := 0.45 size := gocv.GetTextSize(text, gocv.FontHersheySimplex, fontScale, 1) padX, padY := 6, 4 x1 := x y1 := y - size.Y - padY x2 := x + size.X + padX*2 y2 := y + padY - 1 gocv.Rectangle(img, image.Rect(x1, y1, x2, y2), bg, -1) gocv.Rectangle(img, image.Rect(x1, y1, x2, y2), cBorder, 1) gocv.PutText(img, text, image.Point{X: x + padX, Y: y}, gocv.FontHersheySimplex, fontScale, cText, 1) } func DrawCountingLine(img *gocv.Mat, lineX, h, pulse int) { strength := float64(pulse) / float64(max(LinePulseFrames, 1)) glowAlpha := 0.12 + 0.18*strength for _, offset := range []int{14, 9, 5} { alpha := uint8(glowAlpha * 255) clr := color.RGBA{cLineGlow.R, cLineGlow.G, cLineGlow.B, alpha} gocv.Line(img, image.Point{X: lineX - offset, Y: 0}, image.Point{X: lineX - offset, Y: h}, clr, 1) gocv.Line(img, image.Point{X: lineX + offset, Y: 0}, image.Point{X: lineX + offset, Y: h}, clr, 1) } dashLen, gap := 18, 12 for y := 0; y < h; y += dashLen + gap { yEnd := y + dashLen if yEnd > h { yEnd = h } gocv.Line(img, image.Point{X: lineX, Y: y}, image.Point{X: lineX, Y: yEnd}, cLineCore, 2) } gocv.PutText(img, "COUNT LINE", image.Point{X: lineX - 46, Y: 24}, gocv.FontHersheySimplex, 0.42, cLineCore, 1) } func DrawHeroCount(img *gocv.Mat, lineX, h, count, pulse int) { text := fmt.Sprintf("%d", count) boost := 0.35 * float64(pulse) / float64(max(CountPulseFrames, 1)) fontScale := 1.6 + boost thickness := 3 size := gocv.GetTextSize(text, gocv.FontHersheySimplex, fontScale, thickness) pad := 14 tx := lineX - size.X/2 ty := h/2 + size.Y/2 OverlayRect(img, tx-pad, ty-size.Y-pad, tx+size.X+pad, ty+pad/2, cPanel, 0.78) gocv.Rectangle(img, image.Rect(tx-pad, ty-size.Y-pad, tx+size.X+pad, ty+pad/2), cLineCore, 2) gocv.PutText(img, text, image.Point{X: tx, Y: ty}, gocv.FontHersheySimplex, fontScale, cGreen, thickness) } func DrawHUD(img *gocv.Mat, w int, batchNum, batchCount, totalAyam int, elapsedSec float64, rate float64, cameraID string) { barH := 52 OverlayRect(img, 0, 0, w, barH, cPanel, 0.72) gocv.Line(img, image.Point{X: 0, Y: barH}, image.Point{X: w, Y: barH}, cBorder, 1) gocv.PutText(img, "BATCH", image.Point{X: 16, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) batchLabel := "--" if batchNum > 0 { batchLabel = fmt.Sprintf("%d", batchNum) } gocv.PutText(img, batchLabel, image.Point{X: 16, Y: 44}, gocv.FontHersheySimplex, 0.9, cAccent, 2) gocv.PutText(img, "COUNT", image.Point{X: 100, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) gocv.PutText(img, fmt.Sprintf("%d", batchCount), image.Point{X: 100, Y: 44}, gocv.FontHersheySimplex, 0.9, cGreen, 2) gocv.PutText(img, "TOTAL", image.Point{X: 190, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) gocv.PutText(img, fmt.Sprintf("%d", totalAyam), image.Point{X: 190, Y: 44}, gocv.FontHersheySimplex, 0.7, cText, 1) gocv.PutText(img, "UPTIME", image.Point{X: 280, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) gocv.PutText(img, fmt.Sprintf("%.1fh", elapsedSec/3600), image.Point{X: 280, Y: 44}, gocv.FontHersheySimplex, 0.7, cText, 1) gocv.PutText(img, "RATE", image.Point{X: 380, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) gocv.PutText(img, fmt.Sprintf("%.1f/min", rate), image.Point{X: 380, Y: 44}, gocv.FontHersheySimplex, 0.7, cAccent, 1) gocv.PutText(img, fmt.Sprintf("CAM %s", cameraID), image.Point{X: w - 180, Y: 20}, gocv.FontHersheySimplex, 0.45, cMuted, 1) } func DrawFooter(img *gocv.Mat, w, h, frameIdx int, liveTag string) { barH := 28 OverlayRect(img, 0, h-barH, w, h, cPanel, 0.55) gocv.PutText(img, fmt.Sprintf("%s | Frame %d", liveTag, frameIdx), image.Point{X: 12, Y: h - 9}, gocv.FontHersheySimplex, 0.45, cMuted, 1) } func DrawSkeleton(img *gocv.Mat, kpts [][2]float64) { for i, pair := range skeleton { a, b := pair[0], pair[1] if a >= len(kpts) || b >= len(kpts) { continue } xa, ya := int(kpts[a][0]), int(kpts[a][1]) xb, yb := int(kpts[b][0]), int(kpts[b][1]) if xa > 0 && ya > 0 && xb > 0 && yb > 0 { clr := cGreen if i < len(skColors) { clr = skColors[i] } gocv.Line(img, image.Point{X: xa, Y: ya}, image.Point{X: xb, Y: yb}, clr, 3) } } for _, kp := range kpts { x, y := int(kp[0]), int(kp[1]) if x > 0 && y > 0 { gocv.Circle(img, image.Point{X: x, Y: y}, 6, color.RGBA{255, 255, 255, 255}, -1) gocv.Circle(img, image.Point{X: x, Y: y}, 6, color.RGBA{40, 40, 40, 255}, 2) } } } func DrawPopups(img *gocv.Mat, popups []Popup, frameIdx int) []Popup { var alive []Popup for _, pop := range popups { age := frameIdx - pop.Born if age > PopupLifetime { continue } alive = append(alive, pop) fade := 1.0 - float64(age)/PopupLifetime yy := pop.Y - int(float64(age)*1.8) clr := color.RGBA{ uint8(float64(cGreen.R) * fade), uint8(float64(cGreen.G) * fade), uint8(float64(cGreen.B) * fade), 255, } gocv.PutText(img, pop.Text, image.Point{X: pop.X, Y: yy}, gocv.FontHersheySimplex, 0.7, clr, 2) } return alive } func DrawBatchBanner(img *gocv.Mat, w, batchNum, pulse int) { if pulse <= 0 { return } text := fmt.Sprintf("NEW BATCH %d", batchNum) size := gocv.GetTextSize(text, gocv.FontHersheySimplex, 0.8, 2) x1 := w/2 - size.X/2 - 16 y1 := 62 x2 := w/2 + size.X/2 + 16 y2 := 62 + size.Y + 20 OverlayRect(img, x1, y1, x2, y2, cPanel, 0.7) gocv.Rectangle(img, image.Rect(x1, y1, x2, y2), cAccent, 2) gocv.PutText(img, text, image.Point{X: w/2 - size.X/2, Y: 62 + size.Y + 4}, gocv.FontHersheySimplex, 0.8, cAccent, 2) } func NowStr() string { return time.Now().Format("2006-01-02 15:04:05") } func clampI(v, lo, hi int) int { return int(math.Max(float64(lo), math.Min(float64(hi), float64(v)))) }