Matplotlib Text artist - how to get size? (not using pyplot) - python-3.x

Background
I've moved some code to use matplotlib only, instead of pyplot (The reason is it's generating png files in multi-process with futures, and pyplot isn't thread/process safe that way).
So I'm creating my figure and axis via matplotlib.Figure(), not via pyplot.
I've got some code that draw's a text 'table' on a figure, with one side right justified and the other left. In order to keep the spacing between the two sides constant, I was previously using get_window_extent() on my left-side text artist to figure out the location of the right hand side:
# draw left text at figure normalised coordinates, right justified
txt1 = figure.text(x, y, left_str,
ha='right', color=left_color, fontsize=fontsize)
# get the bounding box of that text - this is in display coords
bbox = txt1.get_window_extent(renderer=figure.canvas.get_renderer())
# get x location for right hand side offset from left's bbox
trans = figure.transFigure.inverted()
xr, _ = trans.transform((bbox.x1 + spacing, bbox.y0))
# draw right side text, using lhs's y value
figure.text(xr, y, right_str,
ha='left', color=right_color, fontsize=fontsize)
Problem
My problem is now that I'm not using pyplot, the above code fails to work, because figure.canvas.get_renderer() fails to return a renderer, as I haven't set one, and am simply using Figure.savefig(path) to save my file when I'm done.
So is there a way to find out the bounding box size of an Artist in a Figure without having a renderer set?
From looking at legend, which allows you to use a bounding box line with variable text size, I'm presuming there is, but I can't find how.
I've had a look at https://matplotlib.org/3.1.3/tutorials/intermediate/artists.html, and also tried matplotlib.artist.getp(txt1) on the above, but didn't find any seemingly helpful properties.

Related

How to add border to an image, choosing color dynamically based on image edge color(s)?

I need to add a border to each image in a set of tightly cropped logos. The border color should either match the most commonly used color along the edge, OR, if there are too many different edge colors, it should choose some sort of average of the edge colors.
Here's an example. In this case, I would want the added border to be the same color as the image "background" (using that term in the lay sense). A significant majority of the pixels along the edges are that color, and there are only two other colors, so the decision algorithm would be able to select that rather drecky greenish tan for the added border (not saying anything bad about the organization behind the logo, mind you).
Does Pillow have any functions to simplify this task?
I found answers that show how to use Pillow to add borders and how to determine the average color of an entire image. But I couldn't find any code that looks only at the edges of an image and finds the predominant color, which color could then be used in the border-adding routine. Just in case someone has already done that work, please point me to it. ('Edges' meaning bands of pixels along the top/bottom/left/right margins of the image, whose height or width would be specified as a percentage of the image's total size.)
Short of pointing me to a gist that solves my whole problem, are there Pillow routines that look at edges and/or that count the colors in a pixel range and put them into an array or what not?
I see here that OpenCV can add a border the duplicates the color of the each outermost pixel along all four edges, but that looks funky—I want a solid-color border. And I'd prefer to stick with Pillow—unless another library can do the whole edge-color-analysis-and-add-border procedure in one step, more or less, in which case, please point it out.
Overwrite the center part of the image with some fixed color, that – most likely – won't be present within the edge. For that, maybe use a color with a certain alpha value. Then, there's a function getcolors, which exactly does, what you're looking for. Sort the resulting list, and get the color with the highest count. (That, often, will be the color we used to overwrite the center part. So check for that, and take the second entry, if needed.) Finally, use ImageOps.expand to add the actual border.
That'd be the whole code:
from PIL import Image, ImageDraw, ImageOps
# Open image, enforce RGB with alpha channel
img = Image.open('path/to/your/image.png').convert('RGBA')
w, h = img.size
# Set up edge margin to look for dominant color
me = 3
# Set up border margin to be added in dominant color
mb = 30
# On an image copy, set non edge pixels to (0, 0, 0, 0)
img_copy = img.copy()
draw = ImageDraw.Draw(img_copy)
draw.rectangle((me, me, w - (me + 1), h - (me + 1)), (0, 0, 0, 0))
# Count colors, first entry most likely is color used to overwrite pixels
n_colors = sorted(img_copy.getcolors(2 * me * (w + h) + 1), reverse=True)
dom_color = n_colors[0][1] if n_colors[0][1] != (0, 0, 0, 0) else n_colors[1][1]
# Add border
img = ImageOps.expand(img, mb, dom_color).convert('RGB')
# Save image
img.save('with_border.png')
That'd be the result for your example:
And, that's some output for another image:
It's up to you to decide, whether there are several dominant colors, which you want to mix or average. You'd need to inspect the n_colors appropriately on the several counts for that. That's quite a lot of work, which is left out here.
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
PyCharm: 2021.1
Pillow: 8.2.0
----------------------------------------

Using tk.Scrollbar to update images in tk.canvas

