14

I would like to get the bounding box (dimensions) around some text in a matplotlib figure. The post here, helped me realize that I can use the method text.get_window_extent(renderer) to get the bounding box, but I have to supply the correct renderer. Some backends do not have the method figure.canvas.get_renderer(), so I tried matplotlib.backend_bases.RendererBase() to get the renderer and it did not produce satisfactory results. Here is a simple example

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

fig = plt.figure()
ax = plt.subplot()
txt = fig.text(0.15,0.5,'afdjsklhvvhwd', fontsize = 36)
renderer1 = fig.canvas.get_renderer()
renderer2 = mpl.backend_bases.RendererBase()
bbox1 = txt.get_window_extent(renderer1)
bbox2 = txt.get_window_extent(renderer2)
rect1 = Rectangle([bbox1.x0, bbox1.y0], bbox1.width, bbox1.height, \
    color = [0,0,0], fill = False)
rect2 = Rectangle([bbox2.x0, bbox2.y0], bbox2.width, bbox2.height, \
    color = [1,0,0], fill = False)
fig.patches.append(rect1)
fig.patches.append(rect2)
plt.draw()

This produces the following plot:

image

Clearly the red box is too small. I think a Paul's answer here found the same issue. The black box looks great, but I cannot use the MacOSX backend, or any others that do not have the method figure.canvas.get_renderer().

In case it matters, I am on Mac OS X 10.8.5, Matplotlib 1.3.0, and Python 2.7.5

8
  • The problem is that the text size is not known until it has been rendered. The osx backend issues are due to the way the quartz event loop works you can only render with in a call back (iirc). What do you need the bounding box for? If it is to draw a box, I think annotate will do that for you. Commented Mar 26, 2014 at 17:32
  • and make sure the dpi is correct on all of the renderers (screen-space is in pixels). Commented Mar 26, 2014 at 17:33
  • I am using the bounding box to help me determine how close I can place a text box to something else. I typically place the text at some arbitrary location, get it's bounding box, and then move the text to it's final location, based partly on the size of the bounding box. As for the dpi, I know how to set the dpi in the figure.canvas.get_renderer(), but how do I set the dpi for mpl.backend_bases.RendererBase()? Commented Mar 26, 2014 at 21:58
  • I suspect you could do a lot of what you want with clever use of ha and va. Also take a look at how the tight bounding box code works. Commented Mar 26, 2014 at 23:24
  • I do not think horizontal alignment (ha) and/or vertical alignment (va) will suffice in many cases. Say I want to have two lines of text stacked on top of each other. One line is long, the other line is short. I can use ha to make sure the horizontal center of the top line is directly above the horizontal center of the bottom line. But what if I want the left side of the long line to be at a certain coordinate? I need to know where the left edge of the bounding box is. Thanks for the suggestion about the tight bounding box code. I will take a look. Commented Mar 27, 2014 at 2:01

3 Answers 3

11

Here is my solution/hack. @tcaswell suggested I look at how matplotlib handles saving figures with tight bounding boxes. I found the code for backend_bases.py on Github, where it saves the figure to a temporary file object simply in order to get the renderer from the cache. I turned this trick into a little function that uses the built-in method get_renderer() if it exists in the backend, but uses the save method otherwise.

def find_renderer(fig):

    if hasattr(fig.canvas, "get_renderer"):
        #Some backends, such as TkAgg, have the get_renderer method, which 
        #makes this easy.
        renderer = fig.canvas.get_renderer()
    else:
        #Other backends do not have the get_renderer method, so we have a work 
        #around to find the renderer.  Print the figure to a temporary file 
        #object, and then grab the renderer that was used.
        #(I stole this trick from the matplotlib backend_bases.py 
        #print_figure() method.)
        import io
        fig.canvas.print_pdf(io.BytesIO())
        renderer = fig._cachedRenderer
    return(renderer)

Here are the results using find_renderer() with a slightly modified version of the code in my original example. With the TkAgg backend, which has the get_renderer() method, I get:

TkAgg

With the MacOSX backend, which does not have the get_renderer() method, I get:

MacOSX

Obviously, the bounding box using MacOSX backend is not perfect, but it is much better than the red box in my original question.

Sign up to request clarification or add additional context in comments.

2 Comments

It failed with the error 'AttributeError: 'FigureCanvasMac' object has no attribute 'print_pdf' on my MAC.
I also had this issue of no print_pdf() attribute, but changing to print_figure() (as in the comment above that line) worked for me.
1

If you would like to get the tight bounding box of a rotated text region, here is a possible solution.

# generate text layer
def text_on_canvas(text, myf, ro, margin = 1):
    axis_lim = 1

    fig = plt.figure(figsize = (5,5), dpi=100)
    plt.axis([0, axis_lim, 0, axis_lim])

    # place the left bottom corner at (axis_lim/20,axis_lim/20) to avoid clip during rotation
    aa = plt.text(axis_lim/20.,axis_lim/20., text, ha='left', va = 'top', fontproperties = myf, rotation = ro, wrap=True)
    plt.axis('off')
    text_layer = fig2img(fig) # convert to image
    plt.close()

    we = aa.get_window_extent()
    min_x, min_y, max_x, max_y = we.xmin, 500 - we.ymax, we.xmax, 500 - we.ymin
    box = (min_x-margin, min_y-margin, max_x+margin, max_y+margin)

    # return coordinates to further calculate the bbox of rotated text
    return text_layer, min_x, min_y, max_x, max_y 


def geneText(text, font_family, font_size, style):
    myf = font_manager.FontProperties(fname=font_family, size=font_size)
    ro = 0

    if style < 8: # rotated text
        # no rotation, just to get the minimum bbox
        htext_layer, min_x, min_y, max_x, max_y = text_on_canvas(text, myf, 0)

        # actual rotated text
        ro = random.randint(0, 90)
        M = cv2.getRotationMatrix2D((min_x,min_y),ro,1)
        # pts is 4x3 matrix
        pts = np.array([[min_x, min_y, 1],[max_x, min_y, 1],[max_x, max_y, 1],[min_x, max_y,1]]) # clockwise
        affine_pts = np.dot(M, pts.T).T
        #print affine_pts
        text_layer, _, _, _, _ = text_on_canvas(text, myf, ro)

        visualize_points(htext_layer, pts)
        visualize_points(text_layer, affine_pts)

        return text_layer  

    else:
        raise NotImplementedError


fonts = glob.glob(fonts_path + '/*.ttf')
ret = geneText('aaaaaa', fonts[0], 80, 1)

The result looks like this: The first one is un-rotated, and the second one is rotated text region. The full code snippet is here.

enter image description here enter image description here

Comments

0

The _get_renderer() method from the Figure object gives me satisfactory results:

from matplotlib.figure import Figure
import matplotlib.pyplot as plt

fig1, ax1 = plt.subplots()

plotted_text = ax1.text(0.5, 0.5, "afdjsklhvvhwd")

renderer1 = fig1.canvas.get_renderer()
bb1 = plotted_text.get_window_extent(renderer=renderer1).transformed(ax1.transData.inverted())

text_width1 = bb1.width


fig2 = Figure()
ax2 = fig2.subplots()

plotted_text2 = ax2.text(0.5, 0.5, "afdjsklhvvhwd")

renderer2 = fig2._get_renderer()
bb2 = plotted_text2.get_window_extent(renderer=renderer2).transformed(ax2.transData.inverted())

text_width2 = bb2.width

1 Comment

Hello, please don't just submit code in your answer(s), add some details as to why you think this is the optimal solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.