Skip to content

Commit d5cdc3c

Browse files
Merge pull request #81 from tiagocoutinho/video-gl
Video opengl
2 parents 3d770f2 + 7da4c30 commit d5cdc3c

File tree

9 files changed

+388
-0
lines changed

9 files changed

+388
-0
lines changed

examples/video/gl/common.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import argparse
2+
import logging
3+
import selectors
4+
5+
from OpenGL import GL
6+
7+
from linuxpy.video.device import Device, PixelFormat, VideoCapture
8+
9+
OPENCV_FORMATS = {
10+
PixelFormat.MJPEG,
11+
PixelFormat.YUYV,
12+
PixelFormat.YVYU,
13+
PixelFormat.UYVY,
14+
PixelFormat.YUV420,
15+
PixelFormat.NV12,
16+
PixelFormat.NV21,
17+
}
18+
19+
20+
def opencv_decode_frame(frame):
21+
import cv2
22+
23+
data, fmt = frame.array, frame.pixel_format
24+
if fmt == PixelFormat.MJPEG:
25+
result = cv2.imdecode(data, cv2.IMREAD_COLOR)
26+
else:
27+
YUV_MAP = {
28+
PixelFormat.YUYV: cv2.COLOR_YUV2RGB_YUYV,
29+
PixelFormat.YVYU: cv2.COLOR_YUV2RGB_YVYU,
30+
PixelFormat.UYVY: cv2.COLOR_YUV2RGB_UYVY,
31+
PixelFormat.YUV420: cv2.COLOR_YUV2RGB_I420,
32+
PixelFormat.NV12: cv2.COLOR_YUV2RGB_NV12,
33+
PixelFormat.NV21: cv2.COLOR_YUV2RGB_NV21,
34+
}
35+
data = frame.array
36+
if fmt in {PixelFormat.NV12, PixelFormat.NV21, PixelFormat.YUV420}:
37+
data.shape = frame.height * 3 // 2, frame.width, -1
38+
else:
39+
data.shape = frame.height, frame.width, -1
40+
result = cv2.cvtColor(data, YUV_MAP[fmt])
41+
return result, GL.GL_BGR
42+
43+
44+
def decode_frame(frame):
45+
fmt = frame.pixel_format
46+
if fmt == PixelFormat.RGB24:
47+
return frame.data, GL.GL_RGB
48+
elif fmt == PixelFormat.BGR24:
49+
return frame.data, GL.GL_BGR
50+
elif fmt in OPENCV_FORMATS:
51+
return opencv_decode_frame(frame)
52+
53+
54+
def maybe_frames(capture):
55+
device = capture.device
56+
selector = selectors.DefaultSelector()
57+
selector.register(device, selectors.EVENT_READ)
58+
stream = iter(capture)
59+
try:
60+
while True:
61+
if selector.select(0):
62+
frame = next(stream)
63+
frame.user_data = decode_frame(frame)
64+
yield frame
65+
else:
66+
yield None
67+
finally:
68+
selector.unregister(device)
69+
70+
71+
def device_text(text):
72+
try:
73+
return Device.from_id(int(text))
74+
except ValueError:
75+
return Device(text)
76+
77+
78+
def frame_size(text):
79+
w, h = text.split("x", 1)
80+
return int(w), int(h)
81+
82+
83+
def frame_format(text):
84+
return PixelFormat[text]
85+
86+
87+
def cli():
88+
parser = argparse.ArgumentParser()
89+
parser.add_argument("--log-level", choices=["debug", "info", "warning", "error"], default="info")
90+
parser.add_argument("--nb-buffers", type=int, default=2)
91+
parser.add_argument("--frame-rate", type=float, default=10)
92+
parser.add_argument("--frame-size", type=frame_size, default="640x480")
93+
parser.add_argument("--frame-format", type=frame_format, default="RGB24")
94+
parser.add_argument("device", type=device_text)
95+
return parser
96+
97+
98+
def main(run, args=None):
99+
parser = cli()
100+
args = parser.parse_args(args=args)
101+
fmt = "%(threadName)-10s %(asctime)-15s %(levelname)-5s %(name)s: %(message)s"
102+
logging.basicConfig(level=args.log_level.upper(), format=fmt)
103+
width, height = args.frame_size
104+
aspect = height / width
105+
try:
106+
with args.device as device:
107+
device.set_input(0)
108+
capture = VideoCapture(device, size=args.nb_buffers)
109+
capture.set_fps(args.frame_rate)
110+
capture.set_format(width, height, args.frame_format)
111+
fmt = capture.get_format()
112+
window_width = 1980
113+
window_height = int(window_width * aspect)
114+
run(capture, fmt, window_width, window_height)
115+
except KeyboardInterrupt:
116+
logging.info("Ctrl-C pressed. Bailing out")