I developed a small tkinter GUI to display (and export) images stored in a proprietary format. I managed to load the data into a 3D numpy uint8 array. I want to display one slice of that 3D array in a tkinter.canvas. To do so I used ImageTk.PhotoImage.
Underneath the Canvas I inserted a tk.Scrollbar. My goal is to use that scrollbar to let the user actively "scroll" though the 3D Array. Basically when the slider is moved or any of the arrows is pressed the slice corresponding to the slider position should be displayed in the canvas.
Right now I have the issue that I don't understand how to set the range of the scrollbar to my z-Dimension and then how to bind the scrollbar events to the movement or arrow actions to update the canvas.
Right now I don't have example code since this is a more conceptual problem.
Could you point me in the right direction how to solve this?
Best TMC
edit: Photo
Tkinter Gui with Canvas and Scrollbar
The solution is to use tkinter.Scale instead of tkinter.Scrollbar . As a side note, the command within:
scale = tk.Scale(yourTkFrame, from_=min, to=max, orient='horizontal', command=doSomething)
passes two values to the function doSomething.
Using the Scale allows to use the scale.get() method to retrieve the position between min and max values. Those can then be used to set the image corresponding to the selected position on the slide.

Padding text in matplotlib.text

I am looking for a reasonable way to pad text for matplotlib.text. I saw the bbox has a nice pad option but it does only pad the box and does not move the text. Also strangely enough bbox={'linestyle':'None'} does not remove the box line, however, other linestyles are working as expected. Any suggestions?
Use case is relative positioning of text on the plotting area. Using coordinates (1,0.5) puts it too close to the plotting boundary line to look good, and somehow I am reluctant to do (0.99,0.5). That would require change if someone changes fontsize etc.
One option is to use annotate(), which is more flexible than text. You can easily offset the text from the anchor point by a certain number of points (or pixels).
fig, ax = plt.subplots()
ax.annotate('test', xy=(1,0.5), xytext=(-5,0), xycoords='axes fraction', textcoords='offset points', ha='right')

Matplotlib using text instead of marker BUT can't be the best way to do it?

I would just like to make sure I am on the right track here as this seems to be pretty cumbersome for Matplotlib. I want to use a label as a marker on a plot and have it working to some degree. It uses mathtext BUT I wonder if there isn't another way to do it? Here is the code.
import matplotlib.pyplot as plt
x = []
y = []
symbol = "AAPL"
x = range(5)
y = [5,10,12,15,11]
plt.plot(x,y,lw=2.5,color='r',linestyle='solid',marker=r"$ {} $".format(symbol),markersize=25)
plt.show()
I am not 100% sure what you want, but below I have listed a couple of options that I am aware of for putting text into plots at specific locations (essentially what a marker is).
1) you can uses text/characters as markers through unicode. This is done by adding the unicode value within mathtext characters. E.g. take your example code; you can make the marker a unicode character by adding a 'u' before the string (unneeded in python 3) and then a '\u' and a 4 digit number. this will produce a unicode marker. Not all will work, as it depends on whether your system's font supports it. You can find a long list of them here: http://unicode-table.com/en/#latin-extended-a
plt.plot(x,y,lw=2.5,color='r',linestyle='solid',marker=u'$\u2609$',markersize=25)
\u2609 will produce a 'sun', i.e. a circle with a dot in its centre.
2) plt.text(...) using this function you can add text of your choice to the coordinate you specify.
http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.text
I believe the coordinate will correspond to the bottom left corner of the text box, but you can play around with it to make absolutely sure if you want. E.g.
plt.text(x,y,'string',fontsize=18)
However, this must be done on individual points and will not plot a line over the data; it does not work like 'plot' although you could always brute force a line over the top with a subsequent line plot. This method is more of a pain and hardly optimal but it will do the job and is quite flecible if you want a string for a marker.

SetWorldTransform() and font rotation

I'm trying to display text on a Windows control, rotated by 90 degrees, so that it reads from 'bottom to top' so to speak; basically it's the label on the Y axis of a graph.
I got my text to display vertically by changing my coordinate system for the DC by using SetGraphicsMode(GM_ADVANCED) and then using
XFORM transform;
const double angle = 90 * (boost::math::constants::pi<double>() / 180);
transform.eM11 = (FLOAT)cos(angle);
transform.eM12 = (FLOAT)(-sin(angle));
transform.eM21 = (FLOAT)sin(angle);
transform.eM22 = (FLOAT)cos(angle);
transform.eDx = 0.0;
transform.eDy = 0.0;
dc.SetWorldTransform(&transform);
Now when I run my program, the rotated text looks different from the same text when it's shown 'normally' (horizontally). I've tried with a fixed-width (system) font and the default WinXP font. The system font comes out look anti-aliased and the other one looks almost as if it's being drawn in a 1-pixel smaller font than the horizontal version, although they are drawn using the same DC and with no font changes in between. It looks as if Windows detects that I'm drawing a font not along the normal (0 degrees) axis and that it's trying to 'optimize' by anti-aliasing.
Now I don't want any of that. I just want to same text that I draw horizontally to be drawn exactly the same, except 90 degrees rotated, which is possible since it's a rotation of exactly 90 degrees. Does anyone know what's going on and whether I can change this easily to work as I want? I'd hate to have gone through all this trouble and finding up that I will have to resort to rendering to an off-screen bitmap, rotating it using a simple pixel-by-pixel rotation and having to bitblt that into my control :(
Have you tried setting the nEscapement and nOrientation parameters when you create the font instead of using SetWorldTransform? See CreateFont for details.

Resources