Spaces:
Runtime error
Runtime error
dragonSwing
commited on
Commit
·
e086001
1
Parent(s):
add254f
Add application files
Browse files- README.md +1 -1
- app.py +225 -0
- bg_modeling.py +86 -0
- config.py +41 -0
- download_video.py +81 -0
- frame_differencing.py +97 -0
- output_results/Neural Network In 5 Minutes.pdf +3 -0
- output_results/react-in-5-minutes.pdf +3 -0
- post_process.py +77 -0
- requirements.txt +10 -0
- style.css +25 -0
- utils.py +53 -0
- video_2_slides.py +134 -0
README.md
CHANGED
@@ -4,7 +4,7 @@ emoji: 📊
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 3.
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: apache-2.0
|
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 3.32.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: apache-2.0
|
app.py
ADDED
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import glob
|
4 |
+
import validators
|
5 |
+
from config import *
|
6 |
+
from download_video import download_video
|
7 |
+
from bg_modeling import capture_slides_bg_modeling
|
8 |
+
from frame_differencing import capture_slides_frame_diff
|
9 |
+
from post_process import remove_duplicates
|
10 |
+
from utils import create_output_directory, convert_slides_to_pdf
|
11 |
+
|
12 |
+
|
13 |
+
def process(
|
14 |
+
video_path,
|
15 |
+
bg_type,
|
16 |
+
frame_buffer_history,
|
17 |
+
hash_size,
|
18 |
+
hash_func,
|
19 |
+
hash_queue_len,
|
20 |
+
sim_threshold,
|
21 |
+
):
|
22 |
+
output_dir_path = "output_results"
|
23 |
+
output_dir_path = create_output_directory(video_path, output_dir_path, bg_type)
|
24 |
+
|
25 |
+
if bg_type.lower() == "Frame Diff":
|
26 |
+
capture_slides_frame_diff(video_path, output_dir_path)
|
27 |
+
else:
|
28 |
+
if bg_type.lower() == "gmg":
|
29 |
+
thresh = DEC_THRESH
|
30 |
+
elif bg_type.lower() == "knn":
|
31 |
+
thresh = DIST_THRESH
|
32 |
+
|
33 |
+
capture_slides_bg_modeling(
|
34 |
+
video_path,
|
35 |
+
output_dir_path,
|
36 |
+
type_bgsub=bg_type,
|
37 |
+
history=frame_buffer_history,
|
38 |
+
threshold=thresh,
|
39 |
+
MIN_PERCENT_THRESH=MIN_PERCENT,
|
40 |
+
MAX_PERCENT_THRESH=MAX_PERCENT,
|
41 |
+
)
|
42 |
+
|
43 |
+
# Perform post-processing using difference hashing technique to remove duplicate slides.
|
44 |
+
hash_func = HASH_FUNC_DICT.get(hash_func.lower())
|
45 |
+
|
46 |
+
diff_threshold = int(hash_size * hash_size * (100 - sim_threshold) / 100)
|
47 |
+
remove_duplicates(
|
48 |
+
output_dir_path, hash_size, hash_func, hash_queue_len, diff_threshold
|
49 |
+
)
|
50 |
+
|
51 |
+
pdf_path = convert_slides_to_pdf(video_path, output_dir_path)
|
52 |
+
|
53 |
+
# Remove unneccessary files
|
54 |
+
os.remove(video_path)
|
55 |
+
for image_path in glob.glob(f"{output_dir_path}/*.jpg"):
|
56 |
+
os.remove(image_path)
|
57 |
+
return pdf_path
|
58 |
+
|
59 |
+
|
60 |
+
def process_file(
|
61 |
+
file_obj,
|
62 |
+
bg_type,
|
63 |
+
frame_buffer_history,
|
64 |
+
hash_size,
|
65 |
+
hash_func,
|
66 |
+
hash_queue_len,
|
67 |
+
sim_threshold,
|
68 |
+
):
|
69 |
+
return process(
|
70 |
+
file_obj.name,
|
71 |
+
bg_type,
|
72 |
+
frame_buffer_history,
|
73 |
+
hash_size,
|
74 |
+
hash_func,
|
75 |
+
hash_queue_len,
|
76 |
+
sim_threshold,
|
77 |
+
)
|
78 |
+
|
79 |
+
|
80 |
+
def process_via_url(
|
81 |
+
url,
|
82 |
+
bg_type,
|
83 |
+
frame_buffer_history,
|
84 |
+
hash_size,
|
85 |
+
hash_func,
|
86 |
+
hash_queue_len,
|
87 |
+
sim_threshold,
|
88 |
+
):
|
89 |
+
if validators.url(url):
|
90 |
+
video_path = download_video(url)
|
91 |
+
if video_path is None:
|
92 |
+
raise gr.Error("Please enter a valid video URL")
|
93 |
+
return process(
|
94 |
+
video_path,
|
95 |
+
bg_type,
|
96 |
+
frame_buffer_history,
|
97 |
+
hash_size,
|
98 |
+
hash_func,
|
99 |
+
hash_queue_len,
|
100 |
+
sim_threshold,
|
101 |
+
)
|
102 |
+
else:
|
103 |
+
raise gr.Error("Please enter a valid video URL")
|
104 |
+
|
105 |
+
|
106 |
+
with gr.Blocks(css="style.css") as demo:
|
107 |
+
with gr.Row(elem_classes=["container"]):
|
108 |
+
gr.Markdown(
|
109 |
+
"""
|
110 |
+
# Video 2 Slides Converter
|
111 |
+
|
112 |
+
Convert your video presentation into PDF slides with one click.
|
113 |
+
|
114 |
+
You can browse your video from the local file system, or enter a video URL/YouTube video link to start processing.
|
115 |
+
|
116 |
+
**Note**:
|
117 |
+
- It will take a bit of time to complete (~40% of the original video length), so stay tuned!
|
118 |
+
- Remember to press Enter if you are using an external URL
|
119 |
+
""",
|
120 |
+
elem_id="container",
|
121 |
+
)
|
122 |
+
|
123 |
+
with gr.Row(elem_classes=["container"]):
|
124 |
+
with gr.Column(scale=1):
|
125 |
+
with gr.Accordion("Advanced parameters"):
|
126 |
+
bg_type = gr.Dropdown(
|
127 |
+
["Frame Diff", "GMG", "KNN"],
|
128 |
+
value="GMG",
|
129 |
+
label="Background subtraction",
|
130 |
+
info="Type of background subtraction to be used",
|
131 |
+
)
|
132 |
+
frame_buffer_history = gr.Slider(
|
133 |
+
minimum=5,
|
134 |
+
maximum=20,
|
135 |
+
value=FRAME_BUFFER_HISTORY,
|
136 |
+
step=5,
|
137 |
+
label="Frame buffer history",
|
138 |
+
info="Length of the frame buffer history to model background.",
|
139 |
+
)
|
140 |
+
# Post process
|
141 |
+
hash_func = gr.Dropdown(
|
142 |
+
["Difference hashing", "Perceptual hashing", "Average hashing"],
|
143 |
+
value="Difference hashing",
|
144 |
+
label="Background subtraction",
|
145 |
+
info="Hash function to use for image hashing",
|
146 |
+
)
|
147 |
+
hash_size = gr.Slider(
|
148 |
+
minimum=8,
|
149 |
+
maximum=16,
|
150 |
+
value=HASH_SIZE,
|
151 |
+
step=2,
|
152 |
+
label="Hash size",
|
153 |
+
info="Hash size to use for image hashing",
|
154 |
+
)
|
155 |
+
hash_queue_len = gr.Slider(
|
156 |
+
minimum=5,
|
157 |
+
maximum=15,
|
158 |
+
value=HASH_BUFFER_HISTORY,
|
159 |
+
step=5,
|
160 |
+
label="Hash queue len",
|
161 |
+
info="Number of history images used to find out duplicate image",
|
162 |
+
)
|
163 |
+
sim_threshold = gr.Slider(
|
164 |
+
minimum=90,
|
165 |
+
maximum=100,
|
166 |
+
value=SIM_THRESHOLD,
|
167 |
+
step=1,
|
168 |
+
label="Similarity threshold",
|
169 |
+
info="Minimum similarity threshold (in percent) to consider 2 images to be similar",
|
170 |
+
)
|
171 |
+
|
172 |
+
with gr.Column(scale=2):
|
173 |
+
with gr.Row(elem_id="row-flex"):
|
174 |
+
with gr.Column(scale=3):
|
175 |
+
file_url = gr.Textbox(
|
176 |
+
value="",
|
177 |
+
label="Upload your file",
|
178 |
+
placeholder="Enter a video url or YouTube link",
|
179 |
+
show_label=False,
|
180 |
+
)
|
181 |
+
with gr.Column(scale=1, min_width=160):
|
182 |
+
upload_button = gr.UploadButton("Browse File", file_types=["video"])
|
183 |
+
file_output = gr.File(file_types=[".pdf"], label="Output PDF")
|
184 |
+
gr.Examples(
|
185 |
+
[
|
186 |
+
[
|
187 |
+
"https://www.youtube.com/watch?v=bfmFfD2RIcg",
|
188 |
+
"output_results/Neural Network In 5 Minutes.pdf",
|
189 |
+
],
|
190 |
+
[
|
191 |
+
"https://www.youtube.com/watch?v=EEo10bgsh0k",
|
192 |
+
"output_results/react-in-5-minutes.pdf",
|
193 |
+
],
|
194 |
+
],
|
195 |
+
[file_url, file_output],
|
196 |
+
)
|
197 |
+
|
198 |
+
file_url.submit(
|
199 |
+
process_via_url,
|
200 |
+
[
|
201 |
+
file_url,
|
202 |
+
bg_type,
|
203 |
+
frame_buffer_history,
|
204 |
+
hash_size,
|
205 |
+
hash_func,
|
206 |
+
hash_queue_len,
|
207 |
+
sim_threshold,
|
208 |
+
],
|
209 |
+
file_output,
|
210 |
+
)
|
211 |
+
upload_button.upload(
|
212 |
+
process_file,
|
213 |
+
[
|
214 |
+
upload_button,
|
215 |
+
bg_type,
|
216 |
+
frame_buffer_history,
|
217 |
+
hash_size,
|
218 |
+
hash_func,
|
219 |
+
hash_queue_len,
|
220 |
+
sim_threshold,
|
221 |
+
],
|
222 |
+
file_output,
|
223 |
+
)
|
224 |
+
|
225 |
+
demo.queue(concurrency_count=4).launch()
|
bg_modeling.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import time
|
3 |
+
import sys
|
4 |
+
import cv2
|
5 |
+
from utils import resize_image_frame
|
6 |
+
|
7 |
+
|
8 |
+
def capture_slides_bg_modeling(
|
9 |
+
video_path,
|
10 |
+
output_dir_path,
|
11 |
+
type_bgsub,
|
12 |
+
history,
|
13 |
+
threshold,
|
14 |
+
MIN_PERCENT_THRESH,
|
15 |
+
MAX_PERCENT_THRESH,
|
16 |
+
):
|
17 |
+
print(f"Using {type_bgsub} for Background Modeling...")
|
18 |
+
print("---" * 10)
|
19 |
+
|
20 |
+
if type_bgsub == "GMG":
|
21 |
+
bg_sub = cv2.bgsegm.createBackgroundSubtractorGMG(
|
22 |
+
initializationFrames=history, decisionThreshold=threshold
|
23 |
+
)
|
24 |
+
elif type_bgsub == "KNN":
|
25 |
+
bg_sub = cv2.createBackgroundSubtractorKNN(
|
26 |
+
history=history, dist2Threshold=threshold, detectShadows=False
|
27 |
+
)
|
28 |
+
else:
|
29 |
+
raise ValueError("Please choose GMG or KNN as background subtraction method")
|
30 |
+
|
31 |
+
capture_frame = False
|
32 |
+
screenshots_count = 0
|
33 |
+
|
34 |
+
# Capture video frames.
|
35 |
+
cap = cv2.VideoCapture(video_path)
|
36 |
+
|
37 |
+
if not cap.isOpened():
|
38 |
+
print("Unable to open video file: ", video_path)
|
39 |
+
sys.exit()
|
40 |
+
|
41 |
+
start = time.time()
|
42 |
+
# Loop over subsequent frames.
|
43 |
+
while cap.isOpened():
|
44 |
+
ret, frame = cap.read()
|
45 |
+
|
46 |
+
if not ret:
|
47 |
+
break
|
48 |
+
|
49 |
+
# Create a copy of the original frame.
|
50 |
+
orig_frame = frame.copy()
|
51 |
+
# Resize the frame keeping aspect ratio.
|
52 |
+
frame = resize_image_frame(frame, resize_width=640)
|
53 |
+
|
54 |
+
# Apply each frame through the background subtractor.
|
55 |
+
fg_mask = bg_sub.apply(frame)
|
56 |
+
|
57 |
+
# Compute the percentage of the Foreground mask."
|
58 |
+
p_non_zero = (cv2.countNonZero(fg_mask) / (1.0 * fg_mask.size)) * 100
|
59 |
+
|
60 |
+
# %age of non-zero pixels < MAX_PERCENT_THRESH, implies motion has stopped.
|
61 |
+
# Therefore, capture the frame.
|
62 |
+
if p_non_zero < MAX_PERCENT_THRESH and not capture_frame:
|
63 |
+
capture_frame = True
|
64 |
+
|
65 |
+
screenshots_count += 1
|
66 |
+
|
67 |
+
png_filename = f"{screenshots_count:03}.jpg"
|
68 |
+
out_file_path = os.path.join(output_dir_path, png_filename)
|
69 |
+
print(f"Saving file at: {out_file_path}")
|
70 |
+
cv2.imwrite(out_file_path, orig_frame, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
71 |
+
|
72 |
+
# p_non_zero >= MIN_PERCENT_THRESH, indicates motion/animations.
|
73 |
+
# Hence wait till the motion across subsequent frames has settled down.
|
74 |
+
elif capture_frame and p_non_zero >= MIN_PERCENT_THRESH:
|
75 |
+
capture_frame = False
|
76 |
+
|
77 |
+
end_time = time.time()
|
78 |
+
print("***" * 10, "\n")
|
79 |
+
print("Statistics:")
|
80 |
+
print("---" * 10)
|
81 |
+
print(f"Total Time taken: {round(end_time-start, 3)} secs")
|
82 |
+
print(f"Total Screenshots captured: {screenshots_count}")
|
83 |
+
print("---" * 10, "\n")
|
84 |
+
|
85 |
+
# Release Video Capture object.
|
86 |
+
cap.release()
|
config.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import imagehash
|
2 |
+
|
3 |
+
# -------------- Initializations ---------------------
|
4 |
+
|
5 |
+
DOWNLOAD_DIR = "downloads"
|
6 |
+
|
7 |
+
FRAME_BUFFER_HISTORY = 15 # Length of the frame buffer history to model background.
|
8 |
+
DEC_THRESH = (
|
9 |
+
0.75 # Threshold value, above which it is marked foreground, else background.
|
10 |
+
)
|
11 |
+
DIST_THRESH = 100 # Threshold on the squared distance between the pixel and the sample to decide whether a pixel is close to that sample.
|
12 |
+
|
13 |
+
MIN_PERCENT = (
|
14 |
+
0.15 # %age threshold to check if there is motion across subsequent frames
|
15 |
+
)
|
16 |
+
MAX_PERCENT = (
|
17 |
+
0.01 # %age threshold to determine if the motion across frames has stopped.
|
18 |
+
)
|
19 |
+
|
20 |
+
# Post processing
|
21 |
+
|
22 |
+
SIM_THRESHOLD = (
|
23 |
+
96 # Minimum similarity threshold (in percent) to consider 2 images to be similar
|
24 |
+
)
|
25 |
+
|
26 |
+
HASH_SIZE = 12 # Hash size to use for image hashing
|
27 |
+
|
28 |
+
HASH_FUNC = "dhash" # Hash function to use for image hashing
|
29 |
+
|
30 |
+
HASH_BUFFER_HISTORY = 5 # Number of history images used to find out duplicate image
|
31 |
+
|
32 |
+
HASH_FUNC_DICT = {
|
33 |
+
"dhash": imagehash.dhash,
|
34 |
+
"phash": imagehash.phash,
|
35 |
+
"ahash": imagehash.average_hash,
|
36 |
+
"difference hashing": imagehash.dhash,
|
37 |
+
"perceptual hashing": imagehash.phash,
|
38 |
+
"average hashing": imagehash.average_hash,
|
39 |
+
}
|
40 |
+
|
41 |
+
# ----------------------------------------------------
|
download_video.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import mimetypes
|
2 |
+
import tempfile
|
3 |
+
import requests
|
4 |
+
import os
|
5 |
+
from urllib.parse import urlparse
|
6 |
+
from pytube import YouTube
|
7 |
+
from config import DOWNLOAD_DIR
|
8 |
+
|
9 |
+
|
10 |
+
def download_video_from_url(url, output_dir=DOWNLOAD_DIR):
|
11 |
+
try:
|
12 |
+
response = requests.get(url)
|
13 |
+
response.raise_for_status() # Check if the request was successful
|
14 |
+
|
15 |
+
content_type = response.headers.get("content-type")
|
16 |
+
if "video" not in content_type:
|
17 |
+
print("The given URL is not a valid video")
|
18 |
+
return None
|
19 |
+
file_extension = mimetypes.guess_extension(content_type)
|
20 |
+
|
21 |
+
os.makedirs(output_dir, exist_ok=True)
|
22 |
+
|
23 |
+
temp_file = tempfile.NamedTemporaryFile(
|
24 |
+
delete=False, suffix=file_extension, dir=output_dir
|
25 |
+
)
|
26 |
+
temp_file_path = temp_file.name
|
27 |
+
|
28 |
+
with open(temp_file_path, "wb") as file:
|
29 |
+
file.write(response.content)
|
30 |
+
return temp_file_path
|
31 |
+
|
32 |
+
except requests.exceptions.RequestException as e:
|
33 |
+
print("An error occurred while downloading the video:", str(e))
|
34 |
+
return None
|
35 |
+
|
36 |
+
|
37 |
+
def download_video_from_youtube(url, output_dir=DOWNLOAD_DIR):
|
38 |
+
try:
|
39 |
+
yt = YouTube(url)
|
40 |
+
video = (
|
41 |
+
yt.streams.filter(progressive=True, file_extension="mp4")
|
42 |
+
.order_by("resolution")
|
43 |
+
.desc()
|
44 |
+
.first()
|
45 |
+
)
|
46 |
+
|
47 |
+
os.makedirs(output_dir, exist_ok=True)
|
48 |
+
|
49 |
+
video_path = video.download(output_dir)
|
50 |
+
return video_path
|
51 |
+
|
52 |
+
except Exception as e:
|
53 |
+
print("An error occurred while downloading the video:", str(e))
|
54 |
+
return None
|
55 |
+
|
56 |
+
|
57 |
+
def download_video(url, output_dir=DOWNLOAD_DIR):
|
58 |
+
parsed_url = urlparse(url)
|
59 |
+
domain = parsed_url.netloc.lower()
|
60 |
+
|
61 |
+
print("---" * 5, "Downloading video file", "---" * 5)
|
62 |
+
|
63 |
+
if "youtube" in domain:
|
64 |
+
video_path = download_video_from_youtube(url, output_dir)
|
65 |
+
else:
|
66 |
+
video_path = download_video_from_url(url, output_dir)
|
67 |
+
|
68 |
+
if video_path:
|
69 |
+
print(f"Saving file at: {video_path}")
|
70 |
+
print("---" * 10)
|
71 |
+
return video_path
|
72 |
+
|
73 |
+
|
74 |
+
if __name__ == "__main__":
|
75 |
+
youtube_link = "https://www.youtube.com/watch?v=2OTq15A5s0Y"
|
76 |
+
temp_video_path = download_video_from_youtube(youtube_link)
|
77 |
+
|
78 |
+
if temp_video_path is not None:
|
79 |
+
print("Video downloaded successfully to:", temp_video_path)
|
80 |
+
else:
|
81 |
+
print("Failed to download the video.")
|
frame_differencing.py
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
import sys
|
5 |
+
|
6 |
+
|
7 |
+
def capture_slides_frame_diff(
|
8 |
+
video_path, output_dir_path, MIN_PERCENT_THRESH=0.06, ELAPSED_FRAME_THRESH=85
|
9 |
+
):
|
10 |
+
prev_frame = None
|
11 |
+
curr_frame = None
|
12 |
+
screenshots_count = 0
|
13 |
+
capture_frame = False
|
14 |
+
frame_elapsed = 0
|
15 |
+
|
16 |
+
# Initialize kernel.
|
17 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
18 |
+
|
19 |
+
# Capture video frames
|
20 |
+
cap = cv2.VideoCapture(video_path)
|
21 |
+
|
22 |
+
if not cap.isOpened():
|
23 |
+
print("Unable to open video file: ", video_path)
|
24 |
+
sys.exit()
|
25 |
+
|
26 |
+
success, first_frame = cap.read()
|
27 |
+
|
28 |
+
print("Using frame differencing for Background Subtraction...")
|
29 |
+
print("---" * 10)
|
30 |
+
|
31 |
+
start = time.time()
|
32 |
+
|
33 |
+
# The 1st frame should always be present in the output directory.
|
34 |
+
# Hence capture and save the 1st frame.
|
35 |
+
if success:
|
36 |
+
# Convert frame to grayscale.
|
37 |
+
first_frame_gray = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
|
38 |
+
|
39 |
+
prev_frame = first_frame_gray
|
40 |
+
|
41 |
+
screenshots_count += 1
|
42 |
+
|
43 |
+
filename = f"{screenshots_count:03}.lpg"
|
44 |
+
out_file_path = os.path.join(output_dir_path, filename)
|
45 |
+
print(f"Saving file at: {out_file_path}")
|
46 |
+
|
47 |
+
# Save frame.
|
48 |
+
cv2.imwrite(out_file_path, first_frame, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
49 |
+
|
50 |
+
# Loop over subsequent frames.
|
51 |
+
while cap.isOpened():
|
52 |
+
ret, frame = cap.read()
|
53 |
+
if not ret:
|
54 |
+
break
|
55 |
+
|
56 |
+
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
57 |
+
curr_frame = frame_gray
|
58 |
+
|
59 |
+
if (prev_frame is not None) and (curr_frame is not None):
|
60 |
+
frame_diff = cv2.absdiff(curr_frame, prev_frame)
|
61 |
+
_, frame_diff = cv2.threshold(frame_diff, 80, 255, cv2.THRESH_BINARY)
|
62 |
+
|
63 |
+
# Perform dilation to capture motion.
|
64 |
+
frame_diff = cv2.dilate(frame_diff, kernel)
|
65 |
+
|
66 |
+
# Compute the percentage of non-zero pixels in the frame.
|
67 |
+
p_non_zero = (cv2.countNonZero(frame_diff) / (1.0 * frame_gray.size)) * 100
|
68 |
+
|
69 |
+
if p_non_zero >= MIN_PERCENT_THRESH and not capture_frame:
|
70 |
+
capture_frame = True
|
71 |
+
|
72 |
+
elif capture_frame:
|
73 |
+
frame_elapsed += 1
|
74 |
+
|
75 |
+
if frame_elapsed >= ELAPSED_FRAME_THRESH:
|
76 |
+
capture_frame = False
|
77 |
+
frame_elapsed = 0
|
78 |
+
|
79 |
+
screenshots_count += 1
|
80 |
+
|
81 |
+
filename = f"{screenshots_count:03}.png"
|
82 |
+
out_file_path = os.path.join(output_dir_path, filename)
|
83 |
+
print(f"Saving file at: {out_file_path}")
|
84 |
+
|
85 |
+
cv2.imwrite(out_file_path, frame)
|
86 |
+
|
87 |
+
prev_frame = curr_frame
|
88 |
+
|
89 |
+
end_time = time.time()
|
90 |
+
print("***" * 10, "\n")
|
91 |
+
print("Statistics:")
|
92 |
+
print("---" * 5)
|
93 |
+
print(f"Total Time taken: {round(end_time-start, 3)} secs")
|
94 |
+
print(f"Total Screenshots captured: {screenshots_count}")
|
95 |
+
print("---" * 10, "\n")
|
96 |
+
|
97 |
+
cap.release()
|
output_results/Neural Network In 5 Minutes.pdf
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fe8c6f4fc132de07cc528f6fff4021fd084a07b17858f1b294e7726109dac89a
|
3 |
+
size 3656629
|
output_results/react-in-5-minutes.pdf
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:dee28ec7ce33a50b70553540af6ea4329226785fe1fc9da1f14acf13bf0417d2
|
3 |
+
size 371324
|
post_process.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import imagehash
|
2 |
+
import os
|
3 |
+
from collections import deque
|
4 |
+
from PIL import Image
|
5 |
+
|
6 |
+
|
7 |
+
def find_similar_images(
|
8 |
+
base_dir, hash_size=8, hashfunc=imagehash.dhash, queue_len=5, threshold=4
|
9 |
+
):
|
10 |
+
snapshots_files = sorted(os.listdir(base_dir))
|
11 |
+
|
12 |
+
hash_dict = {}
|
13 |
+
hash_queue = deque([], maxlen=queue_len)
|
14 |
+
duplicates = []
|
15 |
+
num_duplicates = 0
|
16 |
+
|
17 |
+
print("---" * 5, "Finding similar files", "---" * 5)
|
18 |
+
|
19 |
+
for file in snapshots_files:
|
20 |
+
read_file = Image.open(os.path.join(base_dir, file))
|
21 |
+
comp_hash = hashfunc(read_file, hash_size=hash_size)
|
22 |
+
duplicate = False
|
23 |
+
|
24 |
+
if comp_hash not in hash_dict:
|
25 |
+
hash_dict[comp_hash] = file
|
26 |
+
# Compare with hash queue to find out potential duplicates
|
27 |
+
for img_hash in hash_queue:
|
28 |
+
if img_hash - comp_hash <= threshold:
|
29 |
+
duplicate = True
|
30 |
+
break
|
31 |
+
|
32 |
+
if not duplicate:
|
33 |
+
hash_queue.append(comp_hash)
|
34 |
+
else:
|
35 |
+
duplicate = True
|
36 |
+
|
37 |
+
if duplicate:
|
38 |
+
print("Duplicate file: ", file)
|
39 |
+
duplicates.append(file)
|
40 |
+
num_duplicates += 1
|
41 |
+
|
42 |
+
print("\nTotal duplicate files:", num_duplicates)
|
43 |
+
print("-----" * 10)
|
44 |
+
return hash_dict, duplicates
|
45 |
+
|
46 |
+
|
47 |
+
def remove_duplicates(
|
48 |
+
base_dir, hash_size=8, hashfunc=imagehash.dhash, queue_len=5, threshold=4
|
49 |
+
):
|
50 |
+
_, duplicates = find_similar_images(
|
51 |
+
base_dir,
|
52 |
+
hash_size=hash_size,
|
53 |
+
hashfunc=hashfunc,
|
54 |
+
queue_len=queue_len,
|
55 |
+
threshold=threshold,
|
56 |
+
)
|
57 |
+
|
58 |
+
if not len(duplicates):
|
59 |
+
print("No duplicates found!")
|
60 |
+
else:
|
61 |
+
print("Removing duplicates...")
|
62 |
+
|
63 |
+
for dup_file in duplicates:
|
64 |
+
file_path = os.path.join(base_dir, dup_file)
|
65 |
+
|
66 |
+
if os.path.exists(file_path):
|
67 |
+
os.remove(file_path)
|
68 |
+
else:
|
69 |
+
print("Filepath: ", file_path, "does not exists.")
|
70 |
+
|
71 |
+
print("All duplicates removed!")
|
72 |
+
|
73 |
+
print("***" * 10, "\n")
|
74 |
+
|
75 |
+
|
76 |
+
if __name__ == "__main__":
|
77 |
+
remove_duplicates("sample_1")
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
opencv-contrib-python==4.7.0.72
|
2 |
+
numpy
|
3 |
+
Pillow
|
4 |
+
scipy
|
5 |
+
six
|
6 |
+
ImageHash
|
7 |
+
img2pdf
|
8 |
+
pytube
|
9 |
+
validators
|
10 |
+
requests
|
style.css
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.container {
|
2 |
+
max-width: 1200px;
|
3 |
+
margin-left: auto;
|
4 |
+
margin-right: auto;
|
5 |
+
}
|
6 |
+
|
7 |
+
#row-flex {
|
8 |
+
display: flex;
|
9 |
+
align-items: center;
|
10 |
+
justify-content: center;
|
11 |
+
}
|
12 |
+
|
13 |
+
a,
|
14 |
+
a:hover,
|
15 |
+
a:visited {
|
16 |
+
text-decoration-line: underline;
|
17 |
+
font-weight: 600;
|
18 |
+
color: #1f2937 !important;
|
19 |
+
}
|
20 |
+
|
21 |
+
.dark a,
|
22 |
+
.dark a:hover,
|
23 |
+
.dark a:visited {
|
24 |
+
color: #f3f4f6 !important;
|
25 |
+
}
|
utils.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import cv2
|
3 |
+
import shutil
|
4 |
+
import img2pdf
|
5 |
+
import glob
|
6 |
+
|
7 |
+
# PIL can also be used to convert the image set into PDFs.
|
8 |
+
# However, using PIL requires opening each of the images in the set.
|
9 |
+
# Hence img2pdf package was used, which is able to convert the entire image set into a PDF
|
10 |
+
# without opening at once.
|
11 |
+
|
12 |
+
|
13 |
+
def resize_image_frame(frame, resize_width):
|
14 |
+
ht, wd, _ = frame.shape
|
15 |
+
new_height = resize_width * ht / wd
|
16 |
+
frame = cv2.resize(
|
17 |
+
frame, (resize_width, int(new_height)), interpolation=cv2.INTER_AREA
|
18 |
+
)
|
19 |
+
|
20 |
+
return frame
|
21 |
+
|
22 |
+
|
23 |
+
def create_output_directory(video_path, output_path, type_bgsub):
|
24 |
+
vid_file_name = video_path.rsplit(os.sep)[-1].split(".")[0]
|
25 |
+
output_dir_path = os.path.join(output_path, vid_file_name, type_bgsub)
|
26 |
+
|
27 |
+
# Remove the output directory if there is already one.
|
28 |
+
if os.path.exists(output_dir_path):
|
29 |
+
shutil.rmtree(output_dir_path)
|
30 |
+
|
31 |
+
# Create output directory.
|
32 |
+
os.makedirs(output_dir_path, exist_ok=True)
|
33 |
+
print("Output directory created...")
|
34 |
+
print("Path:", output_dir_path)
|
35 |
+
print("***" * 10, "\n")
|
36 |
+
|
37 |
+
return output_dir_path
|
38 |
+
|
39 |
+
|
40 |
+
def convert_slides_to_pdf(video_path, output_path):
|
41 |
+
pdf_file_name = video_path.rsplit(os.sep)[-1].split(".")[0] + ".pdf"
|
42 |
+
output_pdf_path = os.path.join(output_path, pdf_file_name)
|
43 |
+
|
44 |
+
print("Output PDF Path:", output_pdf_path)
|
45 |
+
print("Converting captured slide images to PDF...")
|
46 |
+
|
47 |
+
with open(output_pdf_path, "wb") as f:
|
48 |
+
f.write(img2pdf.convert(sorted(glob.glob(f"{output_path}/*.jpg"))))
|
49 |
+
|
50 |
+
print("PDF Created!")
|
51 |
+
print("***" * 10, "\n")
|
52 |
+
|
53 |
+
return output_pdf_path
|
video_2_slides.py
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import validators
|
4 |
+
from config import *
|
5 |
+
from download_video import download_video
|
6 |
+
from bg_modeling import capture_slides_bg_modeling
|
7 |
+
from frame_differencing import capture_slides_frame_diff
|
8 |
+
from post_process import remove_duplicates
|
9 |
+
from utils import create_output_directory, convert_slides_to_pdf
|
10 |
+
|
11 |
+
|
12 |
+
if __name__ == "__main__":
|
13 |
+
parser = argparse.ArgumentParser(
|
14 |
+
description="This script is used to convert video frames into slide PDFs."
|
15 |
+
)
|
16 |
+
parser.add_argument(
|
17 |
+
"-v", "--video_path", help="Path to the video file, video url, or YouTube video link", type=str
|
18 |
+
)
|
19 |
+
parser.add_argument(
|
20 |
+
"-o",
|
21 |
+
"--out_dir",
|
22 |
+
default="output_results",
|
23 |
+
help="Path to the output directory",
|
24 |
+
type=str,
|
25 |
+
)
|
26 |
+
parser.add_argument(
|
27 |
+
"--type",
|
28 |
+
help="type of background subtraction to be used",
|
29 |
+
default="GMG",
|
30 |
+
choices=["Frame_Diff", "GMG", "KNN"],
|
31 |
+
type=str,
|
32 |
+
)
|
33 |
+
parser.add_argument(
|
34 |
+
"-hf",
|
35 |
+
"--hash-func",
|
36 |
+
help="Hash function to use for image hashing. Only effective if post-processing is enabled",
|
37 |
+
default=HASH_FUNC,
|
38 |
+
choices=["dhash", "phash", "ahash"],
|
39 |
+
type=str,
|
40 |
+
)
|
41 |
+
parser.add_argument(
|
42 |
+
"-hs",
|
43 |
+
"--hash-size",
|
44 |
+
help="Hash size to use for image hashing. Only effective if post-processing is enabled",
|
45 |
+
default=HASH_SIZE,
|
46 |
+
choices=[8, 12, 16],
|
47 |
+
type=int,
|
48 |
+
)
|
49 |
+
parser.add_argument(
|
50 |
+
"--threshold",
|
51 |
+
help="Minimum similarity threshold (in percent) to consider 2 images to be similar. Only effective if post-processing is enabled",
|
52 |
+
default=SIM_THRESHOLD,
|
53 |
+
choices=range(90, 101),
|
54 |
+
type=int,
|
55 |
+
)
|
56 |
+
parser.add_argument(
|
57 |
+
"-q",
|
58 |
+
"--queue-len",
|
59 |
+
help="Number of history images used to find out duplicate image. Only effective if post-processing is enabled",
|
60 |
+
default=HASH_BUFFER_HISTORY,
|
61 |
+
type=int,
|
62 |
+
)
|
63 |
+
parser.add_argument(
|
64 |
+
"--no_post_process",
|
65 |
+
action="store_true",
|
66 |
+
default=False,
|
67 |
+
help="flag to apply post processing or not",
|
68 |
+
)
|
69 |
+
parser.add_argument(
|
70 |
+
"--convert_to_pdf",
|
71 |
+
action="store_true",
|
72 |
+
default=False,
|
73 |
+
help="flag to convert the entire image set to pdf or not",
|
74 |
+
)
|
75 |
+
args = parser.parse_args()
|
76 |
+
|
77 |
+
queue_len = args.queue_len
|
78 |
+
if queue_len <= 0:
|
79 |
+
print(
|
80 |
+
f"Warnings: queue_len argument must be positive. Fallback to {HASH_BUFFER_HISTORY}"
|
81 |
+
)
|
82 |
+
queue_len = HASH_BUFFER_HISTORY
|
83 |
+
|
84 |
+
video_path = args.video_file_path
|
85 |
+
output_dir_path = args.out_dir
|
86 |
+
type_bg_sub = args.type
|
87 |
+
temp_file = False
|
88 |
+
|
89 |
+
if validators.url(video_path):
|
90 |
+
video_path = download_video(video_path)
|
91 |
+
temp_file = True
|
92 |
+
if video_path is None:
|
93 |
+
exit(1)
|
94 |
+
elif not os.path.exists(video_path):
|
95 |
+
raise ValueError(
|
96 |
+
"The video doesn't exist or isn't a valid URL. Please check your video path again"
|
97 |
+
)
|
98 |
+
|
99 |
+
output_dir_path = create_output_directory(video_path, output_dir_path, type_bg_sub)
|
100 |
+
|
101 |
+
if type_bg_sub.lower() == "frame_diff":
|
102 |
+
capture_slides_frame_diff(video_path, output_dir_path)
|
103 |
+
else:
|
104 |
+
if type_bg_sub.lower() == "gmg":
|
105 |
+
thresh = DEC_THRESH
|
106 |
+
elif type_bg_sub.lower() == "knn":
|
107 |
+
thresh = DIST_THRESH
|
108 |
+
|
109 |
+
capture_slides_bg_modeling(
|
110 |
+
video_path,
|
111 |
+
output_dir_path,
|
112 |
+
type_bgsub=type_bg_sub,
|
113 |
+
history=FRAME_BUFFER_HISTORY,
|
114 |
+
threshold=thresh,
|
115 |
+
MIN_PERCENT_THRESH=MIN_PERCENT,
|
116 |
+
MAX_PERCENT_THRESH=MAX_PERCENT,
|
117 |
+
)
|
118 |
+
|
119 |
+
# Perform post-processing using difference hashing technique to remove duplicate slides.
|
120 |
+
if not args.no_post_process:
|
121 |
+
hash_size = args.hash_size
|
122 |
+
hash_func = HASH_FUNC_DICT.get(args.hash_func)
|
123 |
+
sim_threshold = args.threshold
|
124 |
+
|
125 |
+
diff_threshold = int(hash_size * hash_size * (100 - sim_threshold) / 100)
|
126 |
+
remove_duplicates(
|
127 |
+
output_dir_path, hash_size, hash_func, queue_len, diff_threshold
|
128 |
+
)
|
129 |
+
|
130 |
+
if args.convert_to_pdf:
|
131 |
+
convert_slides_to_pdf(video_path, output_dir_path)
|
132 |
+
|
133 |
+
# if temp_file:
|
134 |
+
# os.remove(video_path)
|