GVCCS : Ground Visible Camera Contrail Sequences
收藏资源简介:
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 train and test folders have a similar structure, with images, annotations, csv files. Images are high-resolution (1024×1024 pixels JPG files) stored in the images subfolders. 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.json files 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
GVCCS数据集是首个面向凝结尾迹(contrail)检测、分割与跟踪任务的开源实例级标注视频数据集,采集自Réuniwatt CamVision可见光地面相机。本数据集旨在支撑航空非二氧化碳气候影响相关研究,共包含122段高分辨率视频序列(合计24228张图像),采集于法国奥尔日河畔布雷蒂尼的欧洲空管创新中心(EUROCONTROL Innovation Hub)。 每段序列均经过精细标注,采用实例级多多边形标注方案,包含时序对齐的凝结尾迹掩码与统一的实例标识符。部分凝结尾迹可基于已知的飞机航迹关联至唯一的航班标识符(仅当可完成归属关联时)。 本数据集面向环境科学、航空航天、遥感与计算机视觉领域的研究人员,支持语义分割与全景分割两类任务,旨在推动以下方向的模型研发: 1. 凝结尾迹检测与实例分割 2. 时序跟踪与生命周期分析 3. 凝结尾迹-航班归属关联与物理模型验证 ### 核心特性 - 122段视频序列共计24228张标注图像(帧率30fps),合计包含176194个独立多边形标注。 - 4651个具备时序一致性的实例级多多边形凝结尾迹(凝结尾迹可跨帧跟踪)。 - 3354个凝结尾迹关联至带有唯一标识符的航班。 - 图像分辨率为1024×1024(由鱼眼相机(fisheye camera)几何校正投影得到),覆盖75km×75km的观测区域。 ### 文件夹组织结构 GVCCS/ ├── azimuth_zenith_grid.npy ├── train/ │ ├── annotations.json │ ├── parquet/ │ └── images/ ├── test/ │ ├── annotations.json │ ├── parquet/ │ └── images/ 其中`azimuth_zenith_grid.npy`为像素坐标与方位角-天顶角的映射网格文件。 训练集(train)与测试集(test)文件夹结构一致,均包含图像、标注文件与Parquet格式(Parquet format)数据文件。 图像以JPG格式存储于`images`子文件夹中,分辨率为1024×1024像素。为提升凝结尾迹的可视性,所有投影图像均通过亮度调整、局部对比度增强与色彩重平衡进行了视觉增强处理(详细说明请参考对应论文)。 `parquet`子文件夹中存储各序列对应的Parquet格式航班数据。所有Parquet文件均筛选了飞行高度高于15000英尺的过相机航班数据,包含以下字段: | 字段名 | 说明 | | ---- | ---- | | "FLIGHT_ID" | 航班唯一标识符 | | "CALL_SIGN" | 航空公司呼号(航班号) | | "REGISTRATION" | 航空器注册号 | | "ICAO_TYPE" | 国际民航组织(ICAO)分类下的航空器型号代码 | | "TIMESTAMP_S" | 数据点的时间戳(以纪元秒为单位;注意:本版本时间戳采用UTC-2时区,UTC时区请参考v1.1版本) | | "LONGITUDE" | 对应时间戳下航空器的经度 | | "LATITUDE" | 对应时间戳下航空器的纬度 | | "ALTI_FT" | 标准气压高度,单位为英尺 | | "GRND_SPD" | 航空器地速,单位为节 | | "AZIMUTH" | 航空器航向(方位角),单位为弧度 | | "ZENITH" | 天顶角(垂直角),单位为弧度 | | "PIXEL_X" | 航空器位置在图像中的投影X像素坐标 | | "PIXEL_Y" | 航空器位置在图像中的投影Y像素坐标 | `annotations.json`文件采用微软COCO数据集格式(COCO format)存储,包含逐帧与逐实例的多边形标注、目标跟踪信息(跨帧统一的实例ID)、航班归属关联信息(若可用)以及视频级元数据。该文件包含四个核心字段组,详情如下: #### 1. annotations 字段组 - `area`: 标注多边形的面积(浮点型) - `bbox`: 实例的边界框,格式为`[x, y, width, height]` - `category_id`: 指向目标类别的ID - `contrail_id`: 凝结尾迹实例的唯一ID(跨帧保持一致) - `flight_id`: 将标注关联至航班数据的ID(若可用) - `id`: 唯一标注ID - `image_id`: 关联图像的ID - `iscrowd`: 群体标注的布尔标记 - `segmentation`: 描述目标形状的多边形坐标 - `type`: 标注类型(例如:`polygon`,即多边形) #### 2. categories 字段组(仅包含一个类别:凝结尾迹(contrail)) - `color`: 该类别用于可视化的配色 - `id`: 类别ID - `isthing`: 布尔标记,用于标识该类别为“实体类目标”(区别于“背景类目标”) - `name`: 类别名称 #### 3. images 字段组 - `file_name`: 图像文件名 - `height`: 图像高度(像素) - `id`: 唯一图像ID - `time`: 图像采集的时间戳 - `video_id`: 关联视频的ID(若适用) - `width`: 图像宽度(像素) #### 4. videos 字段组 - `height`: 视频帧高度(像素) - `id`: 唯一视频ID - `length`: 视频包含的帧数 - `start`: 视频的起始时间戳 - `stop`: 视频的结束时间戳 - `width`: 视频帧宽度(像素) ### 像素坐标转换示例代码 python 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 # 获取方位角与天顶角 azimuth_grid = azimuth_zenith_grid[..., 0] zenith_grid = azimuth_zenith_grid[..., 1] # 计算分量 cos_grid = np.cos(azimuth_grid) sin_grid = np.sin(azimuth_grid) # 生成插值器 values_grid = np.stack([cos_grid, sin_grid, zenith_grid], axis=-1) # 定义插值器的规则网格坐标 origin = - 0.5 if extended_grid else 0.5 x = np.arange(grid_size) + origin # 创建插值器(将像素坐标(x,y)映射为(cos(az), sin(az), zenith)) self.pixel_to_azzen_regular = RegularGridInterpolator( (x, x), values_grid, bounds_error=False, fill_value=None # 支持外推 ) # 展平像素坐标 pixels_flat = np.stack(np.meshgrid(x, x, indexing='ij'), axis=-1).reshape(-1, 2) # 展平未包装的方位角与天顶角 azimuth_flat = azimuth_grid.flatten() zenith_flat = zenith_grid.flatten() # 组合为插值器输入 values_flat = np.stack([azimuth_flat, zenith_flat], axis=-1) # 创建反向插值器:(方位角(未包装), 天顶角) → (像素x, 像素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]: # 将像素坐标裁剪至图像分辨率范围内 x = self.resolution - np.asarray(x) y = np.asarray(y) points = np.column_stack((x, y)) # 裁剪像素坐标 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) # 转换为仰角(弧度) elevation_angle_rad = (np.pi / 2.) - zenith_rad # 高度差 delta_altitude_m = altitude_m - self.height_above_ground_m # 地表距离 distance_on_surface_m = delta_altitude_m / np.tan(elevation_angle_rad) # 将方位角转换为角度制 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 # 加载方位角-天顶角网格 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) # 定义像素坐标下的多边形顶点(此处为图像四角) pixel_polygon = np.array([ [0, 0], [0, RESOLUTION-1], [RESOLUTION-1, RESOLUTION-1], [RESOLUTION-1, 0] ]) # 对多边形顶点插值获取方位角与天顶角 azimuths, zeniths = projector.pixel_to_azzen(pixel_polygon[:,0], pixel_polygon[:,1]) # 假设目标高度 target_altitude_m = 10000 # 将多边形顶点从方位角/天顶角转换为经纬度 longitudes, latitudes = projector.azzen_to_lonlat(azimuths, zeniths, target_altitude_m) # 闭合多边形以便绘图 latitudes = np.append(latitudes, latitudes[0]) longitudes = np.append(longitudes, longitudes[0]) # 绘制经纬度下的多边形 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() 若您在研究中使用GVCCS数据集并希望与我们的基线模型进行对比,请引用以下文献: ### 数据集引用 bibtex @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}, } ### 基线模型论文引用 bibtex @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}, } 许可证:CC BY 4.0



