⏱️ Lectura: 12 min

Eighty million six hundred ninety thousand seven hundred seventy-seven triangles: that’s the number of geometric comparisons an OSINT researcher ran on a GPU to pinpoint, without any image search tools, the exact island where a resort photo was taken. The challenge is called gralhix 004, published by researcher Sofia Santos under the alias Gralhix.

📑 En este artículo
  1. TL;DR
  2. Introduction
  3. What Happened
  4. Context and History
  5. Technical Details and Performance
    1. The Filter Pipeline
    2. From Clusters to 80 Million Triangles
  6. How to Try It
    1. Linux
    2. macOS
    3. Windows (PowerShell)
  7. Impact and Analysis
  8. What’s Next
  9. Frequently Asked Questions
    1. What exactly is the gralhix 004 challenge?
    2. Why didn’t the author use Google Lens?
    3. What is the land-polygons-split-4326 dataset?
    4. Why use CUDA instead of just the CPU?
    5. What is cKDTree and how was it used here?
    6. Can this method be applied to other geolocation problems?
  10. References

The solution, documented on August 16, 2026, combines basic photogrammetry, geospatial filtering with OpenStreetMap data, and CUDA programming, without relying at any point on Google Lens or any image search engine.

TL;DR

  • The gralhix 004 challenge asked participants to identify a resort on an island from a single photo with no EXIF or GPS metadata.
  • The author ruled out Google Lens and solved the case with geometry, Python, and GPU CUDA programming.
  • The original image is a 736×515 pixel WEBP verified with exiftool, with no camera data.
  • The geospatial filter used the land-polygons-split-4326 dataset from OpenStreetMap, 882 MB in size.
  • A tropical latitude filter (-30° to 30°) reduced the universe to 141,131 land polygons.
  • A local density filter (maximum 10 neighbors within 5 km) brought it down to 51,576 candidates.
  • Clustering with cKDTree (20 km radius) grouped 23,500 valid clusters with 3 or more islands.
  • Generating triplets produced 80,690,777 candidate triangles, evaluated in parallel with a CUDA kernel.

Introduction

Most geolocation challenges (geoguessing, in OSINT jargon) are solved by looking at shadows, signs, or vegetation and then cross-checking that intuition with Google Lens or Bing Visual Search. The author of this writeup decided to treat the photo as a pure geometry problem and let a GPU do the heavy lifting of comparing millions of possible combinations.

The result blends basic computer vision, spatial data structures (SciPy’s cKDTree) and CUDA applied not to machine learning, but to geometric brute force over real cartographic data.

What Happened

The gralhix 004 challenge, created by Sofia Santos (Gralhix), featured a single aerial image, apparently taken by a drone, of a resort located on a small island (P0) with two other landmasses visible in the background: one to the right (P1) and another to the left with a mountain peak (P2). The three questions were straightforward: the resort’s name, the island’s coordinates, and the cardinal direction the camera was pointing.

The first thing the author checked was the metadata with exiftool. The file turned out to be a lossless WEBP at 736×515 pixels, with no EXIF, no GPS, and no camera brand: a blank starting point, typical of images that circulate on social media and lose their metadata when recompressed.

CUDA geolocation of a tropical island
The land-polygons-split-4326 dataset from OSM weighs 882 MB in WGS84 format. Foto de CHUTTERSNAP en Unsplash

Context and History

Reconstructing a geometric fingerprint from a photo isn’t a new technique: surveyors and navigators used triangulation by bearings and relative distances long before GPS. What’s different about this case is that it applies that logic in reverse: instead of calculating a position from known fixed references, the author needed to find the fixed references (the three real islands) that matched the shape of the triangle observed in the photo.

Without a reliable perspective model (drone photo, unknown altitude, uncalibrated lens), it wasn’t possible to reconstruct an exact overhead view. The author opted for a deliberate simplification: estimate only the relative distances and angles between P0, P1, and P2 by visual intuition, using a custom GUI to click on each point and automatically calculate the triangle’s geometry. To compensate for the margin of error from a manual click, he applied a ±20% tolerance around each value.

Technical Details and Performance

The Filter Pipeline

Comparing a triangle against every island on the planet is unfeasible unless most candidates are ruled out beforehand. The author built a pipeline of four progressive filters, each one more computationally expensive than the last but applied to an increasingly smaller universe.

flowchart TD
    A["Land polygons OSM (882 MB)"] --> B["Tropical latitude filter -30 to 30"]
    B --> C["141131 polygons"]
    C --> D["Local density filter (fewer than 10 neighbors within 5km)"]
    D --> E["51576 candidates"]
    E --> F["Clustering with cKDTree (20km radius, 3 or more points)"]
    F --> G["23500 clusters"]
    G --> H["Triplet generation (stratified sampling, cap 60)"]
    H --> I["80690777 candidate triangles"]
    I --> J["GPU matching with CUDA"]
    J --> K["Candidates ranked by geometric similarity"]

The first filter is the cheapest: if the island in the background of the photo looks tropical, any landmass outside the -30° to 30° latitude band is ruled out before calculating any geometry at all. That single rule reduced the universe to 141,131 polygons.

The second filter measures local density: for each island centroid, it counts how many other centroids fall within a 5 km radius. If there are more than 10 neighbors that close, that island is probably part of a dense reef or an archipelago with dozens of islets, not the group of three isolated landmasses shown in the photo. The filter left 51,576 candidates.

The third step groups the remaining points into clusters: if a point has at least two other neighbors within 20 km, there’s enough nearby landmass to form a real triangle. The author solved this search with a SciPy cKDTree and query_ball_point, the standard way to search for nearby neighbors without comparing every point against all the others. The result: 23,500 valid clusters.

From Clusters to 80 Million Triangles

Each cluster with n points generates C(n,3) possible triangles, a combinatorial count that grows fast: a cluster of just 60 islands already produces 34,220 combinations. To keep a giant cluster from dominating the computation, the author capped each cluster at 60 points using stratified sampling (one third of the smallest islands, one third of the largest, one third from the middle of the area distribution), instead of a random cutoff.

Even with that limit, the 23,500 clusters generated 80,690,777 candidate triangles. Comparing them one by one on CPU, even with vectorized NumPy, would have meant minutes or hours of sequential computation. The solution was to assign a CUDA thread to each triangle: each thread sorts its three points by land area (the smallest is the candidate for P0, the resort’s islet), determines P1 and P2 using the sign of the cross product, and calculates the angle at P0 and the distance ratio, the same metrics the author had extracted by hand from the original photo.

import numpy as np

def triangulo_desde_clicks(p0, p1, p2):
    d1 = np.linalg.norm(np.array(p1) - np.array(p0))
    d2 = np.linalg.norm(np.array(p2) - np.array(p0))
    v1 = np.array(p1) - np.array(p0)
    v2 = np.array(p2) - np.array(p0)
    coseno = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
    angulo_p0 = np.degrees(np.arccos(coseno))
    razon_distancias = d1 / d2
    return angulo_p0, razon_distancias

angulo, razon = triangulo_desde_clicks((120, 340), (610, 210), (95, 60))
print(f"angulo_p0={angulo:.2f} razon_distancias={razon:.3f}")

This script reproduces at small scale what the click GUI does: it turns three on-screen coordinates into an angle and a distance ratio, the geometric fingerprint that’s later searched against the world map.

from numba import cuda
import math

@cuda.jit
def match_triangulos(lats, lons, areas, triples, razon_objetivo, tolerancia, resultados):
    i = cuda.grid(1)
    if i >= triples.shape[0]:
        return

    a, b, c = triples[i][0], triples[i][1], triples[i][2]
    idx = [a, b, c]
    for x in range(1, 3):
        key = idx[x]
        j = x - 1
        while j >= 0 and areas[idx[j]] > areas[key]:
            idx[j + 1] = idx[j]
            j -= 1
        idx[j + 1] = key
    p0, pa, pb = idx[0], idx[1], idx[2]

    ax, ay = lons[pa] - lons[p0], lats[pa] - lats[p0]
    bx, by = lons[pb] - lons[p0], lats[pb] - lats[p0]
    cruz = ax * by - bx * ay
    p1, p2 = (pa, pb) if cruz > 0 else (pb, pa)

    d1 = math.sqrt((lons[p1] - lons[p0]) ** 2 + (lats[p1] - lats[p0]) ** 2)
    d2 = math.sqrt((lons[p2] - lons[p0]) ** 2 + (lats[p2] - lats[p0]) ** 2)
    razon = d1 / d2

    if abs(razon - razon_objetivo) <= tolerancia:
        resultados[i] = 1

Each thread runs that block independently, with no conditional branches depending on which cluster the triangle belongs to: only the sign of the cross product. It’s the same trick used to determine whether three points turn clockwise or counterclockwise, applied here to deterministically fix which background islet is P1 and which is P2. The expected result is a resultados array with a 1 in every position whose triangle falls within tolerance.

ApproachWhen to Use ItAdvantageLimitation
Vectorized NumPy on CPUFewer than one million comparisonsNo hardware dependencies, easy to debugScales poorly past millions of triangles
CUDA with Numba on GPUTens or hundreds of millions of independent comparisonsOne thread per triangle, massive parallelism without writing C++Requires an NVIDIA GPU and the CUDA Toolkit installed
Native C++ CUDAWhen the Numba kernel is already the bottleneckMaximum control over memory and performanceSteeper learning curve, slower development cycle

