31

I draw some rectangles in OpenCV and put text in them. My general approach looks like this:

# Draw rectangle    p1(x,y)    p2(x,y)    Student name box
cv2.rectangle(frame, (500, 650), (800, 700), (42, 219, 151), cv2.FILLED )
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (510, 685), font, 1.0, (255, 255, 255), 1

Everything works so far. The only thing is, that the opacity in all boxes is at 100 %. My question is: How can I change the opacity?

The final result should look like this:

Desired outcome

HansHirse
  • 18,010
  • 10
  • 38
  • 67
Francesco
  • 311
  • 1
  • 3
  • 4
  • 1
    Welcome to Stack Overflow. Please consider [taking the tour](https://stackoverflow.com/tour). Users are much more likely to help if you (1) show some [research effort](https://meta.stackoverflow.com/questions/261592/how-much-research-effort-is-expected-of-stack-overflow-users) on your own, and (2) learn [how to ask](https://stackoverflow.com/help/how-to-ask) questions around here. Please provide a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve) to your _specific_ problem. What have you tried so far? Please show some of your attempts/codes. – HansHirse Jun 06 '19 at 06:36
  • Exactly what you want to ask? At least show what is the input, what you got and what you want it to be (using paint or whatever) then others can help you – Dr Yuan Shenghai Jun 06 '19 at 06:38

4 Answers4

30

I would like to add a small optimization to the @HansHirse answer, Instead of creating the canvas for whole image, we can crop the rectangle first from the src image and then later swap it with the cv2.addWeighted result as:

import cv2
import numpy as np

img = cv2.imread("lena.png")

# First we crop the sub-rect from the image
x, y, w, h = 100, 100, 200, 100
sub_img = img[y:y+h, x:x+w]
white_rect = np.ones(sub_img.shape, dtype=np.uint8) * 255

res = cv2.addWeighted(sub_img, 0.5, white_rect, 0.5, 1.0)

# Putting the image back to its position
img[y:y+h, x:x+w] = res

enter image description here

ZdaR
  • 22,343
  • 7
  • 66
  • 87
  • More a feeling (as I'm too busy to do tests on that right now), but: Since OP wants to add several of these boxes, you'd have to do this in a loop, thus cutting, generating additional sub images, and blending `n` times. So, I'm not sure, if that is really more efficient than producing one large image to blend. Nevertheless, a totally valid way to do it that way! Plus: I've discovered a bug in my solution seeing yours. – HansHirse Jun 06 '19 at 08:02
  • Ok, I need to correct myself: Your solution is definitely better! Cutting out the sub images and blending `a` using `opacity - 1` with `b` using `opacity` gives better results than my approach. – HansHirse Jun 06 '19 at 08:11
  • you can use `numpy.full(shape, fill_value, dtype)` instead of `np.ones(sub_img.shape, dtype=np.uint8) * 255` as `numpy.full(sub_img.shape, 255, np.uint8)` – Sabito stands with Ukraine Jun 24 '20 at 03:58
  • If your shapes overlap, then this solution will highlight the overlap. Compared to creating a single large image and blending with that one image which won't show up the overlaps at all. Also, you could blend directly into `sub_img` with: `cv2.addWeighted(sub_img, 0.5, white_rect, 0.5, 1.0, dst=sub_img)`. This updates the original image and avoids making a copy of sub_img. – Multihunter Jul 22 '21 at 08:44
23

EDIT: Since this answer seems to have some importance, I decided to edit it again, incorporating the proper blending from ZdaR's answer, which initially was an improvement to my original answer (check the timeline if interested). Also, I incorporated Jon's comments to include an example of a non-rectangular shape.


At least from my point of view, built-in functions like cv2.rectangle don't support opacity, even on BGRA images, see here. So, as I described in the linked answer, the only possibility to achieve, what you want, is to use the cv2.addWeighted function. You can simply set up a blank mask image, and draw all possible shapes on that. Doing so, you can also use that as an actual mask to limit the blending to that part only.

An example could be:

import cv2
import numpy as np

# Load image
img = cv2.imread('images/paddington.png')

# Initialize blank mask image of same dimensions for drawing the shapes
shapes = np.zeros_like(img, np.uint8)

# Draw shapes
cv2.rectangle(shapes, (5, 5), (100, 75), (255, 255, 255), cv2.FILLED)
cv2.circle(shapes, (300, 300), 75, (255, 255, 255), cv2.FILLED)

# Generate output by blending image with shapes image, using the shapes
# images also as mask to limit the blending to those parts
out = img.copy()
alpha = 0.5
mask = shapes.astype(bool)
out[mask] = cv2.addWeighted(img, alpha, shapes, 1 - alpha, 0)[mask]

# Visualization
cv2.imshow('Image', img)
cv2.imshow('Shapes', shapes)
cv2.imshow('Output', out)
cv2.waitKey(0)
cv2.destroyAllWindows()

The original Paddington img:

img

The intermediate image to draw the shapes on shapes:

shapes

And, the final result out:

out

After drawing the shapes and blending the images, you can add your texts as before.

Hope that helps!

HansHirse
  • 18,010
  • 10
  • 38
  • 67
  • 1
    This is a good approach for anything-that-is-not-a-rectangle- such as an ellipse or a `cv2.fillPoly` ... in the shape of *a wise bear which keeps a marmalade sandwich in his hat* :) – Jon Jun 06 '21 at 06:43
  • 1
    @Jon I was thinking to update my answer for a while. Your comment now was the trigger. I incorporated the idea of a non-rectangular shape. – HansHirse Jun 07 '21 at 06:39
11

Simply install pyshine and use putBText, it has following inputs and output.

pip install pyshine

"""

Inputs:
    img: cv2 image img
    text_offset_x, text_offset_x: X,Y location of text start
    vspace, hspace: Vertical and Horizontal space between text and box boundaries
    font_scale: Font size
    background_RGB: Background R,G,B color
    text_RGB: Text R,G,B color
    font: Font Style e.g. cv2.FONT_HERSHEY_DUPLEX,cv2.FONT_HERSHEY_SIMPLEX,cv2.FONT_HERSHEY_PLAIN,cv2.FONT_HERSHEY_COMPLEX
          cv2.FONT_HERSHEY_TRIPLEX, etc
    thickness: Thickness of the text font
    alpha: Opacity 0~1 of the box around text
    gamma: 0 by default

Output:
    img: CV2 image with text and background
"""

Example tested on Python3 and complete demonstration is here:

lena.jpg

enter image description here

simple.py

import pyshine as ps
import cv2
image = cv2.imread('lena.jpg')
text  =  'HELLO WORLD!'
image =  ps.putBText(image,text,text_offset_x=20,text_offset_y=20,vspace=10,hspace=10, font_scale=2.0,background_RGB=(0,250,250),text_RGB=(255,250,250))
cv2.imshow('Output', image)
cv2.imwrite('out.jpg',image)
cv2.waitKey(0)

out.jpg

enter image description here

another.py

import pyshine as ps
import cv2
import time
image = cv2.imread('lena.jpg')


text  =  'ID: '+str(123)
image = ps.putBText(image,text,text_offset_x=20,text_offset_y=20,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(1,1,1))
text = str(time.strftime("%H:%M %p"))
image = ps.putBText(image,text,text_offset_x=image.shape[1]-170,text_offset_y=20,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(1,1,1))

text  =  '6842'
image = ps.putBText(image,text,text_offset_x=80,text_offset_y=372,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))

