GVCCS : Ground Visible Camera Contrail Sequences
Authors/Creators
Contributors
Annotator:
Data curator:
Other:
Researcher:
Sponsor:
Supervisor:
Description
The GVCCS dataset provides the first open-access, instance-level annotated video dataset for contrail detection, segmentation, and tracking from Réuniwatt CamVision visible ground-based camera. Designed to support research into aviation’s non-CO₂ climate impacts, it contains 122 high-resolution video sequences (24,228 images) captured at EUROCONTROL’s Innovation Hub in Brétigny-sur-Orge, France.
Each sequence has been carefully labelled with instance-level multi polygon annotations, including temporally resolved contrail masks and consistent instance identifiers. A subset of contrails is also linked to unique flight identifiers (when attribution is possible) based on known aircraft trajectories.
The dataset is aimed at researchers in environmental science, aviation, remote sensing, and computer vision. It supports both semantic and panoptic segmentation tasks and is intended to foster development of models for:
- Contrail detection and instance segmentation
- Temporal tracking and lifecycle analysis
- Contrail-to-flight attribution and validation of physical models
Key Features:
- 24,228 annotated images across 122 video sequences (30sec frame rate) with a total of 176,194 individual polygons.
- 4651 instance-level multi-polygon contrails with temporal consistency (contrails are tracked across frames).
- 3354 contrails are associated to a flight with unique identifier.
- Image resolution: 1024×1024 (geometrically projected from fisheye camera) covering an area of 75km x 75km.
Folder organisation :
GVCCS/
├── azimuth_zenith_grid.npy
├── train/
│ ├── annotations.json
│ ├── parquet/
│ └── images/
├── test/
│ ├── annotations.json
│ ├── parquet/
│ └── images/
azimuth_zenith_grid.npyis the grid of pixel to azitmuth zenith mapping-
Both
trainandtestfolders have a similar structure, with images, annotations, csv files. - Images are high-resolution (1024×1024 pixels JPG files) stored in the
imagessubfolders. Each projected image has been enhanced for visual clarity using brightness adjustment, local contrast amplification, and color rebalancing to improve contrail visibility (see paper description for more details). -
The
parquetfolders include flight data for each sequence saved in parquet format. Each parquet file contains tabular data for the flight passing over the camera filtered with altitude higher than 15 000ft with the following columns:
| Column | Description |
|---|---|
| FLIGHT_ID | Unique identifier for each flight |
| CALL_SIGN | Airline call sign (flight number) |
| REGISTRATION | Aircraft registration code |
| ICAO_TYPE | Aircraft type code according to ICAO classification |
| TIMESTAMP_S | Timestamp of the data point in UTC |
| LONGITUDE | Longitude of the aircraft at the timestamp |
| LATITUDE | Latitude of the aircraft at the timestamp |
| ALTI_FT | Standard Pressure Altitude in feet |
| GRND_SPD | Ground speed of the aircraft in knots |
| AZIMUTH | Aircraft heading (azimuth) in radian |
| ZENITH | Zenith angle (vertical angle) in raduan |
| PIXEL_X | Projected X pixel coordinate corresponding to the aircraft position in the image |
| PIXEL_Y | Projected Y pixel coordinate corresponding to the aircraft position in the image |
-
The
annotations.jsonfiles are provided in COCO format, including per-frame and per-instance polygon annotations, object tracking (consistent instance IDs across frames), flight attribution (when available), and video-level metadata. It contains four main sections with the following fields:
annotations
-
area: Area of the annotated polygon (float)
-
bbox: Bounding box of the instance
[x, y, width, height] -
category_id: ID referring to the object category
-
contrail_id: Unique ID of the contrail instance (consistent across frames)
-
flight_id: ID linking the annotation to the flight data (if available)
-
id: Unique annotation ID
-
image_id: ID of the associated image
-
iscrowd: Boolean flag for crowd annotations
-
segmentation: Polygon coordinates describing the shape
-
type: Type of annotation (e.g., polygon)
categories (only one category: contrail)
-
color: Color associated with the category (for visualization)
-
id: Category ID
-
isthing: Boolean flag indicating whether the category is a "thing" (vs. stuff)
-
name: Category name
images
-
file_name: Image file name
-
height: Image height (pixels)
-
id: Unique image ID
-
time: Timestamp of the image capture
-
video_id: ID of the associated video (if applicable)
-
width: Image width (pixels)
videos
-
height: Video frame height (pixels)
-
id: Unique video ID
-
length: Number of frames in the video
-
start: Start timestamp of the video
-
stop: End timestamp of the video
-
width: Video frame width (pixels)
Code example to retrieve coordinates from pixels
import os
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import RegularGridInterpolator
from geographiclib import geodesic
from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator, NearestNDInterpolator
from geographiclib import geodesic
from typing import Tuple, Union
import numpy as np
RESOLUTION=1024
wgs84 = geodesic.Geodesic.WGS84
class Projector:
def __init__(self,
azimuth_zenith_grid: np.typing.NDArray,
resolution: int = RESOLUTION,
azimuth: float = 270.,
height_above_ground_m: float = 90.,
longitude_deg: float = 2.3467954996250784,
latitude_deg: float = 48.600518087374105):
self.resolution = resolution
self.azimuth = azimuth
grid_size = azimuth_zenith_grid.shape[0]
if grid_size == self.resolution:
extended_grid = False
elif grid_size == (self.resolution + 2):
extended_grid = True
else:
raise ValueError("Grid size must be equal to resolution for regular grid, or resolution + 2 for extended grid")
self.height_above_ground_m = height_above_ground_m
self.longitude_deg = longitude_deg
self.latitude_deg = latitude_deg
# Get azimuth and zenith
azimuth_grid = azimuth_zenith_grid[..., 0]
zenith_grid = azimuth_zenith_grid[..., 1]
# Compute components
cos_grid = np.cos(azimuth_grid)
sin_grid = np.sin(azimuth_grid)
# Generate interpolator
values_grid = np.stack([cos_grid, sin_grid, zenith_grid], axis=-1)
# Define the regular grid coordinates for the interpolator
origin = - 0.5 if extended_grid else 0.5
x = np.arange(grid_size) + origin
# Create the interpolator (maps PIXEL in ENCORD (x, y) -> (cos(az), sin(az), zenith))
self.pixel_to_azzen_regular = RegularGridInterpolator(
(x, x),
values_grid,
bounds_error=False,
fill_value=None # Extrapolate!
)
# Flatten pixel coordinates
pixels_flat = np.stack(np.meshgrid(x, x, indexing='ij'), axis=-1).reshape(-1, 2)
# Flatten unwrapped azimuth and zenith
azimuth_flat = azimuth_grid.flatten()
zenith_flat = zenith_grid.flatten()
# Combine as input for the interpolator
values_flat = np.stack([azimuth_flat, zenith_flat], axis=-1)
# Create reverse interpolator: (azimuth (unwarapped), zenith) → (pixel_x, pixel_y)
self.azzen_to_pixel_linear = LinearNDInterpolator(values_flat, pixels_flat)
def pixel_to_azzen(self,
x: np.typing.NDArray,
y: np.typing.NDArray) -> Tuple[np.typing.NDArray, np.typing.NDArray]:
# Clip pixels to image resolution
x = self.resolution - np.asarray(x)
y = np.asarray(y)
points = np.column_stack((x, y))
# Clip pixels to image resolution
points = np.clip(points, a_min=0, a_max=self.resolution)
result = self.pixel_to_azzen_regular(points)
azimuth_rad = np.arctan2(result[:, 1], result[:, 0])
zenith_rad = result[:, 2]
return azimuth_rad, zenith_rad
return azimuth_rad, zenith_rad
def azzen_to_lonlat(self,
azimuth_rad: np.typing.NDArray,
zenith_rad: np.typing.NDArray,
altitude_m: np.typing.NDArray) -> Tuple[np.typing.NDArray, np.typing.NDArray]:
azimuth_rad = np.asarray(azimuth_rad)
zenith_rad = np.asarray(zenith_rad)
altitude_m = np.asarray(altitude_m)
# Convert to elevation angle in radians
elevation_angle_rad = (np.pi / 2.) - zenith_rad
# Altitude difference
delta_altitude_m = altitude_m - self.height_above_ground_m
# Distance on surface
distance_on_surface_m = delta_altitude_m / np.tan(elevation_angle_rad)
# Convert azimuth to degrees
azimuth_deg = np.degrees(azimuth_rad) + self.azimuth
def compute_direct(azimuth_deg, distance_on_surface_m):
result = wgs84.Direct(self.latitude_deg, self.longitude_deg, azimuth_deg, distance_on_surface_m)
return result['lon2'], result['lat2']
compute_vec = np.vectorize(compute_direct, otypes=[float, float])
lon2, lat2 = compute_vec(azimuth_deg, distance_on_surface_m)
return lon2, lat2
# Load azimuth-zenith grid
folder_path = "YOUR_PATH/GVCCS"
grid_path = os.path.join(folder_path, "azimuth_zenith_grid.npy")
azimuth_zenith_grid = np.load(grid_path)
projector = Projector(azimuth_zenith_grid)
# Define polygon vertices in pixel coordinates (corners of the image here)
pixel_polygon = np.array([
[0, 0],
[0, RESOLUTION-1],
[RESOLUTION-1, RESOLUTION-1],
[RESOLUTION-1, 0]
])
# Interpolate azimuth and zenith for polygon corners
azimuths, zeniths = projector.pixel_to_azzen(pixel_polygon[:,0], pixel_polygon[:,1])
# Altitude hypothesis
target_altitude_m = 10000
# Convert polygon corners from azimuth/zenith to lat/lon
longitudes, latitudes = projector.azzen_to_lonlat(azimuths, zeniths, target_altitude_m)
# Close polygon for plotting
latitudes = np.append(latitudes, latitudes[0])
longitudes = np.append(longitudes, longitudes[0])
# Plot polygon in lat/lon
plt.figure(figsize=(8,6))
plt.plot(longitudes, latitudes, marker='o')
plt.plot(longitudes[:1], latitudes[:1], marker='o',color='r')
plt.title(f'Polygon projected from pixel coordinates to lat/lon at {target_altitude_m} m altitude')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.grid(True)
plt.show()
If you use GVCCS in your work and want to compare with our baselines, please cite the following references:
Dataset :
@dataset{jarry_2025_16419651,
author = {Jarry, Gabriel and
Very, Philippe and
Ballerini, Franck and
Dalmau, Ramon},
title = {GVCCS : Ground Visible Camera Contrail Sequences},
month = jul,
year = 2025,
publisher = {Zenodo},
version = {v1.1},
doi = {10.5281/zenodo.16612390},
url = {https://doi.org/10.5281/zenodo.16612390},
}
Paper baselines :
@misc{jarry2025gvccsdatasetcontrailidentification,
title={GVCCS: A Dataset for Contrail Identification and Tracking on Visible Whole Sky Camera Sequences},
author={Gabriel Jarry and Ramon Dalmau and Philippe Very and Franck Ballerini and Stephania-Denisa Bocu},
year={2025},
eprint={2507.18330},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2507.18330},
}
License:
CC BY 4.0
Files
GVCCS.zip
Additional details
Related works
- Is described by
- Preprint: arXiv:2507.18330 (arXiv)
Software
- Programming language
- Python