⚠️ Heads up: if numba.cuda.detect() doesn’t list any GPU, the kernel runs in CPU simulation mode without throwing any error, and a computation of 80 million triangles that should take minutes can end up running for hours without you noticing.
geographic triangulation for CUDA geolocation
Each CUDA thread evaluates a different triangle: angle at P0 and distance ratio. Foto de Compagnons en Unsplash

How to Try It

The full pipeline relies on free tools: Python, SciPy for cKDTree, Numba to compile CUDA kernels without writing C++, and OpenStreetMap’s public land polygon dataset. Here’s how to set up the environment on the three main operating systems.

Linux

# Debian/Ubuntu
sudo apt install exiftool python3-pip
pip install numpy scipy numba

# Arch/Void
sudo pacman -S perl-image-exiftool python-pip
pip install numpy scipy numba

macOS

brew install exiftool
pip3 install numpy scipy numba

Windows (PowerShell)

# requires Python 3.10+ installed from python.org
winget install ExifTool
pip install numpy scipy numba

For the geospatial filter you need the land-polygons-split-4326 dataset, published at osmdata.openstreetmap.de: 882 MB of global coastline vectors in WGS84, the same coordinate system GPS uses. The initial load with geopandas.read_file() is usually the slowest step in the whole pipeline, so it helps to cache the centroids and areas in a separate file before running the filters repeatedly.

The GPU part also requires the NVIDIA CUDA Toolkit, which Numba detects automatically if installed. Before launching any kernel over millions of triangles, it helps to confirm with numba.cuda.detect() that the card shows up in the list.

Impact and Analysis

What’s valuable about this writeup isn’t so much the final result (identifying one specific resort), but the demonstration that a typical image recognition problem can be solved without neural networks or third-party services: with high school geometry, a standard spatial data structure, and GPU parallelism applied to brute force. It’s a reproducible, auditable alternative to black-box tools like Google Lens, where there’s no way to know why the system suggests a given location.

💭 Key takeaway: the same pattern (extracting a simple geometric fingerprint and searching for it via parallel brute force against a public dataset) works for any matching problem where there isn’t enough information to invert the geometry analytically.

For the OSINT and CTF community, the case leaves behind a reusable pattern: when metadata gives nothing away, as happened here with the WEBP lacking EXIF or GPS, it pays to ask what other geometric or topological structure exists in the image (coastlines, shadows, vegetation patterns) that could be compared at scale against an open database, instead of relying on generic visual recognition.

What’s Next

The author published the complete code, including the click GUI, the filtering scripts, and the CUDA kernel, along with the challenge’s final report, in his GitHub repository. The natural next step for anyone wanting to reproduce the method is adapting it to other metadata-free geolocation problems: mountain photos using the horizon profile, or urban photos using the relative arrangement of tall buildings, following the same scheme of geometric fingerprinting and parallel brute-force search.

📖 Summary on Telegram: View summary

Try it yourself: download the land-polygons-split-4326 dataset and run the tropical latitude filter on your own test coordinates to see in minutes how much it shrinks the search universe.

Frequently Asked Questions

What exactly is the gralhix 004 challenge?

It’s an OSINT challenge created by Sofia Santos (Gralhix) that asks participants to identify, from a single photo with no metadata, a resort’s name on an island, its coordinates, and the direction the camera was pointing.

Why didn’t the author use Google Lens?

Because he considered solving it with his own math and code more interesting and educational than relying on a black-box image search engine.

What is the land-polygons-split-4326 dataset?

It’s a set of coastline vectors for the entire planet in WGS84 format, published by OpenStreetMap Data, 882 MB in size, which serves as the search universe for comparing the geometry of real islands.

Why use CUDA instead of just the CPU?

Because the pipeline generated 80,690,777 candidate triangles: processing them one by one on CPU would be too slow, while assigning a GPU thread to each triangle allows evaluating all of them in parallel.

What is cKDTree and how was it used here?

It’s a SciPy spatial data structure that allows searching for nearby neighbors without comparing each point against all the others; the author used it to group islands into clusters within a 20 km radius.

Can this method be applied to other geolocation problems?

Yes: any problem where a simple geometric fingerprint can be extracted from an image (angles, distance ratios, patterns) and compared against an open geospatial dataset is a candidate for the same approach of progressive filtering and GPU brute-force matching.

References

📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Imagen destacada: Foto de Gilly Tanabose en Unsplash

Categories: Noticias Tech

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.