text  =  "Lena Fors'en"
image = ps.putBText(image,text,text_offset_x=80,text_offset_y=425,vspace=20,hspace=10, font_scale=1.0,background_RGB=(20,210,4),text_RGB=(255,255,255))

text  =  'Status: '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-130,text_offset_y=200,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text  =  'On time'
image = ps.putBText(image,text,text_offset_x=image.shape[1]-130,text_offset_y=242,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))


text  =  'Attendence: '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-200,text_offset_y=394,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text  =  '96.2%      '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-200,text_offset_y=436,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))

cv2.imshow('Output', image)
cv2.imwrite('out.jpg',image)
cv2.waitKey(0)

out.jpg

enter image description here

Trees
  • 1,245
  • 10
  • 20
4

A simpler solution (although a bit less efficient in terms of memory) is:

  1. create a copy of the original image
  2. draw the desired shapes/text on the the original image
  3. get the overlay with: alpha*img + (1-alpha)*img_cpy

In this way each original pixel will not change it's value (since alpha*px + (1-alpha)px = px), whereas pixels which were drawn on will be affected by the overlay.

This eliminates the need to perform crops and pesky calculations seen in the other answers.

...and applying to to the OP's code:

frame_cpy = frame.copy()
cv2.rectangle(frame, (500, 650), (800, 700), (42, 219, 151), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (510, 685), font, 1.0, (255, 255, 255), 1)
alpha = 0.4
frame_overlay=cv2.addWeighted(frame, alpha, frame_cpy,1-alpha, gamma=0)
cv2.imshow("overlay result",frame_overlay)
cv2.waitKey(0)

Disclaimer: this answer was inspired by a post on www.pyimagesearch.com

Daniel
  • 1,319
  • 14
  • 19