examples/video/gl/common_gl.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from OpenGL import GL
2+
3+
TEXTURE_2D = GL.GL_TEXTURE_2D
4+
RGB = GL.GL_RGB
5+
UBYTE = GL.GL_UNSIGNED_BYTE
6+
7+
8+
def create_texture(width, height):
9+
texture_id = GL.glGenTextures(1)
10+
GL.glBindTexture(TEXTURE_2D, texture_id)
11+
GL.glTexParameteri(TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
12+
GL.glTexParameteri(TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
13+
GL.glTexParameteri(TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE)
14+
GL.glTexParameteri(TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE)
15+
# Initial upload (use glTexImage2D only once)
16+
GL.glTexImage2D(
17+
TEXTURE_2D,
18+
0,
19+
RGB,
20+
width,
21+
height,
22+
0,
23+
RGB,
24+
UBYTE,
25+
None,
26+
)
27+
return texture_id
28+
29+
30+
def destroy_texture(texture_id):
31+
GL.glDeleteTextures(1, [texture_id])
32+
33+
34+
def draw_canvas():
35+
GL.glBegin(GL.GL_QUADS)
36+
GL.glTexCoord2f(0.0, 1.0)
37+
GL.glVertex3f(-1.0, -1.0, 0.0)
38+
39+
GL.glTexCoord2f(1.0, 1.0)
40+
GL.glVertex3f(1.0, -1.0, 0.0)
41+
42+
GL.glTexCoord2f(1.0, 0.0)
43+
GL.glVertex3f(1.0, 1.0, 0.0)
44+
45+
GL.glTexCoord2f(0.0, 0.0)
46+
GL.glVertex3f(-1.0, 1.0, 0.0)
47+
GL.glEnd()
48+
49+
50+
def update_texture(texture_id, frame):
51+
data, format = frame.user_data
52+
GL.glTextureSubImage2D(
53+
texture_id,
54+
0,
55+
0,
56+
0,
57+
frame.width,
58+
frame.height,
59+
format,
60+
UBYTE,
61+
data,
62+
)
63+
64+
65+
def init_gl():
66+
GL.glEnable(TEXTURE_2D)
67+
68+
69+
class View:
70+
def __init__(self):
71+
self.texture = None
72+
73+
def __enter__(self):
74+
init_gl()
75+
return self
76+
77+
def render(self, frame):
78+
if frame:
79+
if self.texture is None:
80+
self.texture = create_texture(frame.width, frame.height)
81+
update_texture(self.texture, frame)
82+
draw_canvas()
83+
84+
def __exit__(self, *args):
85+
destroy_texture(self.texture)

examples/video/gl/common_glfw.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import glfw
2+
from common import maybe_frames
3+
4+
5+
class GLFWWindow:
6+
def __init__(self, name, width, height):
7+
self.name = name
8+
self.width = width
9+
self.height = height
10+
self.win = None
11+
12+
def __enter__(self):
13+
glfw.init()
14+
self.win = glfw.create_window(self.width, self.height, self.name, None, None)
15+
glfw.make_context_current(self.win)
16+
return self
17+
18+
def __exit__(self, *args):
19+
pass
20+
21+
22+
def frames(win, capture):
23+
with capture:
24+
stream = maybe_frames(capture)
25+
while not glfw.window_should_close(win.win):
26+
yield next(stream)
27+
glfw.swap_buffers(win.win)
28+
glfw.poll_events()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import struct
2+
3+
import moderngl as mgl
4+
from OpenGL import GL
5+
6+
VS = """
7+
#version 330 core
8+
in vec2 pos;
9+
in vec2 tex;
10+
out vec2 fragTex;
11+
12+
void main() {
13+
gl_Position = vec4(pos, 0.0, 1.0);
14+
fragTex = tex;
15+
}
16+
"""
17+
18+
FS = """
19+
#version 330 core
20+
in vec2 fragTex;
21+
out vec4 fragColor;
22+
uniform sampler2D texS;
23+
24+
void main() {
25+
fragColor = texture(texS, fragTex);
26+
}
27+
"""
28+
29+
# point:2f uv:2f
30+
VERTICES = (-1, -1, 0, 1, 1, -1, 1, 1, 1, 1, 1, 0, -1, 1, 0, 0)
31+
32+
33+
def create_vertex_array(prog):
34+
vertices = struct.pack("16f", *VERTICES)
35+
vbo = prog.ctx.buffer(vertices)
36+
return prog.ctx.vertex_array(prog, [(vbo, "2f 2f", "pos", "tex")])
37+
38+
39+
class View:
40+
def __init__(self, width, height):
41+
self.ctx = None
42+
self.texture = None
43+
self.width = width
44+
self.height = height
45+
46+
def __enter__(self):
47+
self.ctx = mgl.create_context()
48+
self.ctx.enable(mgl.DEPTH_TEST)
49+
self.ctx.viewport = (0, 0, self.width, self.height)
50+
self.program = self.ctx.program(vertex_shader=VS, fragment_shader=FS)
51+
self.vao = create_vertex_array(self.program)
52+
return self
53+
54+
def render(self, frame):
55+
self.ctx.clear(0, 0, 1, 1)
56+
if frame:
57+
if self.texture is None:
58+
self.texture = self.ctx.texture((frame.width, frame.height), 3)
59+
data, fmt = frame.user_data
60+
self.texture.swizzle = "rgb1" if fmt == GL.GL_RGB else "bgr1"
61+
self.texture.write(data)
62+
self.texture.use()
63+
self.vao.render(mgl.TRIANGLE_FAN)
64+
65+
def __exit__(self, *args):
66+
if self.texture:
67+
self.texture.release()
68+
self.texture = None
69+
self.vao.release()
70+
self.program.release()

examples/video/gl/common_rgfw.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import RGFW
2+
from common import maybe_frames
3+
4+
5+
class RGFWWindow:
6+
def __init__(self, name, width, height):
7+
self.name = name
8+
self.rect = RGFW.rect(0, 0, width, height)
9+
self.win = None
10+
11+
def handle_events(self):
12+
while self.win.checkEvent():
13+
if self.win.event.type == RGFW.quit or RGFW.isPressed(self.win, RGFW.Escape):
14+
return False
15+
return True
16+
17+
def __enter__(self):
18+
self.win = RGFW.createWindow(self.name, self.rect, RGFW.CENTER)
19+
return self
20+
21+
def __exit__(self, *args):
22+
self.win.close()
23+
self.win = None
24+
25+
26+
def frames(win, capture):
27+
with capture:
28+
stream = maybe_frames(capture)
29+
while True:
30+
if not win.handle_events():
31+
break
32+
yield next(stream)
33+
win.win.swapBuffers()

examples/video/gl/video_glfw_gl.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from common import main
2+
from common_gl import View
3+
from common_glfw import GLFWWindow, frames
4+
5+
6+
def run(capture, fmt, width, height):
7+
with GLFWWindow("Video GLFW GL", width, height) as win:
8+
with View() as view:
9+
for frame in frames(win, capture):
10+
view.render(frame)
11+
12+
13+
if __name__ == "__main__":
14+
main(run)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from common import main
2+
from common_glfw import GLFWWindow, frames
3+
from common_moderngl import View
4+
5+
6+
def run(capture, fmt, width, height):
7+
with GLFWWindow("Video GLFW GL", width, height) as win:
8+
with View(width, height) as view:
9+
for frame in frames(win, capture):
10+
view.render(frame)
11+
12+
13+
if __name__ == "__main__":
14+
main(run)

examples/video/gl/video_rgfw_gl.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from common import main
2+
from common_gl import View
3+
from common_rgfw import RGFWWindow, frames
4+
5+
6+
def run(capture, fmt, width, height):
7+
with RGFWWindow("Video RGFW GL", width, height) as win:
8+
with View() as view:
9+
for frame in frames(win, capture):
10+
view.render(frame)
11+
12+
13+
if __name__ == "__main__":
14+
main(run)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from common import main
2+
from common_moderngl import View
3+
from common_rgfw import RGFWWindow, frames
4+
5+
6+
def run(capture, fmt, width, height):
7+
with RGFWWindow(f"RGFW ModernGL capture on {capture.device.filename}", width, height) as win:
8+
with View(width, height) as view:
9+
for frame in frames(win, capture):
10+
view.render(frame)
11+
12+
13+
if __name__ == "__main__":
14+
main(run)

0 commit comments

Comments
 (0)