flowdetect 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flowdetect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Charles Nichols
|
|
6
|
+
Author-email: Charles Nichols <nik@vizionikmedia.com>
|
|
7
|
+
Requires-Dist: numpy>=2.4.2
|
|
8
|
+
Requires-Dist: opencv-contrib-python>=4.13.0.92
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flowdetect"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Charles Nichols", email = "nik@vizionikmedia.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"numpy>=2.4.2",
|
|
12
|
+
"opencv-contrib-python>=4.13.0.92",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
flowdetect = "flowdetect:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.10.7,<0.11.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"click>=8.3.1",
|
|
25
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video Analyzer CLI
|
|
3
|
+
|
|
4
|
+
This application provides tools for analyzing video files using Optical Flow techniques.
|
|
5
|
+
It leverages the Click framework to provide a professional, POSIX-compliant interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cv2
|
|
9
|
+
import numpy as np
|
|
10
|
+
import click
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def cli():
|
|
15
|
+
"""
|
|
16
|
+
A professional Command-Line Interface for video motion analysis.
|
|
17
|
+
|
|
18
|
+
This tool provides access to both Sparse and Dense Optical Flow algorithms
|
|
19
|
+
for tracking features and detecting high-motion highlights.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@cli.command(name='track-sparse')
|
|
24
|
+
@click.argument('video_path', type=click.Path(exists=True, readable=True, dir_okay=False))
|
|
25
|
+
def track_sparse(video_path):
|
|
26
|
+
"""
|
|
27
|
+
Track specific points across video frames.
|
|
28
|
+
|
|
29
|
+
This command uses Lucas Kanade Sparse Optical Flow to find and track
|
|
30
|
+
the strongest corners in the image. It is highly useful for stabilizing
|
|
31
|
+
shaky footage or tracking a specific subject to keep them centered.
|
|
32
|
+
"""
|
|
33
|
+
click.echo(f"Starting Sparse Optical Flow on {video_path}...")
|
|
34
|
+
click.echo("Press 'q' or 'ESC' in the video window to quit.")
|
|
35
|
+
|
|
36
|
+
cap = cv2.VideoCapture(video_path)
|
|
37
|
+
|
|
38
|
+
# Parameters for ShiTomasi corner detection
|
|
39
|
+
feature_params = dict(
|
|
40
|
+
maxCorners=100,
|
|
41
|
+
qualityLevel=0.3,
|
|
42
|
+
minDistance=7,
|
|
43
|
+
blockSize=7
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Parameters for Lucas Kanade optical flow
|
|
47
|
+
lk_params = dict(
|
|
48
|
+
winSize=(15, 15),
|
|
49
|
+
maxLevel=2,
|
|
50
|
+
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create random colors for drawing tracks
|
|
54
|
+
color = np.random.randint(0, 255, (100, 3))
|
|
55
|
+
|
|
56
|
+
ret, old_frame = cap.read()
|
|
57
|
+
if not ret:
|
|
58
|
+
click.echo("Error: Could not read the video file.", err=True)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
|
|
62
|
+
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
|
|
63
|
+
|
|
64
|
+
# Create a mask image for drawing purposes
|
|
65
|
+
mask = np.zeros_like(old_frame)
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
ret, frame = cap.read()
|
|
69
|
+
if not ret:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
73
|
+
|
|
74
|
+
# Calculate optical flow
|
|
75
|
+
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
|
|
76
|
+
|
|
77
|
+
# Select good points
|
|
78
|
+
if p1 is not None:
|
|
79
|
+
good_new = p1[st == 1]
|
|
80
|
+
good_old = p0[st == 1]
|
|
81
|
+
|
|
82
|
+
# Draw the tracks
|
|
83
|
+
for i, (new, old) in enumerate(zip(good_new, good_old)):
|
|
84
|
+
a, b = new.ravel()
|
|
85
|
+
c, d = old.ravel()
|
|
86
|
+
# Draw line for motion history
|
|
87
|
+
mask = cv2.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)
|
|
88
|
+
# Draw dot for current position
|
|
89
|
+
frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
|
|
90
|
+
|
|
91
|
+
img = cv2.add(frame, mask)
|
|
92
|
+
cv2.imshow('Sparse Optical Flow Tracker', img)
|
|
93
|
+
|
|
94
|
+
k = cv2.waitKey(30) & 0xff
|
|
95
|
+
if k == 27 or k == ord('q'):
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
# Update the previous frame and previous points
|
|
99
|
+
old_gray = frame_gray.copy()
|
|
100
|
+
if p1 is not None:
|
|
101
|
+
p0 = good_new.reshape(-1, 1, 2)
|
|
102
|
+
|
|
103
|
+
cv2.destroyAllWindows()
|
|
104
|
+
cap.release()
|
|
105
|
+
click.echo("Sparse tracking completed.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command(name='detect-dense')
|
|
109
|
+
@click.argument('video_path', type=click.Path(exists=True, readable=True, dir_okay=False))
|
|
110
|
+
@click.option('--threshold', '-t', default=5.0, type=click.FloatRange(min=0.0), help='Motion intensity threshold required to flag a highlight.')
|
|
111
|
+
def detect_dense(video_path, threshold):
|
|
112
|
+
"""
|
|
113
|
+
Analyze video for global energy highlights.
|
|
114
|
+
|
|
115
|
+
This command calculates motion for every pixel using Farneback Dense Optical Flow.
|
|
116
|
+
It is computationally heavier but allows the system to detect global energy changes
|
|
117
|
+
such as applause, laughter, or rapid movement.
|
|
118
|
+
"""
|
|
119
|
+
click.echo(f"Analyzing {video_path} for high motion events (Threshold: {threshold})...")
|
|
120
|
+
click.echo("Press 'q' or 'ESC' in the video window to quit.")
|
|
121
|
+
|
|
122
|
+
cap = cv2.VideoCapture(video_path)
|
|
123
|
+
|
|
124
|
+
ret, frame1 = cap.read()
|
|
125
|
+
if not ret:
|
|
126
|
+
click.echo("Error: Could not read the video file.", err=True)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
|
|
130
|
+
hsv = np.zeros_like(frame1)
|
|
131
|
+
hsv[..., 1] = 255
|
|
132
|
+
|
|
133
|
+
while True:
|
|
134
|
+
ret, frame2 = cap.read()
|
|
135
|
+
if not ret:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
next_frame = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
|
|
139
|
+
|
|
140
|
+
# Calculate Dense Optical Flow
|
|
141
|
+
flow = cv2.calcOpticalFlowFarneback(prvs, next_frame, None, 0.5, 3, 15, 3, 5, 1.2, 0)
|
|
142
|
+
|
|
143
|
+
# Convert Cartesian coordinates to Polar coordinates
|
|
144
|
+
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
|
|
145
|
+
|
|
146
|
+
# Visualization
|
|
147
|
+
hsv[..., 0] = ang * 180 / np.pi / 2
|
|
148
|
+
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
|
|
149
|
+
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
|
150
|
+
|
|
151
|
+
# Calculate the mean motion intensity of the frame
|
|
152
|
+
avg_motion = np.mean(mag)
|
|
153
|
+
timestamp = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
|
|
154
|
+
|
|
155
|
+
if avg_motion > threshold:
|
|
156
|
+
click.echo(f"Highlight Detected at {timestamp:.2f} seconds (Intensity: {avg_motion:.2f})")
|
|
157
|
+
# Draw a red border to visualize the detection
|
|
158
|
+
cv2.rectangle(bgr, (0, 0), (bgr.shape[1], bgr.shape[0]), (0, 0, 255), 10)
|
|
159
|
+
|
|
160
|
+
cv2.imshow('Dense Optical Flow Highlight Detector', bgr)
|
|
161
|
+
|
|
162
|
+
k = cv2.waitKey(30) & 0xff
|
|
163
|
+
if k == 27 or k == ord('q'):
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
prvs = next_frame
|
|
167
|
+
|
|
168
|
+
cap.release()
|
|
169
|
+
cv2.destroyAllWindows()
|
|
170
|
+
click.echo("Highlight detection completed.")
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
cli()
|