Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat oblique UI merge #712

Draft
wants to merge 3 commits into
base: feat_oblique_ui
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions annotation_gui_gcp/gcp_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class GroundControlPointManager:
def __init__(self, path):
self.points = OrderedDict()
self.latlons = {}
self.alt = {}
self.path = path
self.image_cache = {}
load_or_download_names()
Expand All @@ -63,15 +64,17 @@ def load_from_file(self, file_path):
latlon = point.get("position")
if latlon:
if "altitude" in latlon:
raise NotImplementedError("Not supported: altitude in GCPs")
self.alt[point["id"]] = {'altitude': latlon['altitude']}
self.latlons[point["id"]] = latlon

def write_to_file(self, filename):
output_points = []
for point_id in self.points:
out_point = {"id": point_id, "observations": self.points[point_id]}
if out_point["id"] in self.latlons:
out_point["position"] = self.latlons[point_id]
out_point["position"] = {
**self.latlons[point_id],
**self.alt[point_id]}
output_points.append(out_point)
with open(filename, "wt") as fp:
json.dump({"points": output_points}, fp, indent=4, sort_keys=True)
Expand All @@ -95,15 +98,18 @@ def add_point(self):
self.points[new_id] = []
return new_id

def add_point_observation(self, point_id, shot_id, projection, latlon=None, oblique_coords=None):

def add_point_observation(self, point_id, shot_id, projection, latlon=None, oblique_coords=None, alt=None):
if not self.point_exists(point_id):
raise ValueError(f"ERROR: trying to modify a non-existing point {point_id}")
raise ValueError(
f"ERROR: trying to modify a non-existing point {point_id}")

if latlon:
self.latlons[point_id] = {
"latitude": latlon[0],
"longitude": latlon[1],
}

if oblique_coords:
self.points[point_id].append(
{
Expand All @@ -112,14 +118,23 @@ def add_point_observation(self, point_id, shot_id, projection, latlon=None, obli
"source_xy": oblique_coords
}
)
if alt:
self.alt[point_id] = {
"altitude": alt
}
else:
if alt:
self.alt[point_id] = {
"altitude": alt
}
self.points[point_id].append(
{
"shot_id": shot_id,
"projection": projection,
}
)


def compute_gcp_errors(self):
error_avg = {}
worst_gcp_error = 0
Expand All @@ -146,7 +161,8 @@ def shot_with_max_gcp_error(self, image_keys, gcp):
annotated_images = set(self.gcp_reprojections[gcp]).intersection(
set(image_keys)
)
errors = {k: self.gcp_reprojections[gcp][k]["error"] for k in annotated_images}
errors = {k: self.gcp_reprojections[gcp]
[k]["error"] for k in annotated_images}
if len(errors) > 0:
return max(errors, key=lambda k: errors[k])
else:
Expand Down
99 changes: 39 additions & 60 deletions annotation_gui_gcp/oblique_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from opensfm.dataset import DataSet
from export_reconstruction_points import world_points


IMAGE_MAX_SIZE = 2000
MIN_OBLIQUE_ANGLE = 10
MAX_OBLIQUE_ANGLE = 55
Expand Down Expand Up @@ -104,35 +105,29 @@ def invert_coords_from_rotated_image(point, theta, original_image_shape=(5120, 5


class ObliqueManager:
def __init__(self, path: str, preload_images=True):
def __init__(self, path: str, preload_images=True, rotate=False):
self.path = Path(path)
self.rtree_path=f'{self.path}/rtree_index'
self.rtree_path = f'{self.path}/rtree_index'
self.image_cache = {}
self.image_coord = {}
self.image_rotations = {}
self.candidate_images = []
self.preload_bol = preload_images
self.ds = DataSet(self.path)
self.get_rtree_index()
self.image_data_path = f"{self.path}/{OBLIQUE_DATA_FILE}"
self.image_metadata = pd.read_csv(self.image_data_path, usecols=['PhotoName', 'OmegaDeg', 'PhiDeg', 'KappaDeg'])
self.image_metadata.set_index('PhotoName', inplace=True)
self.rotate=rotate
if self.rotate:
self.image_data_path = f"{self.path}/{OBLIQUE_DATA_FILE}"
self.image_metadata = pd.read_csv(self.image_data_path, usecols=['PhotoName', 'OmegaDeg', 'PhiDeg', 'KappaDeg'])
self.image_metadata.set_index('PhotoName', inplace=True)

def image_path(self, image_name):
return f"{self.path}/images/{image_name}"

def get_image(self, image_name):
# image_name has the convention {root_name}_{px}_{py}
# the image retrieved has been cropped
if image_name not in self.image_cache:
path = self.image_path(image_name)
px = image_name.split('_')[-2]
py = image_name.split('_')[-1]
self.image_cache[image_name] = self.load_image((path, px, py))

img_rgb = self.image_cache[image_name]

return img_rgb
self.image_cache[image_name] = self.load_image(path)
return self.image_cache[image_name]

def load_latlons(self):
# 'canonical' latlon not as useful for obliques
Expand All @@ -151,8 +146,11 @@ def get_candidates(self, lat: float, lon: float, filter_obliques=True):

self.aerial_matches = [x.object['images'] for x in aerial_match][0]
self.image_names = [x['image_name']
for x in self.aerial_matches ]
for x in self.aerial_matches]
self.image_coord = {x['image_name']: (x['x_px_int'] , x['y_px_int'])
for x in self.aerial_matches}
print(f"Found {len(self.aerial_matches)} aerial images")

if self.preload_bol:
self.preload_images()

Expand Down Expand Up @@ -183,7 +181,7 @@ def build_rtree_index(self):
ypx = int(np.round(im['y_px']))
imn = {'x_px_int': xpx,
'y_px_int': ypx,
'image_name': f"{im['image_id']}_{xpx}_{ypx}"}
'image_name': f"{im['image_id']}"}
ims.append(dict(im, **imn))
lat = val['location']['lat']
lon = val['location']['lon']
Expand All @@ -200,32 +198,20 @@ def preload_images(self):
print(f"Preloading images with {n_cpu} processes")
paths = []
image_names = []
coords=[]
for match in self.aerial_matches:
image_names.append(match['image_name'])
paths.append(
(self.image_path(match['image_id']), match['x_px_int'], (match['y_px_int'])))
paths.append(self.image_path(match['image_id']))
coords.append((match['x_px_int'], match['y_px_int']))
pool = multiprocessing.Pool(processes=n_cpu)
images = pool.map(self.load_image, paths)
for image_name, im, path in zip(image_names, images, paths):
for image_name, im, coord in zip(image_names, images, coords):
self.image_cache[image_name] = im
self.image_coord[image_name] = (path[1:])
self.image_coord[image_name] = coord

def get_image_size(self, image_name):
return self.get_image(image_name).shape[:2]

def get_offsets(self, image_name, rotate=True):
px, py = self.image_coord[image_name]
height, width = self.get_image_size(image_name)

if rotate:
theta = self.get_rotation_angle(image_name)
px, py = coords_in_rotated_image((px, py), theta)

win = int(IMAGE_MAX_SIZE/2)
y1 = np.max([py-win, 0])
x1 = np.max([px-win, 0])
return x1, y1

def get_nearest_feature(self, image_name, x, y):
return None

Expand All @@ -237,46 +223,39 @@ def get_rotation_angle(self, image_name):
the cropped version, e.g. with {root}_{px}_{py}

"""
root_name = image_name.split('_')[0] + '_' + image_name.split('_')[1]
omega, phi, kappa = self.image_metadata.loc[root_name]
theta = oblique_rotation_angle(omega, phi, kappa)
return theta
if not self.rotate:
return 0
else:
omega, phi, kappa = self.image_metadata.loc[image_name]
theta = oblique_rotation_angle(omega, phi, kappa)
return theta


def load_image(self, in_tuple, win=int(IMAGE_MAX_SIZE/2), rotate=True):
def load_image(self, path, rotate=False):
'''
Load an image around a pixel location. The input px and py are the
coordinates of the feature in the original image
Load an image and rotate if requested

Inputs
------
in_tuple : tuple
(path, px, py)
path: str, px: int, py:int
path : str
'''

# package for pool
path, px, py = in_tuple
rgb = Image.open(path)
width, height = rgb.size


if rotate:
theta = self.get_rotation_angle(os.path.basename(path))
rgb = rgb.rotate(theta, resample=Image.BICUBIC, expand=True)
px, py = coords_in_rotated_image((px, py), theta, (height, width))

y1 = np.max([py-win, 0])
y2 = np.min([py+win, rgb.height])
x1 = np.max([px-win, 0])
x2 = np.min([px+win, rgb.width])
# Matplotlib will transform to rgba when plotting
return _rgb_to_rgba(np.asarray(rgb))

# use this to mark feature point?
# will need to back out original px, py after click
pt_x = np.min([px, win])
pt_y = np.min([py, win])

if win is not None:
rgb = rgb.crop((x1, y1, x2, y2))
def get_nearest_feature(self, image_name, x, y):
return None

# Matplotlib will transform to rgba when plotting
return _rgb_to_rgba(np.asarray(rgb))
def get_normalized_feature(self, image_name):
pt_x, pt_y = self.image_coord[image_name]
height, width = self.get_image_size(image_name)
nx,ny=pt_x/height, pt_y/width
return nx,ny
Loading