110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/otiai10/gosseract/v2"
|
|
"gocv.io/x/gocv"
|
|
)
|
|
|
|
// Global reusable OCR client
|
|
var timestampOCRClient *gosseract.Client
|
|
|
|
// InitTimestampOCR initializes the reusable OCR client
|
|
func InitTimestampOCR() {
|
|
timestampOCRClient = gosseract.NewClient()
|
|
timestampOCRClient.SetPageSegMode(gosseract.PSM_SINGLE_LINE)
|
|
timestampOCRClient.SetWhitelist("0123456789-: ")
|
|
}
|
|
|
|
// CloseTimestampOCR closes the OCR client
|
|
func CloseTimestampOCR() {
|
|
if timestampOCRClient != nil {
|
|
timestampOCRClient.Close()
|
|
}
|
|
}
|
|
|
|
// extractTimestamp extracts and OCRs the timestamp from the top of the frame
|
|
// Returns: (secondsDifference, ocrDurationMs, error)
|
|
func extractTimestamp(frame gocv.Mat) (int, int64, error) {
|
|
startTime := time.Now()
|
|
|
|
// Extract 1024x68 region
|
|
timestampRegion := frame.Region(image.Rect(0, 0, 1024, 68))
|
|
defer timestampRegion.Close()
|
|
|
|
// Downscale 50% (reduces OCR processing time)
|
|
downscaled := gocv.NewMat()
|
|
defer downscaled.Close()
|
|
gocv.Resize(timestampRegion, &downscaled, image.Point{512, 34}, 0, 0, gocv.InterpolationArea)
|
|
|
|
// Grayscale
|
|
gray := gocv.NewMat()
|
|
defer gray.Close()
|
|
gocv.CvtColor(downscaled, &gray, gocv.ColorBGRToGray)
|
|
|
|
// Binary threshold (Otsu automatically finds best threshold)
|
|
binary := gocv.NewMat()
|
|
defer binary.Close()
|
|
gocv.Threshold(gray, &binary, 0, 255, gocv.ThresholdBinary|gocv.ThresholdOtsu)
|
|
|
|
// Invert (white text on black → black text on white for better OCR)
|
|
inverted := gocv.NewMat()
|
|
defer inverted.Close()
|
|
gocv.BitwiseNot(binary, &inverted)
|
|
|
|
// Encode as PNG (lossless, better for text)
|
|
buf, err := gocv.IMEncode(".png", inverted)
|
|
if err != nil {
|
|
return 0, time.Since(startTime).Milliseconds(), fmt.Errorf("failed to encode: %w", err)
|
|
}
|
|
defer buf.Close()
|
|
|
|
// OCR with REUSED client (major speed boost)
|
|
err = timestampOCRClient.SetImageFromBytes(buf.GetBytes())
|
|
if err != nil {
|
|
return 0, time.Since(startTime).Milliseconds(), fmt.Errorf("failed to set image: %w", err)
|
|
}
|
|
|
|
text, err := timestampOCRClient.Text()
|
|
if err != nil {
|
|
return 0, time.Since(startTime).Milliseconds(), fmt.Errorf("OCR failed: %w", err)
|
|
}
|
|
|
|
// Parse timestamp
|
|
text = strings.TrimSpace(text)
|
|
text = strings.ReplaceAll(text, "\n", "")
|
|
|
|
// Pattern: YYYY-MM-DD HH:MM:SS
|
|
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})`)
|
|
matches := re.FindStringSubmatch(text)
|
|
|
|
if len(matches) != 7 {
|
|
return 0, time.Since(startTime).Milliseconds(), fmt.Errorf("could not parse timestamp from: '%s'", text)
|
|
}
|
|
|
|
// Parse components
|
|
year, _ := strconv.Atoi(matches[1])
|
|
month, _ := strconv.Atoi(matches[2])
|
|
day, _ := strconv.Atoi(matches[3])
|
|
hour, _ := strconv.Atoi(matches[4])
|
|
minute, _ := strconv.Atoi(matches[5])
|
|
second, _ := strconv.Atoi(matches[6])
|
|
|
|
// Create timestamp in local timezone
|
|
cameraTime := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
|
|
serverTime := time.Now()
|
|
|
|
// Calculate difference
|
|
diff := int(serverTime.Sub(cameraTime).Seconds())
|
|
|
|
ocrDuration := time.Since(startTime).Milliseconds()
|
|
|
|
return diff, ocrDuration, nil
|
|
}
|