# ShadowFinder: mapping where a shadow could have fallen

> ShadowFinder is a MIT licensed Python tool and Google Colab notebook that estimates which points on the Earth's surface could have produced a shadow of a given length, given an object's height, a date and a time. It is built for open source geolocation work and it returns a band of possibilities rather than a pin on the map.

**bellingcat/ShadowFinder** — Find possible locations of shadows around the world

- Repository: https://github.com/bellingcat/ShadowFinder
- Website: https://colab.research.google.com/github/Bellingcat/ShadowFinder/blob/main/ShadowFinderColab.ipynb
- Stars: 622 · Forks: 54
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/bellingcat-shadowfinder

## Geolocating from a shadow rather than from metadata

ShadowFinder answers a narrow investigative question: given the height of an object, the length of the shadow it casts, a date and a time, where on Earth could that shadow exist? The tool is used for geolocation, and the project description places it in the open source research category alongside a command line interface, a Python package and a Google Colab notebook.

The audience is investigators and researchers who have an image with a shadow in it and no other positional clue. A flagpole, a fence post or a person on a pavement all cast shadows whose geometry is governed by the sun's altitude at that moment. If the date and time are known to within a couple of hours, the shadow length fixes the sun's altitude, and the sun's altitude fixes a band of latitudes and longitudes that could have produced it.

That reasoning is why the output is a band rather than a point. The tool draws bright regions on a map of the Earth, and the width of those regions reflects how many places on the planet share the sun angle needed for that shadow. Narrowing that band is the job of the investigator, who combines it with other clues in the image.

## The mechanism is a sun angle search across timezones

The data flow runs through four ingredients visible in the project layout. `suncalc` supplies the solar position maths, `timezonefinder` with the `pytz` extra supplies the timezone grid, `cartopy` and `matplotlib` draw the result, and `pandas` and `numpy` handle the scanning of candidate locations.

The central quantity is the sun's altitude angle above the horizon, which the tool can take either directly or derive from the pair of object height and shadow length. A date and a time are then evaluated in each candidate timezone, since the same wall clock moment corresponds to different sun angles at different longitudes. The result is the set of surface points whose solar altitude matches the required angle, and those points are what appears as the bright band on the map.

The dependency list also shows `fire` in the project, which is the command line parsing library behind the `shadowfinder` entry point declared in `pyproject.toml` as `shadowfinder.__main__:main`. Python versions from 3.10 up to but not including 4 are supported.

A `timezone_grid.json` file sits at the root of the repository and the package can load it from disk, generate it, and save it. The point of that file is that the timezone lookup is the expensive part of the computation, and the notebook path exists to pay that cost once.

## The Colab notebook is the fastest way to a first answer

The project offers three entry points, and the documentation is clear that the notebook is the intended starting point. The usage section states that no installation is necessary and points to the Google Colab notebook hosted in the repository, so an investigator can open it in a browser and work through a scenario without touching a local Python environment. That matters for the intended users, since journalists and researchers often do not control the machine they are working on.

The notebook is also the only path that gets the caching benefit without extra effort. The Python library example loads a pre-generated timezone grid when it exists and falls back to generating and saving it when the load raises `FileNotFoundError`, which is the recommended pattern to copy into any script that will be run repeatedly.

For a one-off investigation, opening the notebook and adjusting the parameters is the whole workflow. For anything scripted, the Python API is the better target.

## Driving the Python API: load a grid, then set a scenario

The library usage is short. You construct a finder, make sure a timezone grid exists on disk, describe the scenario, run the search, and ask for the figure:

```python
from shadowfinder import ShadowFinder

finder = ShadowFinder()

# Use a pre-generated timezone grid to save time
# Attempt to load a timezone grid and on a failure generate the grid and save to file
try:
    finder.load_timezone_grid()
except FileNotFoundError:
    finder.generate_timezone_grid()
    finder.save_timezone_grid() # timezone_grid.json
```

