Skip to content

Commit 7ffa486

Browse files
authored
Merge pull request #162 from krishauser/Mohammad
Added geotagging and lidar stitching scripts
2 parents 5f81928 + e367fbe commit 7ffa486

2 files changed

Lines changed: 463 additions & 0 deletions

File tree

GEMstack/scripts/geotag.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import rosbag
2+
from cv_bridge import CvBridge
3+
import cv2
4+
from PIL import Image
5+
import piexif
6+
import numpy as np
7+
import math
8+
import os
9+
from datetime import datetime
10+
11+
def rad_to_deg(rad):
12+
"""Convert radians to degrees"""
13+
return rad * 180.0 / math.pi
14+
15+
def interpolate_position(gnss_times, gnss_data, target_time):
16+
"""
17+
Interpolate position using GNSS data
18+
"""
19+
# Find the two closest GNSS messages
20+
idx = np.searchsorted(gnss_times, target_time)
21+
if idx == 0:
22+
lat_rad = gnss_data[0].latitude
23+
lon_rad = gnss_data[0].longitude
24+
return rad_to_deg(lat_rad), rad_to_deg(lon_rad)
25+
if idx == len(gnss_times):
26+
lat_rad = gnss_data[-1].latitude
27+
lon_rad = gnss_data[-1].longitude
28+
return rad_to_deg(lat_rad), rad_to_deg(lon_rad)
29+
30+
# Get timestamps and data for interpolation
31+
t0, t1 = gnss_times[idx-1], gnss_times[idx]
32+
p0 = gnss_data[idx-1]
33+
p1 = gnss_data[idx]
34+
35+
# Calculate interpolation factor
36+
alpha = (target_time - t0) / (t1 - t0)
37+
38+
# Linear interpolation for position (in radians)
39+
lat_rad = p0.latitude + alpha * (p1.latitude - p0.latitude)
40+
lon_rad = p0.longitude + alpha * (p1.longitude - p0.longitude)
41+
42+
# Convert to degrees
43+
return rad_to_deg(lat_rad), rad_to_deg(lon_rad)
44+
45+
def convert_gps_to_exif_format(latitude, longitude, altitude):
46+
"""Convert GPS coordinates and altitude to EXIF format"""
47+
def decimal_to_dms(decimal):
48+
decimal = float(decimal)
49+
degrees = int(decimal)
50+
minutes = int((decimal - degrees) * 60)
51+
seconds = ((decimal - degrees) * 60 - minutes) * 60
52+
return (degrees, 1), (minutes, 1), (int(seconds * 1000), 1000)
53+
54+
lat_dms = decimal_to_dms(abs(latitude))
55+
lon_dms = decimal_to_dms(abs(longitude))
56+
lat_ref = 'N' if latitude >= 0 else 'S'
57+
lon_ref = 'E' if longitude >= 0 else 'W'
58+
59+
# Convert altitude to EXIF format (rational number)
60+
alt_ref = 0 if altitude >= 0 else 1 # 0 = above sea level, 1 = below sea level
61+
altitude = abs(altitude)
62+
alt_ratio = (int(altitude * 100), 100) # Multiply by 100 for 2 decimal precision
63+
64+
return lat_dms, lon_dms, lat_ref, lon_ref, alt_ratio, alt_ref
65+
66+
# Create images directory if it doesn't exist
67+
os.makedirs('images', exist_ok=True)
68+
69+
# Open the bag file
70+
bag = rosbag.Bag('fr+gnss+lidar.bag')
71+
bridge = CvBridge()
72+
73+
# First pass: collect and sort GNSS data
74+
print("Collecting GNSS data...")
75+
gnss_times = []
76+
gnss_messages = []
77+
78+
for topic, msg, t in bag.read_messages(topics=['/septentrio_gnss/insnavgeod']):
79+
gnss_times.append(t.to_sec())
80+
gnss_messages.append(msg)
81+
82+
# Convert to numpy array for efficient searching
83+
gnss_times = np.array(gnss_times)
84+
85+
print(f"Collected {len(gnss_messages)} GNSS messages")
86+
87+
# Process images with interpolated position data
88+
print("Processing images...")
89+
image_count = 0
90+
for topic, msg, t in bag.read_messages(topics=['/camera_fl/arena_camera_node/image_raw']):
91+
# Convert ROS image to OpenCV image
92+
cv_img = bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8') # Explicitly request BGR
93+
94+
# Convert BGR to RGB
95+
rgb_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
96+
97+
# Convert to PIL Image
98+
pil_img = Image.fromarray(rgb_img)
99+
100+
# Get interpolated position data
101+
image_time = t.to_sec()
102+
lat, lon = interpolate_position(gnss_times, gnss_messages, image_time)
103+
104+
# Get altitude from GNSS message (you'll need to interpolate this as well)
105+
altitude = gnss_messages[np.searchsorted(gnss_times, image_time)].height
106+
107+
# Convert GPS coordinates to EXIF format
108+
lat_dms, lon_dms, lat_ref, lon_ref, alt_ratio, alt_ref = convert_gps_to_exif_format(lat, lon, altitude)
109+
110+
timestamp = datetime.fromtimestamp(t.to_sec())
111+
112+
exif_dict = {
113+
"0th": {
114+
piexif.ImageIFD.Make: "FLIR".encode(),
115+
piexif.ImageIFD.Model: "Blackfly S BFS-U3-16S2C".encode(),
116+
piexif.ImageIFD.Software: "ROS".encode(),
117+
piexif.ImageIFD.DateTime: timestamp.strftime("%Y:%m:%d %H:%M:%S").encode(),
118+
piexif.ImageIFD.ImageDescription: "Captured with ROS and FLIR Blackfly S".encode(),
119+
piexif.ImageIFD.XResolution: (msg.width, 1),
120+
piexif.ImageIFD.YResolution: (msg.height, 1),
121+
piexif.ImageIFD.ResolutionUnit: 2, # inches
122+
},
123+
"Exif": {
124+
piexif.ExifIFD.DateTimeOriginal: timestamp.strftime("%Y:%m:%d %H:%M:%S").encode(),
125+
piexif.ExifIFD.DateTimeDigitized: timestamp.strftime("%Y:%m:%d %H:%M:%S").encode(),
126+
piexif.ExifIFD.ExposureTime: (1, 100), # 1/100 second
127+
piexif.ExifIFD.FNumber: (16, 10), # f/1.6
128+
piexif.ExifIFD.ExposureProgram: 1, # Manual
129+
piexif.ExifIFD.ISOSpeedRatings: 100,
130+
piexif.ExifIFD.ExifVersion: b'0230',
131+
piexif.ExifIFD.ComponentsConfiguration: b'\x01\x02\x03\x00', # RGB
132+
piexif.ExifIFD.FocalLength: (16, 1), # 16mm
133+
piexif.ExifIFD.ColorSpace: 1, # sRGB
134+
piexif.ExifIFD.PixelXDimension: msg.width,
135+
piexif.ExifIFD.PixelYDimension: msg.height,
136+
piexif.ExifIFD.ExposureMode: 1, # Manual exposure
137+
piexif.ExifIFD.WhiteBalance: 1, # Manual white balance
138+
piexif.ExifIFD.SceneCaptureType: 0, # Standard
139+
},
140+
"GPS": {
141+
piexif.GPSIFD.GPSLatitudeRef: lat_ref.encode(),
142+
piexif.GPSIFD.GPSLatitude: lat_dms,
143+
piexif.GPSIFD.GPSLongitudeRef: lon_ref.encode(),
144+
piexif.GPSIFD.GPSLongitude: lon_dms,
145+
piexif.GPSIFD.GPSAltitudeRef: alt_ref,
146+
piexif.GPSIFD.GPSAltitude: alt_ratio,
147+
piexif.GPSIFD.GPSTimeStamp: tuple(map(lambda x: (int(x), 1), timestamp.strftime("%H:%M:%S").split(":"))),
148+
piexif.GPSIFD.GPSDateStamp: timestamp.strftime("%Y:%m:%d").encode(),
149+
piexif.GPSIFD.GPSVersionID: (2, 3, 0, 0),
150+
}
151+
}
152+
153+
# Convert to bytes
154+
exif_bytes = piexif.dump(exif_dict)
155+
156+
# Save image with EXIF data
157+
output_filename = os.path.join('images', f'image_{image_time:.3f}.jpg')
158+
pil_img.save(
159+
output_filename,
160+
'jpeg',
161+
exif=exif_bytes
162+
)
163+
164+
image_count += 1
165+
166+
bag.close()
167+
print(f"\nProcessing complete! Saved {image_count} images to the 'images' folder.")

0 commit comments

Comments
 (0)