py: Add cam.py
Add cam.py, which mimics the 'cam' tool. Four rendering backends are added: * null - Do nothing * kms - Use KMS with dmabufs * qt - SW render on a Qt window * qtgl - OpenGL render on a Qt window All the renderers handle only a few pixel formats, and especially the GL renderer is just a prototype. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
This commit is contained in:
parent
06cb7130c4
commit
74ba01121a
6 changed files with 1516 additions and 0 deletions
383
src/py/cam/cam_qtgl.py
Normal file
383
src/py/cam/cam_qtgl.py
Normal file
|
@ -0,0 +1,383 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ['PYOPENGL_PLATFORM'] = 'egl'
|
||||
|
||||
import OpenGL
|
||||
# OpenGL.FULL_LOGGING = True
|
||||
|
||||
from OpenGL import GL as gl
|
||||
from OpenGL.EGL.EXT.image_dma_buf_import import *
|
||||
from OpenGL.EGL.KHR.image import *
|
||||
from OpenGL.EGL.VERSION.EGL_1_0 import *
|
||||
from OpenGL.EGL.VERSION.EGL_1_2 import *
|
||||
from OpenGL.EGL.VERSION.EGL_1_3 import *
|
||||
|
||||
from OpenGL.GLES2.OES.EGL_image import *
|
||||
from OpenGL.GLES2.OES.EGL_image_external import *
|
||||
from OpenGL.GLES2.VERSION.GLES2_2_0 import *
|
||||
from OpenGL.GLES3.VERSION.GLES3_3_0 import *
|
||||
|
||||
from OpenGL.GL import shaders
|
||||
|
||||
from gl_helpers import *
|
||||
|
||||
# libcamera format string -> DRM fourcc
|
||||
FMT_MAP = {
|
||||
'RGB888': 'RG24',
|
||||
'XRGB8888': 'XR24',
|
||||
'ARGB8888': 'AR24',
|
||||
'YUYV': 'YUYV',
|
||||
}
|
||||
|
||||
|
||||
class EglState:
|
||||
def __init__(self):
|
||||
self.create_display()
|
||||
self.choose_config()
|
||||
self.create_context()
|
||||
self.check_extensions()
|
||||
|
||||
def create_display(self):
|
||||
xdpy = getEGLNativeDisplay()
|
||||
dpy = eglGetDisplay(xdpy)
|
||||
self.display = dpy
|
||||
|
||||
def choose_config(self):
|
||||
dpy = self.display
|
||||
|
||||
major, minor = EGLint(), EGLint()
|
||||
|
||||
b = eglInitialize(dpy, major, minor)
|
||||
assert(b)
|
||||
|
||||
print('EGL {} {}'.format(
|
||||
eglQueryString(dpy, EGL_VENDOR).decode(),
|
||||
eglQueryString(dpy, EGL_VERSION).decode()))
|
||||
|
||||
check_egl_extensions(dpy, ['EGL_EXT_image_dma_buf_import'])
|
||||
|
||||
b = eglBindAPI(EGL_OPENGL_ES_API)
|
||||
assert(b)
|
||||
|
||||
def print_config(dpy, cfg):
|
||||
|
||||
def getconf(a):
|
||||
value = ctypes.c_long()
|
||||
eglGetConfigAttrib(dpy, cfg, a, value)
|
||||
return value.value
|
||||
|
||||
print('EGL Config {}: color buf {}/{}/{}/{} = {}, depth {}, stencil {}, native visualid {}, native visualtype {}'.format(
|
||||
getconf(EGL_CONFIG_ID),
|
||||
getconf(EGL_ALPHA_SIZE),
|
||||
getconf(EGL_RED_SIZE),
|
||||
getconf(EGL_GREEN_SIZE),
|
||||
getconf(EGL_BLUE_SIZE),
|
||||
getconf(EGL_BUFFER_SIZE),
|
||||
getconf(EGL_DEPTH_SIZE),
|
||||
getconf(EGL_STENCIL_SIZE),
|
||||
getconf(EGL_NATIVE_VISUAL_ID),
|
||||
getconf(EGL_NATIVE_VISUAL_TYPE)))
|
||||
|
||||
if False:
|
||||
num_configs = ctypes.c_long()
|
||||
eglGetConfigs(dpy, None, 0, num_configs)
|
||||
print('{} configs'.format(num_configs.value))
|
||||
|
||||
configs = (EGLConfig * num_configs.value)()
|
||||
eglGetConfigs(dpy, configs, num_configs.value, num_configs)
|
||||
for config_id in configs:
|
||||
print_config(dpy, config_id)
|
||||
|
||||
config_attribs = [
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 0,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_NONE,
|
||||
]
|
||||
|
||||
n = EGLint()
|
||||
configs = (EGLConfig * 1)()
|
||||
b = eglChooseConfig(dpy, config_attribs, configs, 1, n)
|
||||
assert(b and n.value == 1)
|
||||
config = configs[0]
|
||||
|
||||
print('Chosen Config:')
|
||||
print_config(dpy, config)
|
||||
|
||||
self.config = config
|
||||
|
||||
def create_context(self):
|
||||
dpy = self.display
|
||||
|
||||
context_attribs = [
|
||||
EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL_NONE,
|
||||
]
|
||||
|
||||
context = eglCreateContext(dpy, self.config, EGL_NO_CONTEXT, context_attribs)
|
||||
assert(context)
|
||||
|
||||
b = eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context)
|
||||
assert(b)
|
||||
|
||||
self.context = context
|
||||
|
||||
def check_extensions(self):
|
||||
check_gl_extensions(['GL_OES_EGL_image'])
|
||||
|
||||
assert(eglCreateImageKHR)
|
||||
assert(eglDestroyImageKHR)
|
||||
assert(glEGLImageTargetTexture2DOES)
|
||||
|
||||
|
||||
class QtRenderer:
|
||||
def __init__(self, state):
|
||||
self.state = state
|
||||
|
||||
def setup(self):
|
||||
self.app = QtWidgets.QApplication([])
|
||||
|
||||
window = MainWindow(self.state)
|
||||
window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
|
||||
window.show()
|
||||
|
||||
self.window = window
|
||||
|
||||
def run(self):
|
||||
camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read)
|
||||
camnotif.activated.connect(lambda x: self.readcam())
|
||||
|
||||
keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)
|
||||
keynotif.activated.connect(lambda x: self.readkey())
|
||||
|
||||
print('Capturing...')
|
||||
|
||||
self.app.exec()
|
||||
|
||||
print('Exiting...')
|
||||
|
||||
def readcam(self):
|
||||
running = self.state['event_handler'](self.state)
|
||||
|
||||
if not running:
|
||||
self.app.quit()
|
||||
|
||||
def readkey(self):
|
||||
sys.stdin.readline()
|
||||
self.app.quit()
|
||||
|
||||
def request_handler(self, ctx, req):
|
||||
self.window.handle_request(ctx, req)
|
||||
|
||||
def cleanup(self):
|
||||
self.window.close()
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QWidget):
|
||||
def __init__(self, state):
|
||||
super().__init__()
|
||||
|
||||
self.setAttribute(Qt.WA_PaintOnScreen)
|
||||
self.setAttribute(Qt.WA_NativeWindow)
|
||||
|
||||
self.state = state
|
||||
|
||||
self.textures = {}
|
||||
self.reqqueue = {}
|
||||
self.current = {}
|
||||
|
||||
for ctx in self.state['contexts']:
|
||||
|
||||
self.reqqueue[ctx['idx']] = []
|
||||
self.current[ctx['idx']] = []
|
||||
|
||||
for stream in ctx['streams']:
|
||||
fmt = stream.configuration.pixel_format
|
||||
size = stream.configuration.size
|
||||
|
||||
if fmt not in FMT_MAP:
|
||||
raise Exception('Unsupported pixel format: ' + str(fmt))
|
||||
|
||||
self.textures[stream] = None
|
||||
|
||||
num_tiles = len(self.textures)
|
||||
self.num_columns = math.ceil(math.sqrt(num_tiles))
|
||||
self.num_rows = math.ceil(num_tiles / self.num_columns)
|
||||
|
||||
self.egl = EglState()
|
||||
|
||||
self.surface = None
|
||||
|
||||
def paintEngine(self):
|
||||
return None
|
||||
|
||||
def create_surface(self):
|
||||
native_surface = c_void_p(self.winId().__int__())
|
||||
surface = eglCreateWindowSurface(self.egl.display, self.egl.config,
|
||||
native_surface, None)
|
||||
|
||||
b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context)
|
||||
assert(b)
|
||||
|
||||
self.surface = surface
|
||||
|
||||
def init_gl(self):
|
||||
self.create_surface()
|
||||
|
||||
vertShaderSrc = '''
|
||||
attribute vec2 aPosition;
|
||||
varying vec2 texcoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);
|
||||
texcoord.x = aPosition.x;
|
||||
texcoord.y = 1.0 - aPosition.y;
|
||||
}
|
||||
'''
|
||||
fragShaderSrc = '''
|
||||
#extension GL_OES_EGL_image_external : enable
|
||||
precision mediump float;
|
||||
varying vec2 texcoord;
|
||||
uniform samplerExternalOES texture;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_FragColor = texture2D(texture, texcoord);
|
||||
}
|
||||
'''
|
||||
|
||||
program = shaders.compileProgram(
|
||||
shaders.compileShader(vertShaderSrc, GL_VERTEX_SHADER),
|
||||
shaders.compileShader(fragShaderSrc, GL_FRAGMENT_SHADER)
|
||||
)
|
||||
|
||||
glUseProgram(program)
|
||||
|
||||
glClearColor(0.5, 0.8, 0.7, 1.0)
|
||||
|
||||
vertPositions = [
|
||||
0.0, 0.0,
|
||||
1.0, 0.0,
|
||||
1.0, 1.0,
|
||||
0.0, 1.0
|
||||
]
|
||||
|
||||
inputAttrib = glGetAttribLocation(program, 'aPosition')
|
||||
glVertexAttribPointer(inputAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertPositions)
|
||||
glEnableVertexAttribArray(inputAttrib)
|
||||
|
||||
def create_texture(self, stream, fb):
|
||||
cfg = stream.configuration
|
||||
fmt = cfg.pixel_format
|
||||
fmt = str_to_fourcc(FMT_MAP[fmt])
|
||||
w, h = cfg.size
|
||||
|
||||
attribs = [
|
||||
EGL_WIDTH, w,
|
||||
EGL_HEIGHT, h,
|
||||
EGL_LINUX_DRM_FOURCC_EXT, fmt,
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT, fb.fd(0),
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride,
|
||||
EGL_NONE,
|
||||
]
|
||||
|
||||
image = eglCreateImageKHR(self.egl.display,
|
||||
EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
None,
|
||||
attribs)
|
||||
assert(image)
|
||||
|
||||
textures = glGenTextures(1)
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textures)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image)
|
||||
|
||||
return textures
|
||||
|
||||
def resizeEvent(self, event):
|
||||
size = event.size()
|
||||
|
||||
print('Resize', size)
|
||||
|
||||
super().resizeEvent(event)
|
||||
|
||||
if self.surface is None:
|
||||
return
|
||||
|
||||
glViewport(0, 0, size.width() // 2, size.height())
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self.surface is None:
|
||||
self.init_gl()
|
||||
|
||||
for ctx_idx, queue in self.reqqueue.items():
|
||||
if len(queue) == 0:
|
||||
continue
|
||||
|
||||
ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx)
|
||||
|
||||
if self.current[ctx_idx]:
|
||||
old = self.current[ctx_idx]
|
||||
self.current[ctx_idx] = None
|
||||
self.state['request_prcessed'](ctx, old)
|
||||
|
||||
next_req = queue.pop(0)
|
||||
self.current[ctx_idx] = next_req
|
||||
|
||||
stream, fb = next(iter(next_req.buffers.items()))
|
||||
|
||||
self.textures[stream] = self.create_texture(stream, fb)
|
||||
|
||||
self.paint_gl()
|
||||
|
||||
def paint_gl(self):
|
||||
b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context)
|
||||
assert(b)
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT)
|
||||
|
||||
size = self.size()
|
||||
|
||||
for idx, ctx in enumerate(self.state['contexts']):
|
||||
for stream in ctx['streams']:
|
||||
if self.textures[stream] is None:
|
||||
continue
|
||||
|
||||
w = size.width() // self.num_columns
|
||||
h = size.height() // self.num_rows
|
||||
|
||||
x = idx % self.num_columns
|
||||
y = idx // self.num_columns
|
||||
|
||||
x *= w
|
||||
y *= h
|
||||
|
||||
glViewport(x, y, w, h)
|
||||
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, self.textures[stream])
|
||||
glDrawArrays(GL_TRIANGLE_FAN, 0, 4)
|
||||
|
||||
b = eglSwapBuffers(self.egl.display, self.surface)
|
||||
assert(b)
|
||||
|
||||
def handle_request(self, ctx, req):
|
||||
self.reqqueue[ctx['idx']].append(req)
|
||||
self.update()
|
Loading…
Add table
Add a link
Reference in a new issue