pythonpysidepyside6pyqtgraph

how to have a fixed azimuth and elevation of a GLTextItem


I am trying to remove the banner effect from the pyqtgraph.opengl.GLTextItem such that whenever the camera angle changes, the angle of the text item is fixed. I can get the size of the text item to be fixed, but I am unable to figure out how to stop the text item from facing the camera view.

import numpy as np
from PySide6 import QtWidgets, QtGui
import pyqtgraph as pg
from pyqtgraph.opengl import GLViewWidget, GLTextItem

def euler_matrix(azimuth_deg, elevation_deg):
    a, e = np.radians([azimuth_deg, elevation_deg])
    ca, sa = np.cos(a), np.sin(a)
    ce, se = np.cos(e), np.sin(e)
    Rz = np.array([[ ca, -sa, 0],
                   [ sa,  ca, 0],
                   [  0,   0, 1]])
    Rx = np.array([[1,  0,   0],
                   [0, ce, -se],
                   [0, se,  ce]])
    return Rx @ Rz

class CustomText(GLTextItem):
    def __init__(self, *args, worldTextHeight=0.1, **kwargs):
        super().__init__(*args, **kwargs)
        self.worldTextHeight=worldTextHeight
        tf = np.eye(4)
        tf[:3, :3] = euler_matrix(0, 0)
        self.fixed=tf
        self.setTransform(tf)
        
    def update_text_rotation(self):
        self.setTransform(self.fixed)

    def update_text_size(self):
        unit_per_pixel = self.view().pixelSize(np.array(self.pos))
        pixel_height = max(1, int(self.worldTextHeight / unit_per_pixel))
        f = QtGui.QFont(self.font)
        f.setPixelSize(pixel_height)
        self.font = f

    def paint(self):
        self.update_text_rotation()
        self.update_text_size()
        super().paint()


class CustomGLView(GLViewWidget):
    def __init__(self):
        super().__init__()
        self.text_item = CustomText(pos=(0,0,0), text="text item", worldTextHeight=1)
        self.addItem(self.text_item)
                
        self.addItem(pg.opengl.GLAxisItem())
        self.setCameraPosition(distance=5)

    
if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    view = CustomGLView()
    view.show()
    app.exec()

Solution

  • Unfortunately, there is no built-in way to do this. I made a workaround for this, by first rendering the text as an image using PIL, then converting that image to a numpy array, and displaying that numpy array as a pyqtgraph.opengl.ImageItem.

    Code

    import numpy as np
    import pyqtgraph.opengl as gl
    from PySide6.QtWidgets import QApplication
    from PIL import Image, ImageDraw, ImageFont
    import sys
    
    
    def string_to_image_array(text, font="arial.ttf", font_size=70, image_size=(400, 100), bg_color=(0, 0, 0, 0), text_color=(255, 255, 255, 255)):
        """
        Convert a string into a numpy array representing an image of the text.
    
        text (str): The string to render.
        font_size (int): Font size for the text.
        image_size (tuple): (width, height) of the output image.
        bg_color (tuple): Background color (R, G, B).
        text_color (tuple): Text color (R, G, B).
    
        Returns:
            numpy.ndarray: Image array (H x W x 3) for RGB.
        """
        img = Image.new("RGBA", image_size, bg_color)
        draw = ImageDraw.Draw(img)
    
        # Specify a path to a .ttf file
        font = ImageFont.truetype(font, font_size)
    
        # Calculate text position (centered)
        text_width, text_height = draw.textbbox((0, 0), text, font=font)[2:]
        x = (image_size[0] - text_width) // 2
        y = (image_size[1] - text_height) // 2
    
        # Draw the text
        draw.text((x, y), text, font=font, fill=text_color)
    
        # Convert to numpy array
        img_array = np.array(img)
    
        return img_array
    
    
    app = QApplication(sys.argv)
    
    # Create a 3D GLViewWidget
    view = gl.GLViewWidget()
    view.setGeometry(0, 0, 800, 600)
    view.show()
    
    # Add grid
    grid = gl.GLGridItem()
    grid.setSize(10, 10)
    grid.setSpacing(1, 1)
    view.addItem(grid)
    
    # Add text
    text = "Hello, World!"
    img_array = string_to_image_array(text)
    
    text_img = gl.GLImageItem(img_array)
    factor = 0.01
    text_img.scale(factor, factor, factor)
    text_img.rotate(90, 0, 1, 0)
    text_img.translate(0, 0, 2)
    view.addItem(text_img)
    
    if __name__ == '__main__':
        sys.exit(app.exec())
    

    Explanation

    First a gl.GLViewWidget() is created to which a grid is added for visualisation. text specifies the text. Then img_array is set to an RGBA image of the text using string_to_image_array. This function creates an image of the text and converts it to a numpy array. Then this array is drawn as an image using gl.GLImageItem. It is scaled so that it isn't huge, and rotated and translated to appear above the grid.

    Customisation

    To get a better resolution, increase font_size, and the x-coordinate of image_size such that the image still contains the entire text. The image does of course get a bit bigger if you do this so change factor to scale the image accordingly. The size of the text can be changed by adjusting factor. To adjust the position and orientation change the arguments of text_img.rotate and text_img.translate accordingly. The color of the text can be changed by passing the desired color to the function. The font can be changed by passing a path to a ttf-file with a font.