A raster image is a 2d matrix of colour values. Each matrix entry is called a pixel.
A vector image is a set of instructions for drawing.
Rendering images on screen or in print often requires a rasterisation step. The main advantage of vector art is that it delays rasterisation until the very last step. This avoids lossy intermediate resampling, and allows rasterisation to be carried out by a specialised device (e.g. a display driver or a printer) that has knowledge of the final pixel size.
For example, the same vector art might be rendered quite differently on a small phone screen than on a large screen. Printers in particular, are full of little hacks and tweaks that allow them to obtain much nicer results. In some cases, rasterisation can even be avoided altogether, e.g. with old-school plotters, or PostScript fonts.
If you start from raster data, e.g. a photograph, then stick with raster formats.
If you start from vector data, e.g. a set of points or a collection of lines, then use vector formats.
In some cases it makes sense to combine vector and raster art (which many vector formats allow). For example, you can add vector annotations to a photograph. Conversly, if you have vector data but find that the vector instructions take very long to execute (for example because you are showing a huge amount of data) it can be helpful to rasterise part of the image early.
Resizing a raster image requires resampling, which usually means a loss of quality. Vector art can be scaled up and down without loss.
However, line widths and font sizes should be appropriate for the format (e.g. 10pt fonts in a paper, 24pt on a poster) and so may need to be adjusted after you resize.
It's also worth remembering that human vision isn't scale invariant: we expect a certain level of detail regardless of size. When scaling up a tiny graphic, even losslessly, you may need to add additional detail or texture. Conversely, when shrinking graphics down, you may need to simplify.
The size of a pixel varies depending on the screen, but the human eye stays the same! Design your figures using real physical sizes, and leave pixels up to the screen or printer.
Define font sizes and line widths in points, where 1 point equals 1/72nd of an inch.
If you do need to get into pixel land:
(Annoyingly, "PPI" is also used to mean "points per inch", i.e. 72 by the modern definition.)
In the code below, we create a figure that's 4 inches wide and 2 inches high, using matplotlib. We don't tell matplotlib anything about DPI or PPI, so it uses its default settings of 100 DPI (where "dots" are translated to "pixels", so it's more of a PPI really). To show the image boundaries, we set a gray background colour (using the web code #cccccc
).
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 100, 1000)
y = np.sin(x) + 1.1 * np.cos(1.2 * (x + 1.3))
fig = plt.figure(figsize=(4, 2), facecolor='#cccccc')
fig.subplots_adjust(0.13, 0.22, 0.99, 0.99)
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.plot(x, y)
fig.savefig('./figures/res-1.png')
plt.close(fig)
We can include this image below (note that Jupyter will cache this image, so it won't change directly if you modify the code above):
We can see the pixel size of the image using e.g. PIL (the Python Image Library):
import PIL.Image
img = PIL.Image.open('./figures/res-1.png')
print(img.size)
(288, 144)
This shows us the image is 4 * 72 = 288
pixels wide, and 2 * 72 = 144
pixels high.
Now get your ruler out! On my PC screen at home, the image measures about 3 by 1.5 inches, which is not a million miles off but not great either. On my old laptop it's 2 inches wide. If I was working on a fancier laptop or looking at my phone, it might render as just 1 inch wide.
This illustrates a first problem with raster graphics, even when staying on screen: The actual display size depends on the hardware, and more expensive hardware does not necessarily increase readability!
We can ask matplotlib to rasterise at a higher resolution by increasing the DPI setting used*:
(Note that we're adjusting DPI, which is for printing not for screens. This is a sign that we're doing it wrong!)
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 100, 1000)
y = np.sin(x) + 1.1 * np.cos(1.2 * (x + 1.3))
fig = plt.figure(figsize=(4, 2), facecolor='#cccccc')
fig.subplots_adjust(0.13, 0.22, 0.99, 0.99)
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.plot(x, y)
fig.savefig('./figures/res-2.png', dpi=200)
plt.close(fig)
This results in the following image:
Rendered 1:1 on the screen, the whole figure now looks a lot bigger, including larger fonts and thicker lines. This is the intended behaviour: Both images are the same size (4 inches wide), and so a 1pt line in the second figure needs more pixels (or dots) than in the first.
img = PIL.Image.open('./figures/res-2.png')
print(img.size)
(800, 400)
Using PIL we see that the image size is now 4 inches * 200 DPI = 800 pixels
wide, by 2 * 200 = 400
pixels high.
What if we want a figure that looks bigger on screen, but still has "normal" font sizes? The answer is to simply tell matplotlib that we want the figure to be bigger, by increasing the figure size, without touching the DPI:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 100, 1000)
y = np.sin(x) + 1.1 * np.cos(1.2 * (x + 1.3))
fig = plt.figure(figsize=(8, 4), facecolor='#cccccc')
fig.subplots_adjust(0.07, 0.11, 0.99, 0.99)
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.plot(x, y)
fig.savefig('./figures/res-3.png')
plt.close(fig)
img = PIL.Image.open('./figures/res-3.png')
print(img.size)
(576, 288)
Now the image is wider, but fonts are rendered with the same number of pixels as in the first example:
Finally, we can avoid a lot of these steps by saving in a vector format:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 100, 1000)
y = np.sin(x) + 1.1 * np.cos(1.2 * (x + 1.3))
fig = plt.figure(figsize=(8, 4), facecolor='#cccccc')
fig.subplots_adjust(0.07, 0.11, 0.99, 0.99)
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.plot(x, y)
fig.savefig('./figures/res-4.svg')
plt.close(fig)
Now get your ruler out again. On my screen this image renders as approximately 8 1/8 inches wide, so almost exactly the intended size. Apparently, whatever PPI setting the SVGs in jupyter notebook are rasterised with almost matches my screen's size.
Other applications, e.g. Inkscape, seem to talk to the display drivers to get your screen's PPI exactly.
Opening this document in Inkscape and using Zoom to 1:1
renders it as exactly 8 inches wide.
Again, consider the rasterisation steps involved: