GVCCS : Ground Visible Camera Contrail Sequences
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 seconds since epoch (warning timestamps are UTC-2 please see v1.1 for 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.0},
doi = {10.5281/zenodo.16419651},
url = {https://doi.org/10.5281/zenodo.16419651},
}
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
        
      
    
    
      
        Files
         (2.1 GB)
        
      
    
    | Name | Size | Download all | 
|---|---|---|
| md5:e5e85c59919a8fcd9a9631e48d623f4b | 2.1 GB | Preview Download | 
Additional details
Related works
- Is described by
- Preprint: arXiv:2507.18330 (arXiv)