Compare commits
5 commits
8ba0319c6e
...
c6755cb585
Author | SHA1 | Date | |
---|---|---|---|
c6755cb585 | |||
aec0f14c68 | |||
3529f7e415 | |||
f9575c92a9 | |||
1b4cd880ef |
4 changed files with 61 additions and 48 deletions
2
.cz.toml
2
.cz.toml
|
@ -2,6 +2,6 @@
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
tag_format = "$version"
|
tag_format = "$version"
|
||||||
version_scheme = "semver2"
|
version_scheme = "semver2"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
update_changelog_on_bump = true
|
update_changelog_on_bump = true
|
||||||
major_version_zero = true
|
major_version_zero = true
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
fps-go-brr
|
fps-go-brr
|
||||||
fps-go-brr-compact
|
fps-go-brr-compact
|
||||||
|
.vscode/settings.json
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
## 0.3.0 (2025-06-15)
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- change variable and function names to fit with golang conventions
|
||||||
|
- remove pointless else statments
|
||||||
|
- add package string
|
||||||
|
- **gitignore**: ignore compact build binary
|
||||||
|
|
||||||
## 0.2.0 (2025-06-14)
|
## 0.2.0 (2025-06-14)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
97
main.go
97
main.go
|
@ -1,3 +1,4 @@
|
||||||
|
// A Go CLI tool for video frame analysis and comparison. Analyze frame persistence, detect dropped frames, and export data for visualization tools like those used by Digital Foundry.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -22,7 +23,7 @@ func main() {
|
||||||
Usage: "Count frames",
|
Usage: "Count frames",
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
|
||||||
return count_video_frames(cmd.Args().First())
|
return countVideoFrames(cmd.Args().First())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -39,12 +40,12 @@ func main() {
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
|
||||||
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
||||||
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
||||||
|
|
||||||
first_rgba := imageToRGBA(first_frame)
|
firstRGBA := imageToRGBA(firstFrame)
|
||||||
second_rgba := imageToRGBA(second_frame)
|
secondRGBA := imageToRGBA(secondFrame)
|
||||||
return compare_frames(first_rgba, second_rgba)
|
return compareFrames(firstRGBA, secondRGBA)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -61,12 +62,12 @@ func main() {
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
|
||||||
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
|
||||||
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
|
||||||
|
|
||||||
first_rgba := imageToRGBA(first_frame)
|
firstRGBA := imageToRGBA(firstFrame)
|
||||||
second_rgba := imageToRGBA(second_frame)
|
secondRGBA := imageToRGBA(secondFrame)
|
||||||
return compare_frames_alt(first_rgba, second_rgba)
|
return compareFramesAlt(firstRGBA, secondRGBA)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -119,7 +120,7 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// count_video_frames
|
// countVideoFrames
|
||||||
// Prints out the total ammount of frames within `video`
|
// Prints out the total ammount of frames within `video`
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
@ -127,18 +128,18 @@ func main() {
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - error
|
// - error
|
||||||
func count_video_frames(video string) error {
|
func countVideoFrames(video string) error {
|
||||||
log.Default().Print("Trying to open video at: " + video)
|
log.Default().Print("Trying to open video at: " + video)
|
||||||
video_file, _ := vidio.NewVideo(video)
|
videoFile, _ := vidio.NewVideo(video)
|
||||||
count := 0
|
count := 0
|
||||||
for video_file.Read() {
|
for videoFile.Read() {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
log.Default().Println("Video total frames: " + strconv.Itoa(count))
|
log.Default().Println("Video total frames: " + strconv.Itoa(count))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
||||||
accumError := int64(0)
|
accumError := int64(0)
|
||||||
for i := 0; i < len(frame1.Pix); i++ {
|
for i := 0; i < len(frame1.Pix); i++ {
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compare_frames_alt(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
func compareFramesAlt(frame1 *image.RGBA, frame2 *image.RGBA) error {
|
||||||
// diff_frame := image.NewRGBA(frame1.Rect)
|
// diff_frame := image.NewRGBA(frame1.Rect)
|
||||||
accumError := int64(0)
|
accumError := int64(0)
|
||||||
for i := 0; i < len(frame1.Pix); i++ {
|
for i := 0; i < len(frame1.Pix); i++ {
|
||||||
|
@ -172,9 +173,10 @@ func isDiffUInt8(x, y uint8) bool {
|
||||||
sq := d * d
|
sq := d * d
|
||||||
if sq > 0 {
|
if sq > 0 {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
|
func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
|
||||||
|
@ -182,45 +184,46 @@ func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
|
||||||
sq := d * d
|
sq := d * d
|
||||||
if sq > tolerance {
|
if sq > tolerance {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func countUniqueVideoFrames(video_path1 string, video_path2 string, min_diff uint64, use_sq_diff bool) error {
|
func countUniqueVideoFrames(videoPath1 string, videoPath2 string, minDiff uint64, useSqDiff bool) error {
|
||||||
video1, _ := vidio.NewVideo(video_path1)
|
video1, _ := vidio.NewVideo(videoPath1)
|
||||||
video2, _ := vidio.NewVideo(video_path2)
|
video2, _ := vidio.NewVideo(videoPath2)
|
||||||
video1_frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
|
video1Frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
|
||||||
video2_frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
|
video2Frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
|
||||||
video1.SetFrameBuffer(video1_frame.Pix)
|
video1.SetFrameBuffer(video1Frame.Pix)
|
||||||
video2.SetFrameBuffer(video2_frame.Pix)
|
video2.SetFrameBuffer(video2Frame.Pix)
|
||||||
total_frames := 0
|
totalFrames := 0
|
||||||
unique_frames := 0
|
uniqueFrames := 0
|
||||||
for video1.Read() {
|
for video1.Read() {
|
||||||
total_frames++
|
totalFrames++
|
||||||
video2.Read()
|
video2.Read()
|
||||||
accumError := uint64(0)
|
accumError := uint64(0)
|
||||||
for i := 0; i < len(video1_frame.Pix); i++ {
|
for i := 0; i < len(video1Frame.Pix); i++ {
|
||||||
if use_sq_diff {
|
if useSqDiff {
|
||||||
if isDiffUInt8WithTolerance(video1_frame.Pix[i], video2_frame.Pix[i], min_diff) {
|
if isDiffUInt8WithTolerance(video1Frame.Pix[i], video2Frame.Pix[i], minDiff) {
|
||||||
accumError++
|
accumError++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
|
if isDiffUInt8(video1Frame.Pix[i], video2Frame.Pix[i]) {
|
||||||
accumError++
|
accumError++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if min_diff <= accumError {
|
if minDiff <= accumError {
|
||||||
unique_frames++
|
uniqueFrames++
|
||||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
|
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Unique frame")
|
||||||
} else {
|
} else {
|
||||||
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
|
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Non-unique frame")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
video1.Close()
|
video1.Close()
|
||||||
video2.Close()
|
video2.Close()
|
||||||
log.Default().Println(strconv.Itoa(unique_frames) + "/" + strconv.Itoa(total_frames) + " are unique!")
|
log.Default().Println(strconv.Itoa(uniqueFrames) + "/" + strconv.Itoa(totalFrames) + " are unique!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,10 +271,10 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
|
||||||
return fmt.Errorf("failed to create CSV file: %v", err)
|
return fmt.Errorf("failed to create CSV file: %v", err)
|
||||||
}
|
}
|
||||||
defer csvFile.Close()
|
defer csvFile.Close()
|
||||||
|
|
||||||
csvWriter = csv.NewWriter(csvFile)
|
csvWriter = csv.NewWriter(csvFile)
|
||||||
defer csvWriter.Flush()
|
defer csvWriter.Flush()
|
||||||
|
|
||||||
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
|
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write CSV header: %v", err)
|
return fmt.Errorf("failed to write CSV header: %v", err)
|
||||||
|
@ -286,10 +289,10 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
|
||||||
currentFrameTime float64
|
currentFrameTime float64
|
||||||
realFrameTime float64
|
realFrameTime float64
|
||||||
}
|
}
|
||||||
|
|
||||||
var frameAnalysisData []FrameData
|
var frameAnalysisData []FrameData
|
||||||
var uniqueFrameDurations []int // Duration of each unique frame
|
var uniqueFrameDurations []int // Duration of each unique frame
|
||||||
|
|
||||||
currentFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
|
currentFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
|
||||||
previousFrame := 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)
|
video.SetFrameBuffer(currentFrame.Pix)
|
||||||
|
@ -316,7 +319,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
|
||||||
uniqueFramesInCurrentSecond = 1
|
uniqueFramesInCurrentSecond = 1
|
||||||
totalUniqueFrames = 1
|
totalUniqueFrames = 1
|
||||||
currentUniqueFrameDuration = 1
|
currentUniqueFrameDuration = 1
|
||||||
|
|
||||||
// Store data for first frame
|
// Store data for first frame
|
||||||
currentTime := float64(frameNumber) / fps
|
currentTime := float64(frameNumber) / fps
|
||||||
effectiveFPS := float64(totalUniqueFrames) / currentTime
|
effectiveFPS := float64(totalUniqueFrames) / currentTime
|
||||||
|
@ -356,7 +359,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
|
||||||
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
|
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if consecutiveDuplicateCount > 1 {
|
if consecutiveDuplicateCount > 1 {
|
||||||
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
|
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
|
||||||
framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
|
framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
|
||||||
|
@ -367,7 +370,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
|
||||||
uniqueFramesInCurrentSecond++
|
uniqueFramesInCurrentSecond++
|
||||||
totalUniqueFrames++
|
totalUniqueFrames++
|
||||||
copy(previousFrame.Pix, currentFrame.Pix)
|
copy(previousFrame.Pix, currentFrame.Pix)
|
||||||
|
|
||||||
// Start tracking new unique frame
|
// Start tracking new unique frame
|
||||||
currentUniqueFrameDuration = 1
|
currentUniqueFrameDuration = 1
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue