Pyqtgraph. Draw text label next to the rectangle - python

I am trying to draw a grid of rectangles with text labels on each rectangle. I am using this code:
import pyqtgraph as pg
win = pg.GraphicsWindow()
vb = win.addViewBox(col=0, row=0)
board = ['1234',
'abcd',
'efgh']
def draw_board(board):
for j, row in enumerate(board):
for i, cell in enumerate(row):
r = pg.QtGui.QGraphicsRectItem(i, -j, 0.9, 0.9)
r.setPen(pg.mkPen((0, 0, 0, 100)))
r.setBrush(pg.mkBrush((50, 50, 200)))
vb.addItem(r)
t = pg.TextItem(cell, (255, 255, 255), anchor=(i, -j))
vb.addItem(t)
pg.QtGui.QApplication.exec_()
draw_board(board)
For some reason labels seem to be drawn in a completely different coordinate system that even uses different scale. What is the easy way to put my labels in the middle of the respective rectangles?

Solved the problem with this code:
t_up = pg.TextItem(cell, (255, 255, 255), anchor=(0, 0))
t_up.setPos(i + 0.5, -j + 0.5)

Related

Python 2D Array - Referencing specific part of grid

I have a 10x10 grid currently filled with zeros and rendered to the screen using Pygame's freetype module.
How would I render the list contents in my for loop rather than hardcode a '0' string?
import pygame
import pygame.freetype
# Define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
# Create a 2 dimensional array. A two dimensional
# array is simply a list of lists.
grid = [[0 for x in range(10)] for y in range(10)]
# Initialize pygame
pygame.init()
# Set the HEIGHT and WIDTH of the screen
WINDOW_SIZE = [255, 255]
screen = pygame.display.set_mode(WINDOW_SIZE)
# Loop until the user clicks the close button.
done = False
# -------- Main Program Loop -----------
while not done:
# Set the screen background
screen.fill(BLACK)
# Draw the grid
for row in range(10):
for column in range(10):
pygame.freetype.Font(None, 32).render_to(screen, (column * 32, row * 32), '0', WHITE)
pygame.display.flip()
pygame.quit()
You can use enumerate here to get the coordinates of your grid with any size you want:
for i, row in enumerate(grid):
for j, item in enumerate(row):
pygame.freetype.Font(None, 32).render_to(screen, (j * 32, i * 32), str(item), WHITE)
This way you don't need to know the size of the grid upfront.
Then simply get the string representation of cell item with str().

OpenCV's `getTextSize` and `putText` return wrong size and chop letters with lower pixels

I have the following Python code:
FONT = cv2.FONT_HERSHEY_SIMPLEX
FONT_SCALE = 1.0
FONT_THICKNESS = 2
bg_color = (255, 255, 255)
label_color = (0, 0, 0)
label = 'Propaganda'
label_width, label_height = cv2.getTextSize(label, FONT, FONT_SCALE, FONT_THICKNESS)[0]
label_patch = np.zeros((label_height, label_width, 3), np.uint8)
label_patch[:,:] = bg_color
I create a new blank image, with the size returned by getTextSize, and then, I add the text at the bottom-left point, according to the docs, which is x = 0, y = (height - 1) and with the same font, scale and thickness parameters used for getTextSize
cv2.putText(label_patch, label, (0, label_height - 1), FONT, FONT_SCALE, label_color, FONT_THICKNESS)
But when I use imshow or imwrite on the image label_patch, this is the results I get:
It can easily be seen that lowercase p and lowercase g are cut in the middle, such that g and a cannot even be distinguished. How can I make OpenCV's getTextSize return the correct size, and how can I make OpenCV's putText start drawing the text from the actual lowest point?
Found the solution to my problem, so sharing it here.
It turns out that there is another parameter that getTextSize returns, which is the baseline. It should have been taken into account when creating the box: the box height should be label_height + baseline:
(label_width, label_height), baseline = cv2.getTextSize(label, FONT, FONT_SCALE, FONT_THICKNESS)
label_patch = np.zeros((label_height + baseline, label_width, 3), np.uint8)
label_patch[:,:] = bg_color
Now, adding the text at the same point as before, which means that the baseline pixels will remain below (the point has actually moved one pixel down, as advised by #api55):
cv2.putText(label_patch, label, (0, label_height), FONT, FONT_SCALE, label_color, FONT_THICKNESS)
And the result:

Python PIL - transparent and dashed lines?

I am trying to superimpose a grid onto an image so that I can use it to position text. I am using PIL (Python) to draw the grid. Ideally I would have dashed and reasonably transparent minor gridlines so that I can see through to the image, and solid and less transparent major gridlines.
A working example of what I am trying to do is below.
My questions refer to the following lines (simplified for presentation):
draw.line( (10, 0, 10, imgHeight), fill = (180, 180, 255, 1) )
draw.line( (10, 0, 10, imgHeight), fill = (180, 180, 255, 200) )
1) I understood the fourth parameter of the fill controls the transparency of the line, with 0 being completely transparent and 255 being completely opaque. However, I cannot detect any difference between a value or 1 or 200, and indeed, when I use 1, it hides the text beneath it when I thought I would be able to see it.
2) How can you make dashed lines in PIL? I have not seen how. Is it possible?
Finally, I am relatively new to programming and python, if there are any tips you can offer on the below MWE to bring it up to a better standard, I would be most grateful.
Note: The example below uses red lines for the major gridlines, but I would like them to be the same colour as the minor gridlines, with the minor gridlines transparent and dashed.
import PIL
from PIL import Image, ImageFont, ImageDraw
grid = 'on'
minor = 5
major = 50
font = ImageFont.truetype('arial.ttf', 10)
textColor = (38, 23, 255)
img = Image.open('2013MCS7.jpg')
draw = ImageDraw.Draw(img)
def gridlines(img, gridWidth, color, lineWidth=1, direction='b', label='n', labelRepeat=None, LabelFont='arial', labelFontSize=10):
'''
Draws gridlines on an image.
img : the image to be modified
gridwith : (int > 0) : size of gridlines in pixels
color : tuple of length 3 or 4 (4th argument controls transparency) : color of gridlines and labels
lineWidth : (int > 0) : width of gridlines
direction : ('v','h','b') : specifies either vetical gridlines, horizontal gridlines or both
label : ('y','n') : turns grid labels on or off
labelRepeat :
'''
imgWidth, imgHeight = img.size
draw = ImageDraw.Draw(img)
textColor = (color[0], color[1], color[2])
textFont = ImageFont.truetype(LabelFont+'.ttf', labelFontSize)
# add gridlines
if direction.lower() == 'b' or direction.lower() == 'v':
for v in range(1, int(imgWidth/gridWidth)+1):
draw.line( (v*gridWidth, 0, v*gridWidth, imgHeight), fill = color, width = lineWidth )
if direction.lower() == 'b' or direction.lower() == 'h':
for h in range(1, int(imgHeight/gridWidth)+1):
draw.line( (0, h*gridWidth, imgWidth, h*gridWidth), fill = color, width = lineWidth )
# add labels
if label.lower() == 'y':
for v in range(1, int(imgWidth/gridWidth)+1):
for h in range(1, int(imgHeight/gridWidth)+1):
if v == 1:
draw.text( ( 3, 1+h*gridWidth), str(h), fill = textColor, font = textFont )
if h == 1:
draw.text( ( 1+v*gridWidth, 3), str(v), fill = textColor, font = textFont )
if labelRepeat is not None:
if ( h % labelRepeat == 0 ) and ( v % labelRepeat == 0 ):
draw.text( ( 1+v*gridWidth, 1+h*gridWidth), '('+str(h)+','+str(v)+')', fill = textColor, font = textFont )
# draw gridlines
if grid == 'on':
gridlines(img, minor, (180, 180, 255, 1))
gridlines(img, major, (255, 0, 0, 100), label='Y', labelRepeat=3)
# populate form
draw.text( (6*major+2*minor+3, 6*major+5*minor+2), 'econ_total', fill=textColor, font=font)
draw.text( (6*major+2*minor+3, 7*major+1*minor+2), 'notional_taxed', fill=textColor, font=font)
draw.text( (6*major+2*minor+3, 7*major+7*minor+2), 'notional_employer', fill=textColor, font=font)
draw.text( (6*major+2*minor+3, 8*major+4*minor+3), 'supca_total', fill=textColor, font=font)
draw.text( (6*major+2*minor+3, 9*major+2*minor-1), 'cgt_exempt_sb_ret', fill=textColor, font=font)
draw.text( (6*major+2*minor+3, 9*major+7*minor+0), 'cgt_exempt_sb_15yr', fill=textColor, font=font)
del draw
img.save('marked-up - 2013MCS7.jpg')
First, replace grid = 'on' with a boolean. You should be using True or False, not string comparisons, to check if the setting is enabled with if grid:.
Second, PIL doesn't have the ability to draw dashed lines by default. To draw a dashed line you have to draw several small lines spaced apart from each other. However, for what you're trying to do, dashed lines should not be necessary; Setting the opacity of a line can be done with a high degree of control.
Finally, you should be able to achieve very fine control over the opacity of the lines with your approach. The gridlines() function is doing a lot of extra work; It redraws the image for no reason, draws text, etc. So what's most likely going on is your grid lines are being drawn over each other several times, causing them to get more and more opaque.
If it turns our your grid drawing is fine, then you should save your image as a PNG, not a JPEG. JPEG does not render red very well since it's designed to save photographic imagery and that means it dedicates more information to storing greens and blues, which our eyes see more of.

How to redraw text on gradient background in pygame?

I am using python 2.7.1 with pygame 1.9.1 on 64-bit win7. I am using the gradient code from http://www.pygame.org/wiki/GradientCode to draw my background. I then display text like so:
countText = font.render( "%d" % secs_left, 1, (255, 0, 0))
countRect = countText.get_rect()
countRect.y, countRect.centerx = yPos, screen.get_rect().width/2
screen.blit(countText, countRect)
pygame.display.flip()
I use this to display a countdown timer, but the problem is the numbers draw on top of one another. I can specify a background color in the font.render() call that will solve this, but then I get another problem where the solid background doesn't match the gradient background.
I think this can be solved by saving a copy of the gradient background in another surface, and then drawing the relevant portion of the saved surface onto the background before drawing the next number, but I am not sure how to do this.
I can save a copy of the gradient background like this:
# save a surface with same size and gradient as background
bg_image = pygame.Surface(screen.get_size())
fill_gradient(bg_image, BG_COLOR, GRADIENT_COLOR)
But how do I select the relevant portion of bg_image and draw it to my main screen background? I tried something like this, doing a screen.blit() to try and erase the current countdown number before blitting the new number, but it doesn't work:
countText = usefont.render( "%d" % secs_left, 1, (255, 0, 0))
countRect = countText.get_rect()
countRect.y, countRect.centerx = yPos, screen.get_rect().width/2
screen.blit(bg_image, (0,0), countRect)
screen.blit(countText, countRect)
pygame.display.flip()
Would this be the best approach (with code that works)? Or is there a better way to do this?
Thanks for your help.
use pygame.display.set_mode(size, 0, 32) to get your screen surface, it supports transparent.
Use a surface to save your background, we name it bg_surface
You get a new text surface every time you do font.render, name it txt_surface
When each time before pygame.display.flip(), blit the bg_surface and then txt_surface to screen.
It's that what you need? If you are rendering a counter, it's enougth to blit the whole background every frame.
I can use Surface.set_clip() to restrict the area being updated to just the rectangle containing the countdown text:
countText = usefont.render( "%d" % secs_left, 1, (255, 0, 0))
countRect = countText.get_rect()
countRect.y, countRect.centerx = yPos, screen.get_rect().width/2
screen.set_clip(countRect) # Allow updates only to area in countRect
screen.blit(bg_image, (0,0)) # bg_img will only be drawn within countRect
screen.blit(countText, countRect)
pygame.display.flip()
Found this post while researching a minor variation of the same question. Here is a running generic solution created after combining information from various places. In this case, the bg_image can be used directly, since the title that is (also) placed on top does not overlap with the count. I used a different method of generating the gradient, but that is not important. Especially when using screen.copy() to create a reference surface to use during the restore.
#!python3
import os
import numpy as np
import pygame
from pygame.locals import *
size = 640, 480
pygame.init()
os.environ['SDL_VIDEO_CENTERED'] = '1'
screen = pygame.display.set_mode(size, NOFRAME, 0)
width, height = screen.get_size()
pygame.event.set_blocked(MOUSEMOTION) # keep our queue cleaner
# build gradient background
colors = np.random.randint(0, 255, (2, 3))
topcolor = np.array(colors[0], copy=0)
bottomcolor = np.array(colors[1], copy=0)
diff = bottomcolor - topcolor
column = np.arange(height, dtype=np.float32) / height # create array from 0.0 to 1.0 triplets
column = np.repeat(column[:, np.newaxis], [3], 1)
column = topcolor + (diff * column).astype(np.int) # create a single column of gradient
column = column.astype(np.uint8)[np.newaxis, :, :] # make column a 3d image column by adding X
column = pygame.surfarray.map_array(screen, column) # 3d array into 2d array
gradient = np.resize(column, (width, height)) # stretch the column into a full image
bg_image = pygame.surfarray.make_surface(gradient)
screen.blit(bg_image, (0, 0))
usefont = pygame.font.Font(None, 144)
# add content that does not get erased with the count down value
title_surf = usefont.render('Counting…', True, (200, 100, 50))
title_rect = title_surf.get_rect()
title_rect.topleft = (20, 5)
screen.blit(title_surf, title_rect)
pygame.display.flip()
savedSurface = screen.copy() # when no convenient surface to restore from
pygame.time.set_timer(USEREVENT, 1000)
screen_center = int(width / 2), int(height / 2)
savedRect = screen_center, (0, 0) # First time, nothing to restore
secs_left = 11
while secs_left > 0:
event = pygame.event.wait()
if event.type in (QUIT, KEYDOWN, MOUSEBUTTONDOWN):
break
if event.type == USEREVENT:
# screen.blit(bg_image, savedRect, savedRect) # restore background
screen.blit(savedSurface, savedRect, savedRect) # restore background
secs_left -= 1
countText = usefont.render('%d' % secs_left, 1, (255, 0, 0))
countRect = countText.get_rect()
countRect.center = screen_center
savedRect = screen.blit(countText, countRect)
pygame.display.flip()

How do I draw text at an angle using python's PIL?

Using Python I want to be able to draw text at different angles using PIL.
For example, imagine you were drawing the number around the face of a clock. The number 3 would appear as expected whereas 12 would we drawn rotated counter-clockwise 90 degrees.
Therefore, I need to be able to draw many different strings at many different angles.
Draw text into a temporary blank image, rotate that, then paste that onto the original image. You could wrap up the steps in a function. Good luck figuring out the exact coordinates to use - my cold-fogged brain isn't up to it right now.
This demo writes yellow text on a slant over an image:
# Demo to add rotated text to an image using PIL
import Image
import ImageFont, ImageDraw, ImageOps
im=Image.open("stormy100.jpg")
f = ImageFont.load_default()
txt=Image.new('L', (500,50))
d = ImageDraw.Draw(txt)
d.text( (0, 0), "Someplace Near Boulder", font=f, fill=255)
w=txt.rotate(17.5, expand=1)
im.paste( ImageOps.colorize(w, (0,0,0), (255,255,84)), (242,60), w)
It's also usefull to know our text's size in pixels before we create an Image object. I used such code when drawing graphs. Then I got no problems e.g. with alignment of data labels (the image is exactly as big as the text).
(...)
img_main = Image.new("RGB", (200, 200))
font = ImageFont.load_default()
# Text to be rotated...
rotate_text = u'This text should be rotated.'
# Image for text to be rotated
img_txt = Image.new('L', font.getsize(rotate_text))
draw_txt = ImageDraw.Draw(img_txt)
draw_txt.text((0,0), rotate_text, font=font, fill=255)
t = img_value_axis.rotate(90, expand=1)
The rest of joining the two images together is already described on this page.
When you rotate by an "unregular" angle, you have to improve this code a little bit. It actually works for 90, 180, 270...
Here is a working version, inspired by the answer, but it works without opening or saving images.
The two images have colored background and alpha channel different from zero to show what's going on. Changing the two alpha channels from 92 to 0 will make them completely transparent.
from PIL import Image, ImageFont, ImageDraw
text = 'TEST'
font = ImageFont.truetype(r'C:\Windows\Fonts\Arial.ttf', 50)
width, height = font.getsize(text)
image1 = Image.new('RGBA', (200, 150), (0, 128, 0, 92))
draw1 = ImageDraw.Draw(image1)
draw1.text((0, 0), text=text, font=font, fill=(255, 128, 0))
image2 = Image.new('RGBA', (width, height), (0, 0, 128, 92))
draw2 = ImageDraw.Draw(image2)
draw2.text((0, 0), text=text, font=font, fill=(0, 255, 128))
image2 = image2.rotate(30, expand=1)
px, py = 10, 10
sx, sy = image2.size
image1.paste(image2, (px, py, px + sx, py + sy), image2)
image1.show()
The previous answers draw into a new image, rotate it, and draw it back into the source image. This leaves text artifacts. We don't want that.
Here is a version that instead crops the area of the source image that will be drawn onto, rotates it, draws into that, and rotates it back. This means that we draw onto the final surface immediately, without having to resort to masks.
def draw_text_90_into (text: str, into, at):
# Measure the text area
font = ImageFont.truetype (r'C:\Windows\Fonts\Arial.ttf', 16)
wi, hi = font.getsize (text)
# Copy the relevant area from the source image
img = into.crop ((at[0], at[1], at[0] + hi, at[1] + wi))
# Rotate it backwards
img = img.rotate (270, expand = 1)
# Print into the rotated area
d = ImageDraw.Draw (img)
d.text ((0, 0), text, font = font, fill = (0, 0, 0))
# Rotate it forward again
img = img.rotate (90, expand = 1)
# Insert it back into the source image
# Note that we don't need a mask
into.paste (img, at)
Supporting other angles, colors etc is trivial to add.
Here's a fuller example of watermarking diagonally. Handles arbitrary image ratios, sizes and text lengths by calculating the angle of the diagonal and font size.
from PIL import Image, ImageFont, ImageDraw
import math
# sample dimensions
pdf_width = 1000
pdf_height = 1500
#text_to_be_rotated = 'Harry Moreno'
text_to_be_rotated = 'Harry Moreno (morenoh149#gmail.com)'
message_length = len(text_to_be_rotated)
# load font (tweak ratio based on your particular font)
FONT_RATIO = 1.5
DIAGONAL_PERCENTAGE = .5
diagonal_length = int(math.sqrt((pdf_width**2) + (pdf_height**2)))
diagonal_to_use = diagonal_length * DIAGONAL_PERCENTAGE
font_size = int(diagonal_to_use / (message_length / FONT_RATIO))
font = ImageFont.truetype(r'./venv/lib/python3.7/site-packages/reportlab/fonts/Vera.ttf', font_size)
#font = ImageFont.load_default() # fallback
# target
image = Image.new('RGBA', (pdf_width, pdf_height), (0, 128, 0, 92))
# watermark
opacity = int(256 * .5)
mark_width, mark_height = font.getsize(text_to_be_rotated)
watermark = Image.new('RGBA', (mark_width, mark_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark)
draw.text((0, 0), text=text_to_be_rotated, font=font, fill=(0, 0, 0, opacity))
angle = math.degrees(math.atan(pdf_height/pdf_width))
watermark = watermark.rotate(angle, expand=1)
# merge
wx, wy = watermark.size
px = int((pdf_width - wx)/2)
py = int((pdf_height - wy)/2)
image.paste(watermark, (px, py, px + wx, py + wy), watermark)
image.show()
Here it is in a colab https://colab.research.google.com/drive/1ERl7PiX6xKy5H9EEMulBKPgglF6euCNA?usp=sharing you should provide an example image to the colab.
I'm not saying this is going to be easy, or that this solution will necessarily be perfect for you, but look at the documentation here:
http://effbot.org/imagingbook/pil-index.htm
and especially pay attention to the Image, ImageDraw, and ImageFont modules.
Here's an example to help you out:
import Image
im = Image.new("RGB", (100, 100))
import ImageDraw
draw = ImageDraw.Draw(im)
draw.text((50, 50), "hey")
im.rotate(45).show()
To do what you really want you may need to make a bunch of separate correctly rotated text images and then compose them all together with some more fancy manipulation. And after all that it still may not look great. I'm not sure how antialiasing and such is handled for instance, but it might not be good. Good luck, and if anyone has an easier way, I'd be interested to know as well.
If you a using aggdraw, you can use settransform() to rotate the text. It's a bit undocumented, since effbot.org is offline.
# Matrix operations
def translate(x, y):
return np.array([[1, 0, x], [0, 1, y], [0, 0, 1]])
def rotate(angle):
c, s = np.cos(angle), np.sin(angle)
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
def draw_text(image, text, font, x, y, angle):
"""Draw text at x,y and rotated angle radians on the given PIL image"""
m = np.matmul(translate(x, y), rotate(angle))
transform = [m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2]]
draw = aggdraw.Draw(image)
draw.settransform(transform)
draw.text((tx, ty), text, font)
draw.settransform()
draw.flush()

Categories