The scenario description carries the constraints that decide what you get back. You supply either the pair of `object_height` and `shadow_length` or the `sun_altitude_angle` directly, and you declare whether the timestamp is local or UTC:

```python
# Set up the scenario
# Provide either object_height and shadow_length OR sun_altitude_angle
finder.set_details(
    date_time=date_time, # datetime object with no timezone awareness
    object_height=object_height, # object height in arbitrary units
    shadow_length=shadow_length, # shadow length in arbitrary units
    time_format=time_type, # string, either 'local' or 'utc'
    sun_altitude_angle=sun_altitude_angle, # altitude angle of the sun, in degrees above the horizon
)
```

The comments in that example are the specification. The `date_time` argument is a datetime object with no timezone awareness, so the interpretation of the wall clock time is controlled entirely by the `time_format` setting, which is either `local` or `utc`. Height and shadow length are described as being in arbitrary units, which means only their ratio carries meaning: the pair must be expressed in the same unit, since a height in metres with a shadow in feet would silently produce a different sun angle.

The last two steps run the search and return the Matplotlib figure:

```python
# Run the finder
finder.find_shadows()

# Access the resulting figure
fig = finder.plot_shadows()
```

## The command line rebuilds the timezone grid every run

The CLI is the fastest to type and the slowest to run, and the project says so directly. An important note in the usage section explains that using the CLI is not the recommended way of using ShadowFinder because it is quite slow, since there is currently no caching strategy for the timezone grid, so the grid is generated on every run, which the note calls resource intensive.

The commands themselves are simple. The height and shadow length come first, then the date and time:

```shell
shadowfinder find 10 5 2024-02-29 13:59:59 --time_format=utc
```

If the sun angle is already known, the second command takes that instead of the two lengths:

```shell
shadowfinder find_sun 50 2024-02-29 13:59:59 --time_format=utc
```

The argument order is documented in the text after each command: `OBJECT_HEIGHT`, `SHADOW_LENGTH`, `DATE` and `TIME` for the first, and `SUN_ALTITUDE_ANGLE`, `DATE` and `TIME` for the second. The full option list is available through the built in help:

```shell
shadowfinder find --help
shadowfinder find_sun --help
```

For repeated runs over many candidate timestamps, the grid rebuild dominates the cost, which is why the library path with a saved grid file is the practical choice for batch work.

## Unit confusion and naive datetimes are the two ways to get it wrong

The library is forgiving in one direction and unforgiving in another. Height and shadow length are documented as arbitrary units, so a mistyped unit pair produces a plausible map with a wrong answer rather than an error. If the shadow is twice the object height, the sun is low; if the numbers are swapped, the tool will happily look for a high sun. Nothing in the usage documentation validates the physical plausibility of the ratio, so the estimate of the object's height is the input that most needs human judgment.

The second trap is timezone handling. The datetime passed to `set_details` has no timezone awareness, and the `time_format` string is what tells the tool how to read it. A timestamp that was recorded as UTC and then run with `local`, or the reverse, shifts the sun angle and moves the candidate band away from the true location. For an investigation where the time comes from a camera metadata field, deciding which flag matches that field is the first thing to get right.

There is a third limit worth stating plainly. The output is a band of possible locations, so the tool narrows the search space rather than resolving it. When the shadow length is short, or the time window is wide, the band can cover a very large area. The value of the tool lies in combining that band with other evidence, and it is not a substitute for the rest of that work.

## ShadowFinder against a hand-rolled sun calculation

The obvious alternative is to skip the package and compute the geometry yourself with `suncalc` alone: derive the required solar altitude from the shadow ratio, then loop over candidate longitudes and timezones yourself, plotting the matching points with `cartopy`. The difference in approach is not the physics but the scan. ShadowFinder already owns the timezone grid and the sweep across it, and it handles the local versus UTC interpretation through the `time_format` argument, which is exactly the part a manual script tends to get wrong.

Against a fully manual approach, ShadowFinder wins on repeatability. You get a documented package with a version, tests, and a consistent definition of how the timezone grid is built and cached. Against a hand-rolled script you already trust, it adds a dependency on `cartopy`, `timezonefinder` and `suncalc` versions that are pinned by a lock file, and `cartopy` in particular needs its own map data at runtime.

The honest comparison is between the project's own three paths. The notebook is the fastest route to an answer for a single investigation, the Python API is the fastest for repeated runs because it can keep the grid on disk, and the CLI is the one the project itself advises against. Nothing here changes the underlying model, only how quickly you can run it.

## Licence, dependencies and the release trail

The licensing is simple. `pyproject.toml` records the licence as `MIT License`, the package is classified under `License :: OSI Approved :: MIT License`, and the repository carries a `LICENSE` file. Contributions and reuse come with no copyleft obligation attached to the code itself. That is a statement about the licence text, not advice about your own obligations.

The dependency list is worth reading before installing, because it is heavier than the word lightweight suggests: `matplotlib`, `suncalc`, `fire`, `pandas`, `numpy`, `pytz`, `cartopy` and `timezonefinder` with its `pytz` extra. `pandas` and `cartopy` in particular pull in a substantial stack, and `cartopy` needs map data available locally when the figure is drawn. Anyone deploying this into a restricted environment should check that stack first.

On releases, version 0.7.1 is the current one, published on 2026-09-03, preceded by 0.7.0 on 2026-02-13 and 0.6.1 on 2025-06-16. The repository is not archived and the last push was on 2026-09-03. One flag stands out in the manifest: the classifier `Development Status :: 3 - Alpha`. The project is honest about its own maturity, and the alpha marker should be taken at face value when you weigh the risk of relying on it in a live workflow. Development tooling is in place for contributors, with Poetry for packaging, `pre-commit` hooks, `black` for formatting, `pytest` for tests and `tox` for running the suite across the supported Python versions.

## Conclusion

ShadowFinder fits open source investigators who have a shadow in a photo, a rough estimate of the object's height and a time window, and who want a candidate region to check against other evidence. It does not fit anyone who needs a single answer, since the output is a band of possible locations, and the shadow length and object height must be in the same units because the tool treats them as arbitrary. Check the timezone grid before trusting a run, since the command line path rebuilds it every time, and note that the package still classifies itself as alpha with version 0.7.1.

## FAQ

### What inputs does ShadowFinder need to search for a shadow location?

Either an object height together with a shadow length, or the sun's altitude angle above the horizon, plus a date and a time. The `time_format` setting decides whether the timestamp is read as local time or UTC.

### Does ShadowFinder need to be installed to try it?

No. The project ships a Google Colab notebook and states that no installation is necessary for that route. The package on PyPI is installed with `pip install shadowfinder` and gives you the Python API and the command line tool.

### Why is the ShadowFinder command line slow?

The CLI has no caching strategy for the timezone grid, so the grid is generated on every run, which the project describes as resource intensive. The Python API can load a saved grid file instead, which is the faster path for repeated runs.

### Does ShadowFinder return one location or many?

It returns a band of possible locations drawn on a world map, because many places on Earth share the same sun angle at a given moment. Narrowing that band is left to other evidence in the image.

### Is ShadowFinder ready for production use?

The manifest classifies the package as `Development Status :: 3 - Alpha` at version 0.7.1. The licence is MIT and the last push was on 2026-09-03, but the alpha classification is the project's own statement about maturity.

## Sources

- [bellingcat/ShadowFinder on GitHub](https://github.com/bellingcat/ShadowFinder)
- [License: MIT](https://github.com/bellingcat/ShadowFinder/blob/main/LICENSE)
- [Project website](https://colab.research.google.com/github/Bellingcat/ShadowFinder/blob/main/ShadowFinderColab.ipynb)
- [README](https://github.com/bellingcat/ShadowFinder/blob/main/README.md)
- [Releases](https://github.com/bellingcat/ShadowFinder/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bellingcat-shadowfinder
