Compare commits
4 commits
6dcf46c597
...
e22c33b78e
Author | SHA1 | Date | |
---|---|---|---|
e22c33b78e | |||
4699841f1e | |||
f4d7a1a04c | |||
5084fad903 |
3 changed files with 414 additions and 43 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
fps-go-brr
|
128
CLAUDE.md
Normal file
128
CLAUDE.md
Normal file
|
@ -0,0 +1,128 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Go CLI application for professional video frame analysis and comparison. The project provides tools to count frames in videos, compare individual frames, analyze differences between two videos, and perform comprehensive frame persistence analysis for single videos with DigitalFoundry-style CSV output.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
The application is built using:
|
||||
- **CLI Framework**: urfave/cli/v3 for command-line interface
|
||||
- **Video Processing**: AlexEidt/Vidio library for video file handling with FPS detection
|
||||
- **Image Processing**: Standard Go image libraries for frame comparison
|
||||
- **CSV Export**: Built-in CSV generation for professional video analysis visualization
|
||||
|
||||
### Main Components
|
||||
|
||||
- **CLI Commands**: Five main commands for comprehensive frame analysis operations
|
||||
- **Frame Comparison**: Pixel-level comparison with configurable tolerance using squared difference
|
||||
- **Video Processing**: Frame-by-frame video analysis with streaming support and memory-efficient processing
|
||||
- **Two-Pass Analysis**: Advanced frame persistence analysis with pre-calculated total durations
|
||||
- **CSV Generation**: DigitalFoundry-style data export for professional visualization tools
|
||||
|
||||
### Key Functions
|
||||
|
||||
- `count_video_frames()`: Counts total frames in a video file
|
||||
- `compare_frames()`: Compares two frames with tolerance-based difference detection
|
||||
- `compare_frames_alt()`: Alternative frame comparison using exact pixel matching
|
||||
- `countUniqueVideoFrames()`: Analyzes differences between corresponding frames in two videos
|
||||
- `analyzeFramePersistence()`: **Main feature** - Two-pass frame persistence analysis with CSV export
|
||||
- `isDiffUInt8WithTolerance()`: Pixel comparison with configurable tolerance threshold
|
||||
- `imageToRGBA()`: Converts images to RGBA format for consistent processing
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Build and Run
|
||||
```bash
|
||||
go build -o fps-go-brr .
|
||||
./fps-go-brr <command> [args]
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Module Management
|
||||
```bash
|
||||
go mod tidy
|
||||
go mod download
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
Available commands:
|
||||
- `count-frames <video>` - Count frames in a video
|
||||
- `compare-frames <frame1> <frame2>` - Compare two image frames
|
||||
- `count-frames-differing-pixels <frame1> <frame2>` - Count pixel differences between frames
|
||||
- `count-unique-video-frames <video1> <video2>` - Compare corresponding frames between two videos
|
||||
- `analyze-frame-persistence [--tolerance float] [--csv-output path] <video>` - **Main feature**: Professional video analysis with CSV export
|
||||
|
||||
### Frame Persistence Analysis with CSV Export
|
||||
|
||||
The main feature provides:
|
||||
- Real-time FPS detection from video metadata
|
||||
- Frame-by-frame comparison with previous frame
|
||||
- Detection of consecutive duplicate frame sequences (3+ identical frames)
|
||||
- Per-second unique frame counting
|
||||
- Two-pass analysis for accurate total frame persistence calculation
|
||||
- Configurable pixel difference tolerance (0-255)
|
||||
- **Professional CSV export** with 5 columns for DigitalFoundry-style analysis
|
||||
|
||||
### CSV Output Format
|
||||
|
||||
The `--csv-output` flag generates a CSV file with these columns:
|
||||
- `frame`: Frame number (1-based, no skipped frames)
|
||||
- `average_fps`: Running effective FPS calculation
|
||||
- `frame_time`: Current frame persistence duration (real-time)
|
||||
- `unique_frame_count`: Cumulative unique frame count (stays constant during duplicates)
|
||||
- `real_frame_time`: **Total persistence time for each unique frame (smooth for visualization)**
|
||||
|
||||
### CSV Usage Examples
|
||||
|
||||
```bash
|
||||
# Basic analysis with CSV export
|
||||
./fps-go-brr analyze-frame-persistence video.mp4 --csv-output analysis.csv
|
||||
|
||||
# With tolerance for noisy videos
|
||||
./fps-go-brr analyze-frame-persistence video.mp4 --tolerance 10 --csv-output analysis.csv
|
||||
```
|
||||
|
||||
## Advanced Implementation Details
|
||||
|
||||
### Two-Pass Analysis Architecture
|
||||
|
||||
The `analyzeFramePersistence()` function uses a sophisticated two-pass approach:
|
||||
|
||||
1. **First Pass**: Analyzes entire video to calculate total duration each unique frame will persist
|
||||
2. **Second Pass**: Writes CSV with correct `real_frame_time` values for smooth visualization
|
||||
|
||||
This ensures:
|
||||
- All instances of the same unique frame show identical `real_frame_time` values
|
||||
- Creates smooth, non-jumpy graphs perfect for professional video analysis
|
||||
- DigitalFoundry-style frame timing visualization compatibility
|
||||
|
||||
### Frame Data Structure
|
||||
|
||||
```go
|
||||
type FrameData struct {
|
||||
frameNumber int // Current frame number
|
||||
uniqueFrameCount int // Cumulative unique frames
|
||||
effectiveFPS float64 // Running average FPS
|
||||
currentFrameTime float64 // Current persistence so far
|
||||
realFrameTime float64 // Total persistence duration
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The application processes video frames in memory using RGBA format
|
||||
- Pixel comparison uses squared difference for tolerance-based matching
|
||||
- Video processing is done frame-by-frame to handle large files efficiently
|
||||
- Frame persistence detection only reports sequences of 3+ consecutive identical frames
|
||||
- Two-pass analysis ensures accurate total persistence calculations for visualization
|
||||
- CSV output is optimized for professional video analysis tools and graphing software
|
||||
- The `analyze-frame-persistence` command is the primary tool for professional video quality analysis
|
||||
- All image formats supported by Go's image package can be used for frame comparison
|
328
main.go
328
main.go
|
@ -2,6 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"log"
|
||||
|
@ -19,7 +21,7 @@ func main() {
|
|||
Name: "count-frames",
|
||||
Usage: "Count frames",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
// fmt.Println("added task: ", cmd.Args().First())
|
||||
|
||||
return count_video_frames(cmd.Args().First())
|
||||
},
|
||||
},
|
||||
|
@ -36,7 +38,7 @@ func main() {
|
|||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
// fmt.Println("added task: ", cmd.Args().First())
|
||||
|
||||
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
||||
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
||||
|
||||
|
@ -58,7 +60,7 @@ func main() {
|
|||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
// fmt.Println("added task: ", cmd.Args().First())
|
||||
|
||||
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
||||
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
||||
|
||||
|
@ -80,7 +82,33 @@ func main() {
|
|||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return countUniqueVideoFrames(cmd.StringArg("video1"), cmd.StringArg("video2"), 1)
|
||||
return countUniqueVideoFrames(cmd.StringArg("video1"), cmd.StringArg("video2"), 1, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "analyze-frame-persistence",
|
||||
Usage: "Analyze frame persistence in a single video",
|
||||
Arguments: []cli.Argument{
|
||||
&cli.StringArg{
|
||||
Name: "video",
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.Float64Flag{
|
||||
Name: "tolerance",
|
||||
Usage: "Pixel difference tolerance (0-255)",
|
||||
Value: 0,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "csv-output",
|
||||
Usage: "Path to CSV file for frame data output",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
tolerance := uint64(cmd.Float64("tolerance"))
|
||||
csvOutput := cmd.String("csv-output")
|
||||
return analyzeFramePersistence(cmd.StringArg("video"), tolerance, csvOutput)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -111,32 +139,26 @@ func count_video_frames(video string) error {
|
|||
}
|
||||
|
||||
func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
||||
// diff_frame := image.NewRGBA(frame1.Rect)
|
||||
|
||||
accumError := int64(0)
|
||||
|
||||
for i := 0; i < len(frame1.Pix); i++ {
|
||||
accumError += int64(sqDiffUInt8(frame1.Pix[i], frame2.Pix[i]))
|
||||
|
||||
if isDiffUInt8WithTolerance(frame1.Pix[i], frame2.Pix[i], 0) { // Set tolerance to 0
|
||||
accumError++
|
||||
}
|
||||
}
|
||||
|
||||
log.Default().Println("Total image error: " + strconv.FormatInt(accumError, 10))
|
||||
|
||||
log.Default().Println("Total differing pixels: " + strconv.FormatInt(accumError, 10))
|
||||
return nil
|
||||
}
|
||||
|
||||
func compare_frames_alt(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
||||
// diff_frame := image.NewRGBA(frame1.Rect)
|
||||
|
||||
accumError := int64(0)
|
||||
|
||||
for i := 0; i < len(frame1.Pix); i++ {
|
||||
if isDiffUInt8(frame1.Pix[i], frame2.Pix[i]) {
|
||||
accumError++
|
||||
}
|
||||
}
|
||||
|
||||
log.Default().Println("Total differing pixels: " + strconv.FormatInt(accumError, 10))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -148,7 +170,6 @@ func sqDiffUInt8(x, y uint8) uint64 {
|
|||
func isDiffUInt8(x, y uint8) bool {
|
||||
d := uint64(x) - uint64(y)
|
||||
sq := d * d
|
||||
|
||||
if sq > 0 {
|
||||
return true
|
||||
} else {
|
||||
|
@ -156,6 +177,53 @@ func isDiffUInt8(x, y uint8) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
|
||||
d := uint64(x) - uint64(y)
|
||||
sq := d * d
|
||||
if sq > tolerance {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func countUniqueVideoFrames(video_path1 string, video_path2 string, min_diff uint64, use_sq_diff bool) error {
|
||||
video1, _ := vidio.NewVideo(video_path1)
|
||||
video2, _ := vidio.NewVideo(video_path2)
|
||||
video1_frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
|
||||
video2_frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
|
||||
video1.SetFrameBuffer(video1_frame.Pix)
|
||||
video2.SetFrameBuffer(video2_frame.Pix)
|
||||
total_frames := 0
|
||||
unique_frames := 0
|
||||
for video1.Read() {
|
||||
total_frames++
|
||||
video2.Read()
|
||||
accumError := uint64(0)
|
||||
for i := 0; i < len(video1_frame.Pix); i++ {
|
||||
if use_sq_diff {
|
||||
if isDiffUInt8WithTolerance(video1_frame.Pix[i], video2_frame.Pix[i], min_diff) {
|
||||
accumError++
|
||||
}
|
||||
} else {
|
||||
if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
|
||||
accumError++
|
||||
}
|
||||
}
|
||||
}
|
||||
if min_diff <= accumError {
|
||||
unique_frames++
|
||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
|
||||
} else {
|
||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
|
||||
}
|
||||
}
|
||||
video1.Close()
|
||||
video2.Close()
|
||||
log.Default().Println(strconv.Itoa(unique_frames) + "/" + strconv.Itoa(total_frames) + " are unique!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageToRGBA(src image.Image) *image.RGBA {
|
||||
|
||||
// No conversion needed if image is an *image.RGBA.
|
||||
|
@ -180,41 +248,215 @@ func getImageFromFilePath(filePath string) (image.Image, error) {
|
|||
return image, err
|
||||
}
|
||||
|
||||
func countUniqueVideoFrames(video_path1 string, video_path2 string, min_diff int64) error {
|
||||
video1, _ := vidio.NewVideo(video_path1)
|
||||
video2, _ := vidio.NewVideo(video_path2)
|
||||
func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput string) error {
|
||||
video, err := vidio.NewVideo(videoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer video.Close()
|
||||
|
||||
video1_frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
|
||||
video2_frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
|
||||
video1.SetFrameBuffer(video1_frame.Pix)
|
||||
video2.SetFrameBuffer(video2_frame.Pix)
|
||||
fps := video.FPS()
|
||||
frameTimeMs := 1000.0 / fps
|
||||
|
||||
total_frames := 0
|
||||
unique_frames := 0
|
||||
for video1.Read() {
|
||||
total_frames++
|
||||
video2.Read()
|
||||
log.Default().Printf("Video FPS: %.2f, Frame time: %.2f ms", fps, frameTimeMs)
|
||||
|
||||
accumError := int64(0)
|
||||
|
||||
for i := 0; i < len(video1_frame.Pix); i++ {
|
||||
if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
|
||||
accumError++
|
||||
}
|
||||
var csvWriter *csv.Writer
|
||||
var csvFile *os.File
|
||||
if csvOutput != "" {
|
||||
csvFile, err = os.Create(csvOutput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create CSV file: %v", err)
|
||||
}
|
||||
|
||||
if min_diff <= accumError {
|
||||
unique_frames++
|
||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
|
||||
} else {
|
||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
|
||||
defer csvFile.Close()
|
||||
|
||||
csvWriter = csv.NewWriter(csvFile)
|
||||
defer csvWriter.Flush()
|
||||
|
||||
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write CSV header: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
video1.Close()
|
||||
video2.Close()
|
||||
// Data structures for frame analysis
|
||||
type FrameData struct {
|
||||
frameNumber int
|
||||
uniqueFrameCount int
|
||||
effectiveFPS float64
|
||||
currentFrameTime float64
|
||||
realFrameTime float64
|
||||
}
|
||||
|
||||
var frameAnalysisData []FrameData
|
||||
var uniqueFrameDurations []int // Duration of each unique frame
|
||||
|
||||
currentFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
|
||||
previousFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
|
||||
video.SetFrameBuffer(currentFrame.Pix)
|
||||
|
||||
log.Default().Println(strconv.Itoa(unique_frames) + "/" + strconv.Itoa(total_frames) + " are unique!")
|
||||
// FIRST PASS: Analyze frame durations
|
||||
var frameNumber int
|
||||
var uniqueFramesPerSecond []int
|
||||
var framePersistenceDurations []float64
|
||||
|
||||
currentSecond := 0
|
||||
uniqueFramesInCurrentSecond := 0
|
||||
consecutiveDuplicateCount := 0
|
||||
totalUniqueFrames := 0
|
||||
currentUniqueFrameDuration := 1
|
||||
|
||||
hasFirstFrame := false
|
||||
|
||||
for video.Read() {
|
||||
frameNumber++
|
||||
|
||||
if !hasFirstFrame {
|
||||
copy(previousFrame.Pix, currentFrame.Pix)
|
||||
hasFirstFrame = true
|
||||
uniqueFramesInCurrentSecond = 1
|
||||
totalUniqueFrames = 1
|
||||
currentUniqueFrameDuration = 1
|
||||
|
||||
// Store data for first frame
|
||||
currentTime := float64(frameNumber) / fps
|
||||
effectiveFPS := float64(totalUniqueFrames) / currentTime
|
||||
actualFrameTimeMs := float64(currentUniqueFrameDuration) * frameTimeMs
|
||||
frameAnalysisData = append(frameAnalysisData, FrameData{
|
||||
frameNumber: frameNumber,
|
||||
uniqueFrameCount: totalUniqueFrames,
|
||||
effectiveFPS: effectiveFPS,
|
||||
currentFrameTime: actualFrameTimeMs,
|
||||
realFrameTime: 0, // Will be calculated in second pass
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
isFrameDifferent := false
|
||||
pixelDifferences := uint64(0)
|
||||
|
||||
for i := 0; i < len(currentFrame.Pix); i++ {
|
||||
if isDiffUInt8WithTolerance(currentFrame.Pix[i], previousFrame.Pix[i], tolerance) {
|
||||
pixelDifferences++
|
||||
}
|
||||
}
|
||||
|
||||
if pixelDifferences > 0 {
|
||||
isFrameDifferent = true
|
||||
}
|
||||
|
||||
if !isFrameDifferent {
|
||||
consecutiveDuplicateCount++
|
||||
currentUniqueFrameDuration++
|
||||
} else {
|
||||
// Record the duration of the previous unique frame
|
||||
if totalUniqueFrames > 0 {
|
||||
if len(uniqueFrameDurations) < totalUniqueFrames {
|
||||
uniqueFrameDurations = append(uniqueFrameDurations, currentUniqueFrameDuration)
|
||||
} else {
|
||||
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
|
||||
}
|
||||
}
|
||||
|
||||
if consecutiveDuplicateCount > 1 {
|
||||
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
|
||||
framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
|
||||
log.Default().Printf("Frame persisted for %.2f ms (%d consecutive duplicates)", persistenceMs, consecutiveDuplicateCount)
|
||||
}
|
||||
consecutiveDuplicateCount = 0
|
||||
|
||||
uniqueFramesInCurrentSecond++
|
||||
totalUniqueFrames++
|
||||
copy(previousFrame.Pix, currentFrame.Pix)
|
||||
|
||||
// Start tracking new unique frame
|
||||
currentUniqueFrameDuration = 1
|
||||
}
|
||||
|
||||
// Store data for EVERY frame
|
||||
currentTime := float64(frameNumber) / fps
|
||||
effectiveFPS := float64(totalUniqueFrames) / currentTime
|
||||
actualFrameTimeMs := float64(currentUniqueFrameDuration) * frameTimeMs
|
||||
frameAnalysisData = append(frameAnalysisData, FrameData{
|
||||
frameNumber: frameNumber,
|
||||
uniqueFrameCount: totalUniqueFrames,
|
||||
effectiveFPS: effectiveFPS,
|
||||
currentFrameTime: actualFrameTimeMs,
|
||||
realFrameTime: 0, // Will be calculated in second pass
|
||||
})
|
||||
|
||||
newSecond := int(float64(frameNumber-1) / fps)
|
||||
if newSecond > currentSecond {
|
||||
uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond)
|
||||
log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond)
|
||||
currentSecond = newSecond
|
||||
uniqueFramesInCurrentSecond = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Record the final unique frame duration
|
||||
if totalUniqueFrames > 0 {
|
||||
if len(uniqueFrameDurations) < totalUniqueFrames {
|
||||
uniqueFrameDurations = append(uniqueFrameDurations, currentUniqueFrameDuration)
|
||||
} else {
|
||||
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND PASS: Calculate real frame times and write CSV
|
||||
if csvWriter != nil {
|
||||
for i, frameData := range frameAnalysisData {
|
||||
realFrameTimeMs := float64(uniqueFrameDurations[frameData.uniqueFrameCount-1]) * frameTimeMs
|
||||
err := csvWriter.Write([]string{
|
||||
strconv.Itoa(frameData.frameNumber),
|
||||
fmt.Sprintf("%.2f", frameData.effectiveFPS),
|
||||
fmt.Sprintf("%.2f", frameData.currentFrameTime),
|
||||
strconv.Itoa(frameData.uniqueFrameCount),
|
||||
fmt.Sprintf("%.2f", realFrameTimeMs),
|
||||
})
|
||||
if err != nil {
|
||||
log.Default().Printf("Warning: failed to write CSV row %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if consecutiveDuplicateCount > 1 {
|
||||
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
|
||||
framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
|
||||
log.Default().Printf("Final frame persisted for %.2f ms (%d consecutive duplicates)", persistenceMs, consecutiveDuplicateCount)
|
||||
}
|
||||
|
||||
if uniqueFramesInCurrentSecond > 0 {
|
||||
uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond)
|
||||
log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond)
|
||||
}
|
||||
|
||||
log.Default().Printf("\n=== SUMMARY ===")
|
||||
log.Default().Printf("Total frames analyzed: %d", frameNumber)
|
||||
log.Default().Printf("Video duration: %.2f seconds", float64(frameNumber)/fps)
|
||||
|
||||
summaryUniqueFrames := 0
|
||||
for i, count := range uniqueFramesPerSecond {
|
||||
summaryUniqueFrames += count
|
||||
log.Default().Printf("Second %d: %d unique frames", i+1, count)
|
||||
}
|
||||
|
||||
log.Default().Printf("Total unique frames: %d", summaryUniqueFrames)
|
||||
if len(uniqueFramesPerSecond) > 0 {
|
||||
log.Default().Printf("Average unique frames per second: %.2f", float64(summaryUniqueFrames)/float64(len(uniqueFramesPerSecond)))
|
||||
}
|
||||
|
||||
if len(framePersistenceDurations) > 0 {
|
||||
log.Default().Printf("\nFrame persistence durations:")
|
||||
totalPersistence := 0.0
|
||||
for _, duration := range framePersistenceDurations {
|
||||
totalPersistence += duration
|
||||
}
|
||||
avgPersistence := totalPersistence / float64(len(framePersistenceDurations))
|
||||
log.Default().Printf("Average frame persistence: %.2f ms", avgPersistence)
|
||||
log.Default().Printf("Number of persistence events: %d", len(framePersistenceDurations))
|
||||
} else {
|
||||
log.Default().Printf("No frame persistence detected (all frames are unique)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue