Exploring the mathematics behind camera lens apertures and f-stop ratios with a bit of help from Python.

## Camera Lenses and Apertures

Anyone attempting to take their photography beyond what is possible with a smartphone or simple fully automatic camera will have to get their head round the baffling topic of lens apertures and f-stop ratios, one of the three settings you can use to set exposure.

The other two are shutter speed and ISO sensitivity. Shutter speed is straightforward to understand: on most cameras each shutter speed is double or half the one next to it, and expressed in fractions of a second or whole seconds. For example 1/500 is half of 1/250 so an exposure at that speed will let in half as much light. ISO is equally straightforward: the higher the number the more sensitive the sensor is to light, so ISO 800 is twice as sensitive as ISO 400, although intermediate settings between exact halves or doubles are offered by many cameras.

The aperture, or area of the lens through which light is allowed to pass, also works on the same half or double principle. However, the numbers used to set the aperture don't run in a nice logical sequence like shutter speeds or ISO. This is a list of apertures offered by a typical lens. As you read left to right each aperture lets in half the amount of light as the previous one.

1.4 | 2 | 2.8 | 4 | 5.6 | 8 | 11 | 16 | 22 |

A lens has metal blades which open and close to vary the amount of light which can pass through, leaving a roughly circular opening. With modern lenses this is controlled electronically but in older lenses the process is purely mechanical, as seen in this 1960s vintage Nikon lens. In the image on the left the lens is set to f2, the maximum, and on the right is set to f16, the minimum.

So what do these f-numbers mean? Quite simply they are the ratio of the focal length of the lens to the diameter of the circular opening. (The focal length, 50mm on this lens, determines the angle of view, lower focal lengths having a wider angle of view, longer focal lengths having a narrower angle of view.)

The lens shown here has a focal length of 50mm, so when set to f2 the aperture blades leave an opening of 50mm / 2, or 25mm. When set to f16 the opening is 50mm / 16, or just 3.125mm. The seemingly strange sequence of ratios shown in table above is chosen so that each one leaves an opening half the area of the previous one and therefore lets in half as much light, the same straightforward principle as used for shutter speeds and ISO.

As a more formal illustration, take a look at the diagram below. It represents the widest three apertures of a 50mm lens, this time with a maximum aperture of f1, ie. the maximum opening is the same as the focal length. (Such lenses are rare, big, heavy and expensive but handy for illustrative purposes.) The second aperture, f1.4, has half the area and lets in half as much light as f1. The next, f2, again halves the area and light. I'll stop there before things get tedious and the diagram gets too big, but the repeated halving carries on to the minimum aperture of the specific lens. Most lenses have a minimum aperture of f16 or f22, but a few go to f32 or even f64.

Furthermore, some lenses provide intermediate apertures, sometimes halves but more usually thirds. This is a series of photos of a Fujifilm lens set to f4 and f5.6, as well as third-stop intermediate apertures.

Even if you choose to set the exposure on your camera manually, as many people do despite the highly sophisticated automation of most modern cameras, the information above is really all you'll ever need to know. However, if you are interested in how the ratios and their corresponding light levels are calculated then you might find this simple little Python project might be to your liking.

## The Project

There are actually three numbers which can be used to describe a given lens aperture.

- The focal length ratio or f-number described and illustrated above
- The amount of light let through by the lens as a proportion of the maximum
- The number of "stops" reduction in light, each stop representing half the amount of light as the previous one. The word "stop" is a bit of photographic jargon dating back to the early days of photography, a stop being a strip of metal with various sized holes in it which slid across the lens. As an example you might say "f2.8 is 2 stops slower than f1.4".

This project will output three lists

- A range of f-stop ratios with their corresponding light levels and "stops"
- A list of light levels with their corresponding stops and ratios
- A list of stops with their corresponding light levels and ratios

The project consists of a single Python file, fstopratios.py. It can be downloaded as a zip, or you can clone/download the Github repository if you prefer.

Source Code Links

## The Code

This is the fstopratios.py file in its entirety.

fstopratios.py

import numpy as np

def main():

print("-----------------")

print("| codedrome.com |")

print("| F-Stop Ratios |")

print("-----------------\n")

from_ratios()

# from_light()

# from_stops()

def ratios_to_light(ratios):

return 1 / ratios**2

def light_to_stops(light):

return -(np.log2(1 / light))

def light_to_ratios(light):

return 1/light**0.5

def stops_to_light(stops):

return 1/2**-stops

def from_ratios():

ratios = np.arange(1, 5.7, 0.1)

light = ratios_to_light(ratios)

stops = light_to_stops(light)

print("------------------------")

print("| f | light | stops |")

print("------------------------")

for i in range(0, len(ratios)):

print(f"| {ratios[i]:.1f} | {light[i]:.3f} | {stops[i]:.2f} |")

print("------------------------")

def from_light():

light = np.fromfunction(lambda n: 1/(2**n), (10,))

stops = light_to_stops(light)

ratios = light_to_ratios(light)

print("----------------------------")

print("| light | stops | f |")

print("----------------------------")

for i in range(0, len(light)):

print(f"| {light[i]:.6f} | {stops[i]:.1f} | {ratios[i]:>5.2f} |")

print("----------------------------")

def from_stops():

stops = np.arange(-0, -10, -1)

light = stops_to_light(stops)

ratios = light_to_ratios(light)

print("--------------------------")

print("| stops | light | f |")

print("--------------------------")

for i in range(0, len(stops)):

print(f"| {stops[i]:>4.1f} | {light[i]:.3f} | {ratios[i]:>4.1f} |")

print("--------------------------")

if __name__ == "__main__":

main()

This project uses NumPy which provides a typed array which is much faster and more capable than a Python list, as well as being easier to work with. You don't need to be an expert on NumPy to understand this code, you can just install it with pip and away you go but the main thing to remember for this project is that you can carry out arithmetic on all the elements of a NumPy array in a single statement rather than a loop. This is the NumPy page on PyPi.

https://pypi.org/project/numpy/

After importing numpy we have the main function which has three function calls. These can be commented/uncommented to run the functions in turn.

Next are four simple one-line functions which convert ratios, light levels or stops to one of the other values. Each of these take a NumPy array and return another NumPy array, doing their stuff using single statements as mentioned above. I'll now run through these one at a time.

## ratios_to_light

This function takes an array of f-numbers as its argument and returns the fractions of light, compared to f1, a lens set to that aperture will let through.

It's a simple calculation: squaring the ratio gives us the denominator of the required fraction when the numerator is 1. Here are a few examples.

Ratio | Calculation | Result |
---|---|---|

1 | 1 / 1^{2} | 1 |

1.4 | 1 / 1.4^{2} | 0.5102 |

2 | 1 / 2^{2} | 0.25 |

2.8 | 1 / 2.8^{2} | 0.1276 |

I mentioned above that f-numbers and their corresponding levels of light are only approximations. We can see that already with two of the ratios: the light levels for f1.4 and f2.8 aren't exactly half the previous levels.

## light_to_stops

Here we take a light levels as a fraction of 1 and calculate the corresponding numbers of stops. The calculation involves the use of the log2 function which tells us what power we need to raise 2 to to obtain a given number. Here are a few examples.

Light | 1 / Light | Negative log2 of Previous Column |
---|---|---|

1 | 1 | -0 |

0.5 | 2 | -1 |

0.25 | 4 | -2 |

0.125 | 8 | -3 |

So here each reduction in the light level by one half corresponds to another stop reduction in exposure.

## light_to_ratios

This function is the inverse of ratios_to_light.

Light | Light^{0.5} | 1 / Previous Column |
---|---|---|

1 | 1 | 1 |

0.5 | 0.707 | 1.414 |

0.25 | 0.5 | 2 |

0.125 | 0.354 | 2.825 |

Again a few approximations creep in compared to the standard sequence 1, 1.4, 2, 2.8 . . .

## stops_to_light

This is the last of the set of four functions which take arays of values and calculate one of the other corresponding values. This one is the inverse of light_to_stops.

The stops argument represents reductions so are negative. We therefore raise 2 to the *negative* power of stops, ie a positive power. We then divide 1 by this to get the reciprocal, the value we want.

Here are the examples.

stops | 2^{-stops} | 1 / previous column |
---|---|---|

-0 | 1 | 1 |

-1 | 2 | 0.5 |

-2 | 4 | 0.25 |

-3 | 8 | 0.125 |

Now let's move on to a set of functions which produce tables showing corresponding values for ratios (f-numbers), light levels and reduction in stops.

## from_ratios

The first of these iterates a range of ratios from f1 to f5.6 at 0.1 intervals (obviously you can expand this but I didn't want to take up huge amounts of space with tedious output), grabs the corresponding reductions in light and stops using the appropriate functions I described above, and prints them out in a table format.

Now run the program with this command:

Running the Program

python3 fstopratios.py

This is the output.

As I have already mentioned the f-number sequence does not correspond exactly to the nominal reductions in light levels or stops. For example f1.4 is nominally a 1 stop reduction from f1.0 but is actually only 0.97 stops, or 0.51 of the light level. These differences are very small and in practice make no noticeable difference to the image.

I have no idea how lens manufacturers actually deal with this. They could make lenses with the exact apertures corresponding to the standard scale and accept that the actual amounts of light getting through are slightly off. On the other hand they could make lenses which allow exactly half or double the amount of light through and engrave approximations of the real ratios on the lens to comply with the conventional sequence. If you know please leave a comment, although it's entirely possible that different companies take different approaches.

## from_light

This function follows the same pattern as the previous one but this time iterating light levels and calculating the corresponding stops and ratios.

A lambda function is used to calculate 1, 0.5, 0.25, 0.125 etc., this function being passed to the NumPy method fromfunction. This is a useful thing to know as it enables you to populate a NumPy array using any function you need without resorting to a for loop.

Uncomment from_light in main and run the program again.

## from_stops

Finally we have the from_stops function which calculates light and ratios from a range of stops from -0 to -10. Uncomment from_stops in main and run the program one more time.

*Me on Mastodon (not Twitter!) where I post new CodeDrome articles and other interesting or useful programming stuff*