{"slug": "geolocating-a-random-island-using-geometry-and-cuda-programming", "title": "Geolocating a random island using geometry and CUDA programming", "summary": "A developer known as gralhix solved an OSINT geolocation challenge by writing CUDA-accelerated geometry code instead of using Google Lens, identifying a resort island from a drone photo. The solution, documented in a GitHub writeup, used OpenStreetMap land polygon data (882 MB) and a series of filters—tropical latitude band (-30° to 30°), local density (≤10 neighbors within 5 km), and clustering (≥3 points within 20 km)—to narrow 141,131 land polygons down to 23,500 clusters, then generated candidate triangles for matching. The challenge, created by Sofia Santos, asked for the resort name, island coordinates, and camera direction.", "body_md": "# gralhix004 | Geolocating Random Islet Image Using Geometry & CUDA GPU Programming\n\n16-08-2026\n\nNOTE: this is a genuine human work, didnt use LLM generation.\n\nI'm writing this page as a writeup for this challenge [gralhix 004 made by Sofia Santos | Gralhix](https://gralhix.com/list-of-osint-exercises/osint-exercise-004/).\n\nYou can view, clone and locally try all code files and the final report with all instructions\n\n[here at github.]\n\n## Task briefing:\n\nThis is a photo of a resort located on an island.\n\na) What is the name of the resort?\n\nb) What are the coordinates of the island?\n\nc) In which cardinal direction was the camera facing when the photo was taken?\n\nIn my opinion, solving this challenge with `google lens`\n\nis wasting a fun opportunity, so decided to solve it with math and programming.\n\n## a] Metadata\n\nOf course, first thing u look for is the `metadata`\n\n. Ran that on my `linux void`\n\n:\n\n```\n> exiftool main.png\n\nFile Type                       : WEBP (lossless)\nMIME Type                       : image/webp\nImage Width                     : 736\nImage Height                    : 515\n```\n\nAs expected, nothing useful here. No EXIF, no GPS, no camera make or model.\n\n## b] Building the fingerprint\n\nU can see from the img, there are 3 landmasses:\n\n- P0: the islet itself,\n- P1: the right island,\n- P2: the left front island ( having mountain peak )\n\nI couldnt make a correct perspective model of birdview of this image, as clearly the image is taken by a drone and cant estimate the elevation at all (and not found in the metadata).\n\nSo I had to estimate that by intuition, I just want the relative distances between the 3 islands and angles of that triangle.\n\nI built a small click GUI `01_triangle_gui.py`\n\nthat records pixel coordinates for each point in order and computes the triangle's geometry.\n\nSince clicking exact centers by eye isn't perfectly precise, I added a `±20% tolerance`\n\nband around both values when searching.\n\n## c] SEARCH\n\nWith the fingerprint locked in, the next step is checking every real landmass on Earth against it !\n\nI used\n\n`OpenStreetMap's split land polygon set`\n\nas the dataset[land-polygons-split-4326], full global coastline vectors in WGS84 which has size of`882 MB`\n\n.\n\nI created heuristic filters (all by just intuition and non tangible proofs), spent days (yea full days) tweaking values and tons of trial and error 😭 untill I got this working filters recipe.\n\n### 01] Tropical latitude bounding box\n\n$$ -30° \\le latitude \\le 30° $$\n\nthe islet in the photo reads as tropical, so I decided that anything outside the tropics is thrown out immediately, before doing any expensive geometry work.\n\nExactly\n\n`141,131`\n\nland polygons survive that band filter.\n\n### 02] Local density filter\n\n$$ N_{5\\text{km}}(p) \\le 10 $$\n\n$ N_{5\\text{km}}(p) $ counts how many other centroids fall within 5km of point (p). `Cap is 10`\n\n: if an islet has more than 10 neighbors that close, it's sitting in a dense reef field, a crowded coastline or a archipelago clutter, not a small isolated 3-4 island group like the photo shows.\n\nThis dropped candidates down to\n\n`51,576`\n\n.\n\n### 03] Clustering\n\nFor every surviving point, find every other point within 20km (heuristic, by eye from the image). If it has at least 2 neighbors that close (3 points total), it's a cluster. Points with no cluster of 3+ nearby are dropped, they can't form a triangle at all.\n\n```\ntree = cKDTree(f_coords)\nneigh = tree.query_ball_point(\n                            f_coords, \n                            CLUSTER_RADIUS_KM / 111.0)\nclusters = set(tuple(sorted(n)) for n in neigh if len(n) >= 3)\n```\n\n$$ \\left|\\{q : \\text{dist}(p,q) \\le 20\\,\\text{km}\\}\\right| \\ge 3 $$\n\nThat collapses down to\n\n`23,500`\n\nclusters.\n\n### 04] Generating Triplets\n\nFor every cluster, every combination of 3 points inside it becomes a candidate triangle. That's $ C(n, 3) $, which explodes fast for big clusters, for example: a cluster of 60 points already gives `34,220`\n\ntriples on its own. So each cluster gets capped at 60 points first, sampled by size, not randomly.\n\n$$ \\binom{n}{3} = \\frac{n(n-1)(n-2)}{6} $$\n\n``` python\ndef stratified_sample(idx_arr, area_arr, cap):\n    order = np.argsort(area_arr[idx_arr])\n    n_small = cap // 3\n    n_large = cap // 3\n    n_mid = cap - n_small - n_large\n    mid_start = max(0, (len(idx_arr) - n_large - n_mid) // 2)\n    keep = np.unique(np.concatenate([\n        order[:n_small], \n        order[-n_large:], \n        order[mid_start:mid_start + n_mid],\n    ]))\n    return idx_arr[keep]\n\ndef gen_cluster_triples(idx_arr):\n    local = np.array(list(\n                itertools.combinations(range(len(idx_arr)), 3)), \n                dtype=np.int64)\n    return idx_arr[local]\n```\n\nThe sampling takes a third small islands, a third large, a third from the middle of the size distribution, instead of the full cluster or a random cut.\n\n`23,500`\n\nclusters produce`80,690,777`\n\ntriples total !!\n\n### 05] Matching, on the GPU\n\nI gave every triple one CUDA thread. Each thread sorts its 3 points by land area to pick out P0 (smallest, the resort islet), then uses the winding direction of the other two to assign P1 and P2:\n\n```\nlong long i = blockIdx.x * (long long)blockDim.x + threadIdx.x;\nif (i >= n_triples) return;\n\nint pos[3] = {0, 1, 2};\nfor (int a1 = 1; a1 < 3; a1++) \n{\n    int key = pos[a1];\n    double keyval = a[key];\n    int j = a1 - 1;\n    while (j >= 0 && a[pos[j]] > keyval) \n    {\n        pos[j + 1] = pos[j];\n        j--;\n    }\n    pos[j + 1] = key;\n}\n```\n\nP1 vs P2 comes from a 2D cross product, no branching on which cluster the triple came from, just the sign:\n\n$$ \\text{cross} = x_a y_b - x_b y_a $$ $$ P1 = \\begin{cases} a & \\text{cross} > 0 \\\\ b & \\text{cross} \\le 0 \\end{cases} $$\n\nWalk from P0 to a, then to b. If cross > 0, that's a left turn (counterclockwise). If cross < 0, it's a right turn (clockwise). It's the same sign trick used to tell if 3 points curve one way or the other.\n\nthen angle at P0 and the distance ratio, same formulas as the fingerprint step, computed independently by every thread:\n\n$$ \\theta_0 = \\arccos\\left(\\frac{\\vec{d_1} \\cdot \\vec{d_2}}{|\\vec{d_1}||\\vec{d_2}|}\\right), \\qquad r = \\frac{|\\vec{d_1}|}{|\\vec{d_2}|} $$\n\nA triple survives if angle, ratio, P0's size, the separation between P0 and P1, and both side lengths all land inside the fingerprint's tolerance windows. Threads that pass write their result into a shared output array using an atomic counter, so two threads finishing at the same time never overwrite each other:\n\n```\nif (hit) \n{\n    unsigned long long slot = atomicAdd(out_count, 1ULL);\n    out_p0[slot] = p0idx;\n    out_p1[slot] = p1idx;\n    out_p2[slot] = p2idx;\n}\n```\n\nNow printed in the CLI directly from the kernel:\n\n```\ngpu: NVIDIA GeForce RTX 3050 (sm_86)\nvram used: 5169 MB\nkernel time: 204.1 ms\n```\n\n`80.7 million`\n\ntriples go in, one thread each, in parallel.`158,784`\n\npass the mask.\n\n### 06] Dedup\n\nSince same physical triple can get hit by multiple GPU threads if it belonged to more than one overlapping cluster, so raw matches get collapsed by identity first:\n\n```\nseen = set()\nuniq = []\nfor i in range(len(p0_all)):\n    key = (p0_all[i], p1_all[i], p2_all[i])\n    if key not in seen:\n        seen.add(key)\n        uniq.append(i)\n```\n\n`8,915`\n\nunique triples after dedup.\n\n### 07] The Open Rectangle\n\nEvery surviving triple gets one more test: is the space next to it actually open water, like the photo shows ? A rectangle gets built along the P0→P1 edge, on whichever side P2 is not on, then checked against the land dataset for anything else sitting inside it.\n\n```\nwidth = np.hypot(x1, y1)\nu = np.array([x1, y1]) / width\nv = np.array([-u[1], u[0]])\n\n# p2 sits on the +v side by construction, \n# so the check goes on -v\nlength = 2 * width\ncorners_local = [\n    (0, 0), (x1, y1),\n    (x1 - v[0]*length, y1 - v[1]*length),\n    (-v[0]*length, -v[1]*length),\n]\n```\n\nIf anything other than the 3 candidate islands themselves intersects that rectangle, the candidate is dropped. Land sitting there means it's not the open, unobstructed water the photo actually shows.\n\n`8,915`\n\nunique triples down to`948`\n\n.\n\nand below is the map of places of the 948 candidates.\n\n## d] Coral Cay Shape Check\n\nIn this stage, we look only at P0, the resort islet, and check whether its shape actually looks like a coral cay.\n\n### 1] `Compactness`\n\n, how close to a circle the shape is:\n\n`Polsby Popper Score:`\n\n$$ PP = \\frac{4\\pi \\cdot \\text{area}}{\\text{perimeter}^2} $$\n\n``` python\ndef compactness(row):\n    return (4 * np.pi * row.area_km2) / (row.perim_km ** 2 + 1e-12)\n```\n\n`1.0`\n\nis a perfect circle, lower means a more jagged or elongated outline. Coral cays tend to be round from wave deposition, so anything `< 0.5`\n\ngets dropped.\n\n### 2] Micro Cay Halo Check:\n\n``` python\ndef micro_cay_count(gdf, sindex, lon, lat):\n    dists_km = nearby.geometry.distance(pt) * 111.0\n    mask = (dists_km > 0) \n           & (dists_km <= HALO_KM) \n           & (nearby[\"area_km2\"].values < MICRO_KM2)\n    return int(mask.sum())\n```\n\nWe Count land fragments under 0.05 km² within 1.5km of P0 ( just heuristic ). Real reef systems scatter tiny sandbars around the main island, not just one isolated landmass (I knew that with the hardway 😭). So we need at least 1.\n\n`213/948`\n\ncandidates survive both checks.\n\n## e] Oval Shape Check\n\nAnother geometric filter on P0's own polygon. Fits the minimum rotated rectangle around it and measures two ratios from that box.\n\n``` python\ndef aspect_and_fill(geom):\n    mrr = geom.minimum_rotated_rectangle\n    coords = list(mrr.exterior.coords)\n    s1 = math.hypot(coords[1][0] - coords[0][0], \n                    coords[1][1] - coords[0][1])\n    s2 = math.hypot(coords[2][0] - coords[1][0], \n                    coords[2][1] - coords[1][1])\n    long_side, short_side = max(s1, s2), min(s1, s2)\n    return long_side / short_side, geom.area / mrr.area\n```\n\n`Aspect ratio`\n\nis long side over short side of that box:\n\n$$ \\text{aspect} = \\frac{\\text{long side}}{\\text{short side}} \\in [1.05,\\ 2.2] $$\n\nToo close to 1.0 and it's basically a perfect circle, not the slightly elongated shape in the photo. Too high are shapes too much elongated more than 2:1.\n\n`Fill ratio`\n\nis how much of that bounding box the shape actually fills, and this one has an identity behind it: any ellipse fills precisely $ \\pi / 4 $ of its own minimum area bounding rectangle, regardless of how stretched it is.\n\n$$ \\frac{\\text{area}_{\\text{ellipse}}}{\\text{area}_{\\text{box}}} = \\frac{\\pi}{4} \\approx 0.785 $$\n\nthat's the theoretical ceiling for a perfectly smooth oval. Real coral cays aren't perfect ellipses, so the cutoff is set as a heuristic safe fraction of that ceiling:\n\n$$ \\text{FILL\\_RATIO\\_MIN} = 0.75 \\times \\frac{\\pi}{4} \\approx 0.589 $$\n\nA shape needs to retain at least 75% of a perfect ellipse's fill to survive. Crescents, rings, and notched coastlines fall well below that, solid rounded cays don't.\n\n`137/213`\n\ncandidates survive.\n\n## f] NDVI Vegetation Check\n\nWe reached the final API phase, I put it at the end, because it is network bound not compute bound.\n\nWe gonna connect to\n\n`Earth Search, run by Element84`\n\n, a public STAC API that indexes Sentinel-2 imagery hosted on AWS's Open Data program, free, no API key.\n\nYou can look at it [https://earth-search.aws.element84.com/v1](https://earth-search.aws.element84.com/v1)\n\nWe now check whether P0 is actually vegetated, palm cover, not bare sand or rock. It pulls the most recent low cloud Sentinel-2 scene over the point from a public STAC catalog, samples the red and near infrared bands at that exact pixel.\n\n$$ \\text{NDVI} = \\frac{\\text{NIR} - \\text{Red}}{\\text{NIR} + \\text{Red}} $$\n\nLive vegetation reflects strongly in near infrared and absorbs red light, so healthy palm cover pushes NDVI well above 0, bare sand or open water sits near 0 or negative.\n\nYou can view this image I got from this nice [Geoawesome Blog.](https://geoawesome.com/eo-hub/understanding-aerial-data-normalized-difference-vegetation-index-ndvi/)\n\n`Threshold is set at 0.6`\n\n, high enough to require real tree cover, not just scattered units.\n\n`66/137`\n\nsurvive the NDVI check.\n\n## g] Elevation & Mountain Check\n\nLast check before the final reveal. There are two conditions:\n\n- P0 itself must be low and flat, consistent with a small reef islet,\n- P2 must have real elevated terrain in the direction the camera was actually facing.\n\nThe \"front\" of the shot is the bisector between the bearing to P1 and the bearing to P2:\n\n$$ \\theta(P_0, P_i) = $$ $$ \\text{atan2}\\Big(\\sin(\\Delta\\lambda)\\cos\\phi_i,\\ \\cos\\phi_0\\sin\\phi_i - \\sin\\phi_0\\cos\\phi_i\\cos(\\Delta\\lambda)\\Big) $$\n\n$$ \\theta_{\\text{front}} = $$ $$ \\theta(P_0, P_2) + \\frac{\\big((\\theta(P_0,P_1) - \\theta(P_0,P_2) + 180) \\bmod 360\\big) - 180}{2} $$\n\nThat gives one heading, the direction the lens was pointed. From there, a fan of sample points gets swept ±50° around that heading, at radii from 2km out to 20km:\n\n$$ (\\text{lat}, \\text{lon}) = \\Big(\\text{lat}_0 + \\frac{r\\cos\\theta}{111},\\ \\ \\text{lon}_0 + \\frac{r\\sin\\theta}{111\\cos(\\text{lat}_0)}\\Big) $$\n\nEvery one of those points gets sampled against real `30m Copernicus DEM tiles`\n\n.\n\n`Copernicus DEM GLO-30`\n\n, published by the EU's Copernicus program, hosted as free public Cloud-Optimized GeoTIFFs on AWS Open Data, no account or key needed.\n\nFor more info, you can view [https://registry.opendata.aws/copernicus-dem/](https://registry.opendata.aws/copernicus-dem/)\n\nFinally, those two simple heuristic conditions decide survival (yea I know, everything became heuristic haha):\n\n$$ \\text{elev}(P_0) \\le 50\\text{m} $$ $$ 100\\text{m} \\le \\max_{\\text{arc}}(\\text{elev}) \\le 500\\text{m} $$\n\nYou can see from this abstract graph image, the dashed line is the camera's front bearing, the wedge is the ±50° search arc swept out to 20km for the elevation check.\n\n`26/66`\n\nsurvive the elevation check.\n\nYou can see the 26 survivors, all are located in southern Asia, Australia and Oceania, except one near Brazil!\n\n## h] Final Report\n\nFinally, last stage, it just makes the final candidates checkable by eye. Each survivor gets its country name via a `point in polygon lookup`\n\nagainst a country boundary file, then a direct Google Maps satellite link for P0, P1, and P2.\n\nOutput is a plain HTML table, index, country, three clickable coordinate pairs per row.\n\nI got this final list, lets check each one by eye.\n\nWon't go one by one here, but those first 7 are totally off for me.\n\nTill I opened that 8th one in the table of country of Micronesia 😍 (first time to know that a country named Micronesia):\n\nand ensured through P1 and P2:\n\nand that is the solution 🥳 ...\n\nyou can view it here on [google maps](https://www.google.com/maps/@7.3633,151.755983,50m/data=!3m1!1e3)\n\n## i] FINALLY, ANSWERS ...\n\n```\na) What is the name of the resort?\n```\n\n$$ \\text{Oan} $$\n\n```\nb) What are the coordinates of the island?\n```\n\n$$7^\\circ\\,21^\\prime\\,48.4^{\\prime\\prime}\\,\\text{N} \\qquad 151^\\circ\\,45^\\prime\\,20.7^{\\prime\\prime}\\,\\text{E}$$\n\n$$ \\text{or} $$\n\n$$7.363444^\\circ,\\ 151.755750^\\circ$$\n\n```\nc) In which cardinal direction was the \ncamera facing when the photo was taken?\n```\n\n$$ \\because\\quad \\theta = \\text{atan2}\\Big(\\sin(\\Delta\\lambda)\\cos\\phi_1,\\ \\cos\\phi_0\\sin\\phi_1 - \\sin\\phi_0\\cos\\phi_1\\cos(\\Delta\\lambda)\\Big) $$\n\n$$ P_0 = (7.3633,\\ 151.755983), \\quad P_1 = (7.386573,\\ 151.739534) $$\n\n$$ \\therefore\\quad \\theta = 324.97^\\circ \\implies \\textbf{NW} $$\n\n## j] Data & Licenses\n\n**Coastline polygons**:\n\n[land-polygons-split-4326](https://osmdata.openstreetmap.de/data/land-polygons.html)\n© OpenStreetMap contributors, available under the\n[Open Database License (ODbL) 1.0](https://opendatacommons.org/licenses/odbl/).\nThe candidate sets and final report in the repo are a Derived Database and are\npublished under the same license.\n\n**Elevation **:\n\nCopernicus DEM GLO-30.\n© DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under\nCOPERNICUS by the European Union and ESA; all rights reserved.\n\n**Satellite imagery **:\n\nContains modified Copernicus Sentinel data 2025-2026,\naccessed through [Earth Search](https://earth-search.aws.element84.com/v1)\nby Element 84 on AWS Open Data.\n\n**Country boundaries **:\n\n[Natural Earth](https://www.naturalearthdata.com/) 10m\nadmin-0, public domain.\n\n**Challenge & source photo **:\n\n[OSINT Exercise #004](https://gralhix.com/list-of-osint-exercises/osint-exercise-004/)\nby Sofia Santos ([gralhix](https://gralhix.com/)).\n\nSatellite screenshots in section (h) are from Google Maps / Google Earth", "url": "https://wpnews.pro/news/geolocating-a-random-island-using-geometry-and-cuda-programming", "canonical_source": "https://yassa9.github.io/osint/gralhix-004/", "published_at": "2026-08-19 12:19:52+00:00", "updated_at": "2026-08-19 12:42:23.191718+00:00", "lang": "en", "topics": ["computer-vision", "developer-tools"], "entities": ["gralhix", "Sofia Santos", "OpenStreetMap", "CUDA", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/geolocating-a-random-island-using-geometry-and-cuda-programming", "markdown": "https://wpnews.pro/news/geolocating-a-random-island-using-geometry-and-cuda-programming.md", "text": "https://wpnews.pro/news/geolocating-a-random-island-using-geometry-and-cuda-programming.txt", "jsonld": "https://wpnews.pro/news/geolocating-a-random-island-using-geometry-and-cuda-programming.jsonld"}}