Arkueid / live2d-py

Live2D Library for Python (C++ Wrapper): Supports model loading, lip-sync and basic face rigging.
https://arkueid.github.io/live2d-py-docs/
MIT License
55 stars 10 forks source link

OpenGL获取对应位置的像素信息不正确 #16

Closed ChillRainQ closed 1 week ago

ChillRainQ commented 2 weeks ago

使用Pyside6中的QOpenGLWidget显示live2d,实现透明区域鼠标事件忽略效果,在点击部分透明位置时获取到的alpha通道不为0,而一部分渲染了live2d的位置alpha通道为零。 下面是我的检测透明度的方法:

 def isIngoreArea(self, event):
        height = self.height()
        x, y = event.pos().x(), event.pos().y()
        alpha = gl.glReadPixels(x, height - y, 1, 1, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)[3]
        print(f'alpha: {alpha}')
        return alpha > 0

使用的模型为nn。 python版本为3.12。 windows11操作系统。 live2d.v3

Arkueid commented 1 week ago

之前尝试过 glReadPixels,当时也是读取值和实际值不一样,但是现在不能复现了。

read pixel 测试 live2d.v3 (PyPi) python 3.12 ```python import os import PIL.Image as Image import numpy as np import OpenGL.GL as gl from PySide6.QtCore import QTimerEvent, Qt from PySide6.QtGui import QMouseEvent, QCursor from PySide6.QtOpenGLWidgets import QOpenGLWidget from PySide6.QtWidgets import QApplication import live2d.v3 as live2d import resources def callback(): print("motion end") class Win(QOpenGLWidget): model: live2d.LAppModel def __init__(self) -> None: super().__init__() self.isInLA = False self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) self.a = 0 self.resize(270, 200) self.read = False self.clickX = -1 self.clickY = -1 def initializeGL(self) -> None: # 将当前窗口作为 OpenGL 的上下文 # 图形会被绘制到当前窗口 self.makeCurrent() if live2d.LIVE2D_VERSION == 3: live2d.glewInit() live2d.setGLProperties() # 创建模型 self.model = live2d.LAppModel() # 加载模型参数 # 适用于 3 的模型 self.model.LoadModelJson(os.path.join(resources.RESOURCES_DIRECTORY, "v3/nn/nn.model3.json")) # 以 fps = 30 的频率进行绘图 self.startTimer(int(1000 / 30)) def resizeGL(self, w: int, h: int) -> None: if self.model: # 使模型的参数按窗口大小进行更新 self.model.Resize(w, h) def paintGL(self) -> None: live2d.clearBuffer() self.model.Update() self.model.Draw() if not self.read: self.savePng('screenshot.png') self.read = True def savePng(self, fName): data = gl.glReadPixels(0, 0, self.width(), self.height(), gl.GL_RGBA, gl.GL_UNSIGNED_BYTE) data = np.frombuffer(data, dtype=np.uint8).reshape(self.height(), self.width(), 4) data = np.flipud(data) new_data = np.zeros_like(data) for rid, row in enumerate(data): for cid, col in enumerate(row): color = None new_data[rid][cid] = col if cid > 0 and data[rid][cid - 1][3] == 0 and col[3] != 0: color = new_data[rid][cid - 1] elif cid > 0 and data[rid][cid - 1][3] != 0 and col[3] == 0: color = new_data[rid][cid] if color is not None: color[0] = 255 color[1] = 0 color[2] = 0 color[3] = 255 color = None if rid > 0: if data[rid - 1][cid][3] == 0 and col[3] != 0: color = new_data[rid - 1][cid] elif data[rid - 1][cid][3] != 0 and col[3] == 0: color = new_data[rid][cid] elif col[3] != 0: color = new_data[rid][cid] if color is not None: color[0] = 255 color[1] = 0 color[2] = 0 color[3] = 255 img = Image.fromarray(new_data, 'RGBA') img.save(fName) def timerEvent(self, a0: QTimerEvent | None) -> None: if not self.isVisible(): return if self.a == 0: # 测试一次播放动作和回调函数 self.model.StartMotion("TapBody", 0, live2d.MotionPriority.FORCE.value, onFinishMotionHandler=callback) self.a += 1 local_x, local_y = QCursor.pos().x() - self.x(), QCursor.pos().y() - self.y() if self.isInL2DArea(local_x, local_y): self.isInLA = True # print("in l2d area") else: self.isInLA = False # print("out of l2d area") self.update() def isInL2DArea(self, click_x, click_y): h = self.height() alpha = gl.glReadPixels(click_x, h - click_y, 1, 1, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)[3] return alpha > 0 def mousePressEvent(self, event: QMouseEvent) -> None: x, y = event.scenePosition().x(), event.scenePosition().y() # 传入鼠标点击位置的窗口坐标 # if self.isInL2DArea(x, y): if self.isInLA: self.clickX, self.clickY = x, y print("pressed") def mouseReleaseEvent(self, event): x, y = event.scenePosition().x(), event.scenePosition().y() # if self.isInL2DArea(x, y): if self.isInLA: self.model.Touch(x, y) print("released") def mouseMoveEvent(self, event: QMouseEvent) -> None: x, y = event.scenePosition().x(), event.scenePosition().y() # if self.isInL2DArea(x, y): if self.isInLA: self.move(int(self.x() + x - self.clickX), int(self.y() + y - self.clickY)) if __name__ == "__main__": import sys live2d.init() app = QApplication(sys.argv) win = Win() win.show() app.exec() live2d.dispose() ```

像素检测的轮廓图:

screenshot

ChillRainQ commented 1 week ago

感谢回复以及提供的测试代码。测试代码中的使用self.isInLA进行判断点击位置是否在L2d区域。其中的判断方法

    def isInL2DArea(self, click_x, click_y):
        h = self.height()
        alpha = gl.glReadPixels(click_x, h - click_y, 1, 1, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)[3]
        return alpha > 0

和我提供的方法功能是一致的,在你提供的代码中,点击可以拖动的位置与我的实现是几乎完全相同的。 你提到了不能复现,我不明白具体情况,相同的代码,在我这里依旧是获取到了不准确的alpha值。这可能和具体设备有关系?我认为可能性不大。 代码中绘制的边缘检测被我更改为:不透明区域使用红色,结果图与您的几乎相同(虽然缺少了头)。但是这大概就是原因,因为我的图像是400*600的,从glReadPixels中获取到的图像形状是与渲染出来的形状完全对不上的,即缓冲区中的角色与桌面显示的角色大小不同。

    def savePng(self, fName):
        data = gl.glReadPixels(0, 0, self.width(), self.height(), gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
        data = np.frombuffer(data, dtype=np.uint8).reshape(self.height(), self.width(), 4)
        data = np.flipud(data)
        new_data = np.zeros_like(data)
        for rid, row in enumerate(data):
            for cid, col in enumerate(row):
                if col[3] != 0:
                    new_data[rid][cid] = [255, 0, 0, 255]
        img = Image.fromarray(new_data, 'RGBA')
        img.save(fName)

下面是获取到的透明度不为0的区域图像: screenshot 下面是显示图像与上图的对比图: image

原因应该是读取到的位置实际上有偏移,我点击了一些位置进行了测试,原因就是显示的图像所对应的透明度图像不正确,但目前不知道原因。实际原因是操作系统问题,操作系统中设置了缩放,导致了该问题,您可以尝试修改缩放。然后重复该过程,应该可以得到相同的结果。 经过测试,实际获取鼠标点击位置的像素信息需要经过如下变换才可以获取到正确坐标信息:

x = x * systemScale
y = (height - y) * systemScale

systemScale为系统的缩放因子