From 3c3d38cc128db9a0e02777b459fce3d227aca14b Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Thu, 12 Jan 2023 11:42:05 -0800 Subject: [PATCH 01/35] start pano_stitch refactor --- pano/pano_stitch/docker/Dockerfile.pano_stitch | 13 +++++++++++++ pano/pano_stitch/docker/build.sh | 4 ++++ pano/pano_stitch/docker/run.sh | 3 +++ 3 files changed, 20 insertions(+) create mode 100644 pano/pano_stitch/docker/Dockerfile.pano_stitch create mode 100644 pano/pano_stitch/docker/build.sh create mode 100644 pano/pano_stitch/docker/run.sh diff --git a/pano/pano_stitch/docker/Dockerfile.pano_stitch b/pano/pano_stitch/docker/Dockerfile.pano_stitch new file mode 100644 index 00000000..498d1219 --- /dev/null +++ b/pano/pano_stitch/docker/Dockerfile.pano_stitch @@ -0,0 +1,13 @@ + +ARG UBUNTU_VERSION=20.04 +ARG REMOTE=ghcr.io/nasa + +FROM ${REMOTE}/astrobee:latest-ubuntu${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + hugin \ + && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/astrobee_init.sh"] +CMD ["/bin/bash"] diff --git a/pano/pano_stitch/docker/build.sh b/pano/pano_stitch/docker/build.sh new file mode 100644 index 00000000..c5250a59 --- /dev/null +++ b/pano/pano_stitch/docker/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd ${HOME}/isaac/src +docker build . -f pano/pano_stitch/docker/Dockerfile.pano_stitch -t isaac/pano_stitch + diff --git a/pano/pano_stitch/docker/run.sh b/pano/pano_stitch/docker/run.sh new file mode 100644 index 00000000..3b6cd642 --- /dev/null +++ b/pano/pano_stitch/docker/run.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd ${HOME}/isaac/src +docker run -it --rm --name isaac_pano_stitch isaac/pano_stitch From feae7598c98c7dcfbe36afc76ff7b4b13b682d2c Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Wed, 18 Jan 2023 15:03:20 -0800 Subject: [PATCH 02/35] config_panos.py and snakemake starting to work --- .../pano_stitch/docker/Dockerfile.pano_stitch | 13 -- pano/pano_stitch/docker/build.sh | 4 +- pano/pano_stitch/docker/exec.sh | 2 + .../pano_stitch/docker/pano_stitch.Dockerfile | 16 ++ pano/pano_stitch/docker/run.sh | 19 +- pano/pano_stitch/package.xml | 2 +- pano/pano_stitch/scripts/Snakefile | 17 ++ pano/pano_stitch/scripts/config_panos.py | 163 +++--------------- pano/pano_stitch/scripts/pano_image_meta.py | 5 +- 9 files changed, 79 insertions(+), 162 deletions(-) delete mode 100644 pano/pano_stitch/docker/Dockerfile.pano_stitch mode change 100644 => 100755 pano/pano_stitch/docker/build.sh create mode 100755 pano/pano_stitch/docker/exec.sh create mode 100644 pano/pano_stitch/docker/pano_stitch.Dockerfile mode change 100644 => 100755 pano/pano_stitch/docker/run.sh create mode 100644 pano/pano_stitch/scripts/Snakefile diff --git a/pano/pano_stitch/docker/Dockerfile.pano_stitch b/pano/pano_stitch/docker/Dockerfile.pano_stitch deleted file mode 100644 index 498d1219..00000000 --- a/pano/pano_stitch/docker/Dockerfile.pano_stitch +++ /dev/null @@ -1,13 +0,0 @@ - -ARG UBUNTU_VERSION=20.04 -ARG REMOTE=ghcr.io/nasa - -FROM ${REMOTE}/astrobee:latest-ubuntu${UBUNTU_VERSION} - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - hugin \ - && rm -rf /var/lib/apt/lists/* - -ENTRYPOINT ["/astrobee_init.sh"] -CMD ["/bin/bash"] diff --git a/pano/pano_stitch/docker/build.sh b/pano/pano_stitch/docker/build.sh old mode 100644 new mode 100755 index c5250a59..38f817e9 --- a/pano/pano_stitch/docker/build.sh +++ b/pano/pano_stitch/docker/build.sh @@ -1,4 +1,4 @@ #!/bin/sh +set -x cd ${HOME}/isaac/src -docker build . -f pano/pano_stitch/docker/Dockerfile.pano_stitch -t isaac/pano_stitch - +docker build . -f pano/pano_stitch/docker/pano_stitch.Dockerfile -t isaac/pano_stitch diff --git a/pano/pano_stitch/docker/exec.sh b/pano/pano_stitch/docker/exec.sh new file mode 100755 index 00000000..c13138b8 --- /dev/null +++ b/pano/pano_stitch/docker/exec.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker exec -it isaac_pano_stitch /bin/bash -ic "$*" diff --git a/pano/pano_stitch/docker/pano_stitch.Dockerfile b/pano/pano_stitch/docker/pano_stitch.Dockerfile new file mode 100644 index 00000000..d6771894 --- /dev/null +++ b/pano/pano_stitch/docker/pano_stitch.Dockerfile @@ -0,0 +1,16 @@ + +ARG UBUNTU_VERSION=20.04 +ARG REMOTE=ghcr.io/nasa + +FROM ${REMOTE}/isaac:latest-ubuntu${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + hugin \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +RUN pip3 install --no-cache-dir --upgrade pip \ + && pip3 install --no-cache-dir snakemake + +RUN echo 'source "/src/isaac/devel/setup.bash"\nexport ASTROBEE_CONFIG_DIR="/src/astrobee/src/astrobee/config"' >> "${HOME}/.bashrc" diff --git a/pano/pano_stitch/docker/run.sh b/pano/pano_stitch/docker/run.sh old mode 100644 new mode 100755 index 3b6cd642..58080162 --- a/pano/pano_stitch/docker/run.sh +++ b/pano/pano_stitch/docker/run.sh @@ -1,3 +1,20 @@ #!/bin/sh +if [ "$ISAAC_PANO_INPUT" = "" ] || [ ! -d "$ISAAC_PANO_INPUT" ]; then + echo "ISAAC_PANO_INPUT env var must point to input folder" + exit 1 +fi +if [ "$ISAAC_PANO_STITCH" = "" ] || [ ! -d "$ISAAC_PANO_STITCH" ]; then + echo "ISAAC_PANO_STITCH env var must point to output folder" + exit 1 +fi + +set -x + cd ${HOME}/isaac/src -docker run -it --rm --name isaac_pano_stitch isaac/pano_stitch +docker run \ + -it --rm \ + --name isaac_pano_stitch \ + --mount type=bind,source=${ISAAC_PANO_INPUT},target=/input,readonly \ + --mount type=bind,source=${ISAAC_PANO_STITCH},target=/stitch \ + --mount type=bind,source=$(pwd),target=/src/isaac/src \ + isaac/pano_stitch diff --git a/pano/pano_stitch/package.xml b/pano/pano_stitch/package.xml index f4ea4030..7085561b 100644 --- a/pano/pano_stitch/package.xml +++ b/pano/pano_stitch/package.xml @@ -15,5 +15,5 @@ catkin rosbag - rosbag + rosbag diff --git a/pano/pano_stitch/scripts/Snakefile b/pano/pano_stitch/scripts/Snakefile new file mode 100644 index 00000000..0531db8e --- /dev/null +++ b/pano/pano_stitch/scripts/Snakefile @@ -0,0 +1,17 @@ + +import json +import yaml + +configfile: "/stitch/pano_meta.yaml" + +rule all: + input: + expand("/stitch/{scene_id}/pano.png", scene_id=config["scenes"].keys()) + +rule stitch: + output: + "/stitch/{scene_id}/pano.png" + params: + m=lambda wildcards: config["scenes"][wildcards.scene_id] + shell: + "/src/isaac/src/pano/pano_stitch/scripts/stitch_panorama.py --no-lens --world=iss --robot={params.m[robot]} --images-dir={params.m[images_dir]} {params.m[bag_path]} --output-dir=/stitch/{wildcards.scene_id}" diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index ea17f098..732fca9a 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -17,23 +17,15 @@ # under the License. """ -Detect panoramas (bag files and associated SciCam images) and write -config file for stitching. Manual review of the config file is -recommended before starting a large stitching job. +Detect panoramas (bag files and associated SciCam images) and write template config file for stitching. -If --add is specified, add only newly detected panoramas to the -existing config file, without modifying the existing entries. +Example: rosrun pano_stitch scripts/config_panos.py /input /stitch/pano_meta.yaml """ import argparse -import copy -import datetime import os -import random import re -import sys -import numpy as np import yaml import pano_image_meta @@ -59,159 +51,66 @@ } -def get_scene_position(bag_meta): - pos_data = np.zeros((len(bag_meta), 3)) - for i, image_meta in enumerate(bag_meta): - pos_data[i, :] = [image_meta["x"], image_meta["y"], image_meta["z"]] - median_pos = np.median(pos_data, axis=0) - return { - "x": float(median_pos[0]), - "y": float(median_pos[1]), - "z": float(median_pos[2]), - } - - -def get_image_timestamp(image_meta): - timestamp = datetime.datetime.utcfromtimestamp(image_meta["timestamp"]) - return timestamp.isoformat() + "Z" - - def detect_pano_meta(in_folder): - """ - Detect panoramas (bag files and associated SciCam images). Return - pano metadata. - """ - bags = {} sci_cam_images = {} for dirname, subdirs, files in os.walk(in_folder): for f in files: if f.endswith(".bag"): bag_path = os.path.join(dirname, f) - bags[bag_path] = pano_image_meta.get_image_meta(bag_path) + bags[bag_path] = pano_image_meta.get_image_meta(bag_path, 1) elif SCI_CAM_IMG_REGEX.search(f): sci_cam_images[f] = dirname - print("Detected {} candidate bags".format(len(bags))) - print("Detected {} candidate SciCam images".format(len(sci_cam_images))) scenes = {} pano_meta = {"scenes": scenes} for i, (bag_path, bag_meta) in enumerate(bags.items()): - print("Bag {}".format(bag_path)) - print(" {} SciCam images with pose data".format(len(bag_meta))) if not bag_meta: - print(" (Skipping)") + # bag_meta would have length 0 if e.g. the bag has no SciCam images continue scene_meta = { + "scene_id": None, "bag_path": bag_path, "images_dir": None, "robot": None, "activity": None, "module": None, "bay": None, - "position": get_scene_position(bag_meta), - "start_time": get_image_timestamp(bag_meta[0]), - "end_time": get_image_timestamp(bag_meta[-1]), - "extra_stitch_args": "", - "extra_tour_params": {}, } - scene_meta["images_dir"] = sci_cam_images.get(bag_meta[0]["img_path"]) + scene_meta["images_dir"] = sci_cam_images.get( + bag_meta[0]["img_path"], "REQUIRED" + ) for field, regex in SCENE_REGEXES: - match = regex.search(bag_path) - if match: - val = match.group(field).lower() + m = regex.search(bag_path) + if m: + val = m.group(field).lower() if val.isdigit(): val = int(val) scene_meta[field] = val + else: + scene_meta[field] = None scene_id = "scene%03d" % i for field, regex in SCENE_REGEXES: if field in scene_meta: scene_id += "_%s%s" % (FIELD_PREFIXES.get(field, ""), scene_meta[field]) + scene_meta["scene_id"] = scene_id scenes[scene_id] = scene_meta return pano_meta def write_pano_meta(pano_meta, out_yaml_path): - """ - Write pano metadata to a YAML file, to be used as a configfile for - snakemake. - """ with open(out_yaml_path, "w") as out_yaml: - out_yaml.write( - yaml.safe_dump(pano_meta, default_flow_style=False, sort_keys=False) - ) - print("wrote to %s" % out_yaml_path) - - -def add_pano_meta(new_meta, out_yaml_path): - """ - Add new_meta to the existing pano metadata in the YAML file. - """ - with open(out_yaml_path, "r") as old_yaml_stream: - old_meta = yaml.safe_load(old_yaml_stream) - old_bag_paths = set( - (os.path.realpath(scene["bag_path"]) for scene in old_meta["scenes"].values()) - ) - - merged_meta = copy.deepcopy(old_meta) - scene_prefix = re.compile(r"scene\d\d\d_") - for scene_id, scene_meta in new_meta["scenes"].items(): - new_bag_path = os.path.realpath(scene_meta["bag_path"]) - if new_bag_path in old_bag_paths: - continue - - # renumber scene id - scene_id = scene_prefix.sub("scene%03d_" % len(merged_meta["scenes"]), scene_id) - - merged_meta["scenes"][scene_id] = scene_meta + out_yaml.write(yaml.dump(pano_meta, default_flow_style=False, sort_keys=False)) - num_old = len(old_meta["scenes"]) - num_new = len(new_meta["scenes"]) - num_out = len(merged_meta["scenes"]) - num_added = num_out - num_old - num_skipped = num_new - num_added - - print( - "out of %d panos detected, %d were added and %d existing entries were skipped" - % (num_new, num_added, num_skipped) - ) - - tmp_path = out_yaml_path + ".tmp" - with open(tmp_path, "w") as out_yaml: - out_yaml.write( - yaml.safe_dump(merged_meta, default_flow_style=False, sort_keys=False) - ) - - stem, suffix = os.path.splitext(out_yaml_path) - random.seed() - unique_id = "%0x" % random.getrandbits(32) - old_yaml_backup_path = "%s-old-%s%s" % (stem, unique_id, suffix) - - os.rename(out_yaml_path, old_yaml_backup_path) - os.rename(tmp_path, out_yaml_path) - - print("wrote to %s" % out_yaml_path) - print("old version backed up at %s" % old_yaml_backup_path) - - -def config_panos(in_folder, out_yaml_path, force, add_panos): - if os.path.exists(out_yaml_path) and not (force or add_panos): - print( - "output file %s exists, not overwriting (did you mean --force or --add?)" - % out_yaml_path - ) - sys.exit(1) +def config_panos(in_folder, out_yaml_path): pano_meta = detect_pano_meta(in_folder) - if os.path.exists(out_yaml_path) and add_panos: - add_pano_meta(pano_meta, out_yaml_path) - else: - write_pano_meta(pano_meta, out_yaml_path) + write_pano_meta(pano_meta, out_yaml_path) class CustomFormatter( @@ -225,40 +124,20 @@ def main(): description=__doc__, formatter_class=CustomFormatter ) parser.add_argument( - "-i", - "--in-folder", + "in_folder", type=str, help="input path for folder to search for bag files and SciCam images", default="/input", - required=False, ) parser.add_argument( - "-o", - "--out-yaml", + "out_yaml", type=str, help="output path for YAML pano stitch config", - default="/output/pano_meta.yaml", - required=False, - ) - parser.add_argument( - "-f", - "--force", - action="store_true", - help="overwrite output file if it exists", - default=False, - required=False, - ) - parser.add_argument( - "-a", - "--add", - action="store_true", - help="add new panos to existing file without changing old ones", - default=False, - required=False, + default="/stitch/pano_meta.yaml", ) args = parser.parse_args() - config_panos(args.in_folder, args.out_yaml, args.force, args.add) + config_panos(args.in_folder, args.out_yaml) if __name__ == "__main__": diff --git a/pano/pano_stitch/scripts/pano_image_meta.py b/pano/pano_stitch/scripts/pano_image_meta.py index b20934cd..e70934b6 100755 --- a/pano/pano_stitch/scripts/pano_image_meta.py +++ b/pano/pano_stitch/scripts/pano_image_meta.py @@ -50,9 +50,8 @@ def get_image_meta(inbag_path, num_images=None): images = [] with rosbag.Bag(inbag_path) as bag: img_meta = None - topics = IMAGE_TOPIC + [POSE_TOPIC] - for topic, msg, t in bag.read_messages(topics): - if topic in IMAGE_TOPIC: + for topic, msg, t in bag.read_messages([IMAGE_TOPIC, POSE_TOPIC]): + if topic == IMAGE_TOPIC: if num_images is not None and len(images) == num_images: break From 24cb9444645b96511fdd61d418a361027a04f3c2 Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Fri, 20 Jan 2023 12:22:22 -0800 Subject: [PATCH 03/35] several minor fixes --- pano/pano_stitch/scripts/Snakefile | 52 ++++++++++++++++---- pano/pano_stitch/scripts/config_panos.py | 62 ++++++++++++++++-------- pano/pano_stitch/scripts/dot_dict.py | 31 ++++++++++++ 3 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 pano/pano_stitch/scripts/dot_dict.py diff --git a/pano/pano_stitch/scripts/Snakefile b/pano/pano_stitch/scripts/Snakefile index 0531db8e..26d199f7 100644 --- a/pano/pano_stitch/scripts/Snakefile +++ b/pano/pano_stitch/scripts/Snakefile @@ -1,17 +1,49 @@ +#!/usr/bin/env python +# Copyright (c) 2017, United States Government, as represented by the +# Administrator of the National Aeronautics and Space Administration. +# +# All rights reserved. +# +# The Astrobee platform is licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. -import json -import yaml +""" +Snakemake rules for batch stitching multiple panoramas. +""" + +from dot_dict import DotDict configfile: "/stitch/pano_meta.yaml" rule all: - input: - expand("/stitch/{scene_id}/pano.png", scene_id=config["scenes"].keys()) + input: + expand("/stitch/{scene_id}/pano.png", scene_id=config["scenes"].keys()) + +# We set up an explicit check if the output pano exists before +# restitching. This avoids unwanted restitching of all panos based on +# irrelevant changes to the Snakefile or configfile. Explicitly delete +# the output pano.png file if the pano needs to be restitched. rule stitch: - output: - "/stitch/{scene_id}/pano.png" - params: - m=lambda wildcards: config["scenes"][wildcards.scene_id] - shell: - "/src/isaac/src/pano/pano_stitch/scripts/stitch_panorama.py --no-lens --world=iss --robot={params.m[robot]} --images-dir={params.m[images_dir]} {params.m[bag_path]} --output-dir=/stitch/{wildcards.scene_id}" + output: + "/stitch/{scene_id}/pano.png" + params: + s=lambda wildcards: DotDict(config["scenes"][wildcards.scene_id]) + shell: + ("[ -f {output} ] ||" + " /src/isaac/src/pano/pano_stitch/scripts/stitch_panorama.py" + " --no-lens" + " --robot={params.s.robot}" + " --images-dir={params.s.images_dir}" + " {params.s.bag_path}" + " --output-dir=/stitch/{wildcards.scene_id}" + " {params.s.extra_stitch_args}") diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index 732fca9a..1729c7ca 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -17,14 +17,14 @@ # under the License. """ -Detect panoramas (bag files and associated SciCam images) and write template config file for stitching. - -Example: rosrun pano_stitch scripts/config_panos.py /input /stitch/pano_meta.yaml +Detect panoramas (bag files and associated SciCam images) and write +template config file for stitching. """ import argparse import os import re +import sys import yaml @@ -52,6 +52,12 @@ def detect_pano_meta(in_folder): + """ + Detect panoramas (bag files and associated SciCam images). Return + pano metadata. + """ + + bags = {} sci_cam_images = {} for dirname, subdirs, files in os.walk(in_folder): @@ -70,47 +76,53 @@ def detect_pano_meta(in_folder): continue scene_meta = { - "scene_id": None, "bag_path": bag_path, "images_dir": None, "robot": None, "activity": None, "module": None, "bay": None, + "extra_stitch_args": "", } - scene_meta["images_dir"] = sci_cam_images.get( - bag_meta[0]["img_path"], "REQUIRED" - ) + scene_meta["images_dir"] = sci_cam_images.get(bag_meta[0]["img_path"]) for field, regex in SCENE_REGEXES: - m = regex.search(bag_path) - if m: - val = m.group(field).lower() + match = regex.search(bag_path) + if match: + val = match.group(field).lower() if val.isdigit(): val = int(val) scene_meta[field] = val - else: - scene_meta[field] = None scene_id = "scene%03d" % i for field, regex in SCENE_REGEXES: if field in scene_meta: scene_id += "_%s%s" % (FIELD_PREFIXES.get(field, ""), scene_meta[field]) - scene_meta["scene_id"] = scene_id scenes[scene_id] = scene_meta return pano_meta -def write_pano_meta(pano_meta, out_yaml_path): +def write_pano_meta(pano_meta, out_yaml_path, force): + """ + Write pano metadata to a YAML file, to be used as a configfile for + snakemake. + """ + if os.path.exists(out_yaml_path) and not force: + print( + "output file %s exists and --force not specified, not overwriting" + % out_yaml_path + ) + sys.exit(1) with open(out_yaml_path, "w") as out_yaml: out_yaml.write(yaml.dump(pano_meta, default_flow_style=False, sort_keys=False)) + print("wrote to %s" % out_yaml_path) -def config_panos(in_folder, out_yaml_path): +def config_panos(in_folder, out_yaml_path, force): pano_meta = detect_pano_meta(in_folder) - write_pano_meta(pano_meta, out_yaml_path) + write_pano_meta(pano_meta, out_yaml_path, force) class CustomFormatter( @@ -124,20 +136,32 @@ def main(): description=__doc__, formatter_class=CustomFormatter ) parser.add_argument( - "in_folder", + "-i", + "--in-folder", type=str, help="input path for folder to search for bag files and SciCam images", default="/input", + required=False, ) parser.add_argument( - "out_yaml", + "-o", + "--out-yaml", type=str, help="output path for YAML pano stitch config", default="/stitch/pano_meta.yaml", + required=False, + ) + parser.add_argument( + "-f", + "--force", + action="store_true", + help="overwrite output file if it exists", + default=False, + required=False, ) args = parser.parse_args() - config_panos(args.in_folder, args.out_yaml) + config_panos(args.in_folder, args.out_yaml, args.force) if __name__ == "__main__": diff --git a/pano/pano_stitch/scripts/dot_dict.py b/pano/pano_stitch/scripts/dot_dict.py new file mode 100644 index 00000000..1c669b57 --- /dev/null +++ b/pano/pano_stitch/scripts/dot_dict.py @@ -0,0 +1,31 @@ +# Copyright (c) 2017, United States Government, as represented by the +# Administrator of the National Aeronautics and Space Administration. +# +# All rights reserved. +# +# The Astrobee platform is licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Convenience wrapper for dict so you can access members with dot notation. +""" + + +class DotDict(dict): + """ + Convenience wrapper for dict so you can access members with dot notation. + """ + + def __getattr__(self, attr): + if attr in self: + return self[attr] + return super(DotDict, self).__getattribute__(attr) From b210c1ebbded7512fb2d600cdcb3d19131506597 Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Fri, 20 Jan 2023 15:54:33 -0800 Subject: [PATCH 04/35] make process more configurable; move some files; add docs --- pano/README.md | 101 +++++------------- pano/docker/pano.Dockerfile | 63 +---------- pano/pano_stitch/docker/build.sh | 4 - pano/pano_stitch/docker/exec.sh | 2 - .../pano_stitch/docker/pano_stitch.Dockerfile | 16 --- pano/pano_stitch/docker/run.sh | 20 ---- pano/pano_stitch/scripts/Snakefile | 11 +- pano/pano_stitch/scripts/config_panos.py | 3 +- 8 files changed, 36 insertions(+), 184 deletions(-) delete mode 100755 pano/pano_stitch/docker/build.sh delete mode 100755 pano/pano_stitch/docker/exec.sh delete mode 100644 pano/pano_stitch/docker/pano_stitch.Dockerfile delete mode 100755 pano/pano_stitch/docker/run.sh diff --git a/pano/README.md b/pano/README.md index 3485af31..649be57b 100644 --- a/pano/README.md +++ b/pano/README.md @@ -6,7 +6,7 @@ This document describes how to stitch panoramas collected by an Astrobee robot f We follow an approach using a Docker container that should be highly repeatable. Much of this process can also be run natively in your host OS if needed, but we don't cover that. -## Install the Docker image for panorama stitching +## Install the Docker container for panorama stitching ### Prerequisites @@ -14,7 +14,7 @@ We follow an approach using a Docker container that should be highly repeatable. - Check out the source code from the `isaac` repo and set the `ISAAC_WS` environment variable following the [Instructions on installing and using the ISAAC Software](https://nasa.github.io/isaac/html/md_INSTALL.html) -### Build the Docker image +### Build the container Run: ```bash @@ -26,8 +26,8 @@ $ISAAC_WS/src/pano/docker/build.sh ### Input requirements The expected input data for stitching a single panorama is: -- `bag_path`: Points to an Astrobee telemetry bag file recorded during an ISAAC panorama-style survey. The only SciCam image timestamps in the bag should be from within a single panorama. -- `images_dir`: Points to a folder containing the full-resolution SciCam image JPEG files saved on the HLP at the same time when the bag was recorded. Note that it's typical and no problem if a single image folder contains SciCam images that span multiple panoramas (and multiple bag files). However, the stitching script does assume all SciCam images for any given bag are in the same folder. +- `bag_path`: An Astrobee telemetry bag file recorded during an ISAAC panorama-style survey. The only SciCam image timestamps in the bag should be from within a single panorama. +- `images_dir`: A folder containing the full-resolution SciCam image JPEG files saved on the HLP at the same time when the bag was recorded. Note that it's typical and no problem if a single image folder contains SciCam images that span multiple panoramas (and multiple bag files). However, the stitching script does assume all SciCam images for any given bag are in the same folder. Multiple panoramas can be stitched in a single batch job. @@ -35,7 +35,7 @@ Multiple panoramas can be stitched in a single batch job. Here are some typical pre-processing steps you might need before stitching when working with real Astrobee data: -- The documentation on [Using Astrobee Robot Telemetry Logs](https://nasa.github.io/astrobee/html/using_telemetry.html) applies. For example, if processing older bags that have obsolete message types, you might need to run the `rosbag_fix_all.py` script. +- The documentation on [Using Astrobee Robot Telemetry Logs](https://nasa.github.io/astrobee/html/using_telemetry.html) applies. For example, if using bags that have obsolete message types, you might need to run the `rosbag_fix_all.py` script. - If you have a bag that includes telemetry from multiple panoramas (or other SciCam image timestamps), you should split it up so that each bag contains one panorama and no other SciCam data. - You may find it more convenient to work with filtered telemetry bags that contains only the messages required by the panorama stitching. This is particularly useful if you need to transfer the bags to a different host before stitching (minimizing transfer data volume and storage required on the stitching host). It also speeds up stitching slightly. You can generate filtered bags like this: ```bash @@ -45,7 +45,6 @@ Here are some typical pre-processing steps you might need before stitching when ### Configure folders for processing -Run: ```bash # choose your own folders in the host OS export ISAAC_PANO_INPUT="$HOME/pano_input" @@ -84,7 +83,7 @@ $ISAAC_WS/src/pano/docker/exec.sh mycmd arg1 arg2 ... Inside the container, run: ```bash -rosrun pano_stitch scripts/config_panos.py +/src/isaac/src/pano/scripts/config_panos.py ``` This will create the panorama config file `pano_meta.yaml` in the output folder. You should verify the config file looks correct by opening `$ISAAC_PANO_OUTPUT/pano_meta.yaml` in your favorite editor in the host OS. @@ -92,97 +91,49 @@ This will create the panorama config file `pano_meta.yaml` in the output folder. Below is an example `pano_meta.yaml`: ```yaml scenes: - scene000_isaac10_queen_nod2_bay2: - bag_path: /input/isaac10_queen/20220617_1554_survey_nod2_bay2_std_panorama.bag - images_dir: /input/isaac10_queen/isaac_sci_cam_image_delayed - robot: queen - activity: isaac10 - module: nod2 - bay: 2 - position: - x: 10.996580718096382 - y: 0.0018100984828177873 - z: 4.899023194998069 - start_time: '2022-06-17T15:57:23.139000Z' - end_time: '2022-06-17T16:08:06.880000Z' - extra_stitch_args: '' - extra_tour_params: {} - scene001_isaac11_bumble_usl_bay6: - bag_path: /input/isaac11_bumble/20220711_1238_survey_usl_bay6_std_panorama_run1.bag + scene000_isaac11_bumble_usl_bay4: + bag_path: /input/isaac11_bumble/isaac11_bumble_usl_bay4.bag images_dir: /input/isaac11_bumble/isaac_sci_cam_image_delayed robot: bumble activity: isaac11 module: usl - bay: 6 - position: - x: -0.3593667375653261 - y: 0.0072030887961762385 - z: 4.885617819225414 - start_time: '2022-07-11T12:40:00.841000Z' - end_time: '2022-07-11T12:51:01.391000Z' + bay: 4 + extra_stitch_args: '' + scene001_isaac11_queen_usl_bay1: + bag_path: /input/isaac11_queen/isaac11_queen_usl_bay1.bag + images_dir: /input/isaac11_queen/isaac_sci_cam_image_delayed + robot: queen + activity: isaac11 + module: usl + bay: 1 extra_stitch_args: '' - extra_tour_params: {} ``` -Ideally, the `config_panos.py` script will set all the config fields correctly, but it is not especially smart and could get fooled in some situations. It fills many of the later fields by attempting to parse the `bag_path`. If you want its auto-configure to work better, you can rename your bags in advance to filenames that follow the conventions in the example. If a field is not detected, its value will be set to `null`. +Ideally, the `config_panos.py` script will set all the config fields correctly, but it is not especially smart and could get fooled in some situations. It fills many of the later fields by attempting to parse the `bag_path`. You can help it by renaming your bags to filenames that follow the conventions in the example. If it fails to auto-configure a field, its value will be set to `null`. Here's what to check in the config file: - Each entry in the `scenes` list defines a single panorama to stitch. All of your panoramas should appear in the list. - The header line for each panorama defines its `scene_id`. This id is normally not important, but it does control the name of the output subfolder for that panorama, as well as its scene id in the Pannellum tour. If you find a meaningful id helpful for debugging, you can set it to whatever you like, as long as every panorama has a unique id. -- The `bag_path` and `images_dir` fields should contain valid paths that specify the location of the input data for that panorama. They should match, in that all of the SciCam image timestamps in the bag should refer to SciCam images in the folder. +- The `bag_path` and `images_dir` fields should contain valid paths that specify the location of the input data for that panorama. They should match, in that the SciCam image timestamps in the bag should correspond to SciCam images in the folder. - The `robot` field should correctly specify the robot that collected the panorama. This field is used to look up the correct SciCam lens calibration parameters to use during stitching. -- The `activity`, `module`, `bay`, `position`, `start_time`, and `end_time` fields don't affect the stitching step but should be set correctly so they can be displayed to users in the final output tour. -- The `extra_stitch_args` field provides a way for advanced users to pass extra options to the `stitch_panorama.py` script for a specific panorama. It would typically be used for debugging and tuning when the stitching process fails or produces a low-quality result. -- The `extra_tour_params` field provides a way for advanced users to overwrite the parameters for a specific panorama in the `tour.json` file that configures the Pannellum display interface. +- The `activity`, `module`, and `bay` fields don't affect the stitching step but should be filled in correctly because they are displayed to users in the final output tour. +- The `extra_stitch_args` field provides a way for advanced users to modify the stitching parameters on a per-panorama basis. It would typically be used for debugging and tuning when the stitching process fails. -### Stitch the panoramas and generate the tour +### Stitch the panoramas Inside the container, run: ```bash -snakemake -s /src/isaac/src/pano/pano_view/scripts/Snakefile -d /output -c1 +snakemake -s /src/isaac/src/pano/pano_stitch/scripts/Snakefile -d /output -c1 ``` This will trigger the `snakemake` build system to stitch the panoramas. There is one job per panorama in the config file, and `snakemake` will try to run these jobs in parallel up to the number of cores specified in the `-c` argument. (When `-c` is specified with no arguments, it will use the number of cores allocated to the container.) Note that some of the individual steps within each panorama stitch (e.g., `enblend`) run multi-threaded, and each individually tries to use all available cores, which could cause problems when stitching multiple panoramas in parallel. Both `snakemake` and `enblend` provide ways to manage this, which could be an area for future work. -### View the tour +### Generate the tour TODO +### View the tour -### Generate target pose from image - -Once you have the json file exported you can generate the target pose where the attitude of the target is extracted based on the camera point of view at the time of the picture: -(1) use the bagfile where that panorama was recorded to extract the point cloud and estimate target pose -(2) use the mesh generated by the geometry mapper (optional) -or both and compare results. To use the tool, you will first have to define `ASTROBEE_CONFIG_DIR`, `ASTROBEE_RESOURCE_DIR`, `ASTROBEE_ROBOT`, `ASTROBEE_WORLD`, and then run: - - rosrun pano_view find_point_coordinate [OPTIONS] - -General parameters -| Parameter | Default value | Description | -| -------------------- | --------------------- | -------------------------------------------------------- | -| camera | "sci_cam" | Camera to use | -| depth | "haz" | Depth camera name | -| json_config | "" | json file with configure data | -| bag_name | "" | Bagname where image can be found. Make sure it has haz cam and ground truth| -| mesh_name | "" | Meshfile path | -| depth_cam_topic | "/hw/depth_haz/points"| Point Cloud topic name | -| ground_truth_topic | "/gnc/ekf" | Robot pose topic name | - -The script will print in the screen the target pose estimates. -An example of this script would be: - - rosrun pano_view find_point_coordinate -bag_name bag_name.bag -mesh_name mesh_name.obj -json_config test/viewpoint.json - -An example output would be: - - Closest timestamp depth: 0.050772; Closest timestamp pose: 0.000275373 - Point Cloud Point to vector distance: 0.00341976 - Intersection point pcl: (0.997071, 0.238026, -0.571279)(0,4.71044,-14.8078) - Intersection point mesh: (0.993323, 0.237302, -0.5721)(0,4.71044,-14.8078) - -Things to look for are that the timestamps are not too large, this would mean that results are unreliable. -Pay attention to the point cloud point to vector distance to make sure that the point is not too far away from the target vector, meaning that the point cloud does not include the target which is possible due to different field of views between the cameras. -Lastly the mesh and pcl should not disagree, if they do, some manual analysis is needed. Be aware that the attitude provided is defined by the direction pointing at the target assuming roll zero. \ No newline at end of file +TODO \ No newline at end of file diff --git a/pano/docker/pano.Dockerfile b/pano/docker/pano.Dockerfile index ea986cd4..d6771894 100644 --- a/pano/docker/pano.Dockerfile +++ b/pano/docker/pano.Dockerfile @@ -4,72 +4,13 @@ ARG REMOTE=ghcr.io/nasa FROM ${REMOTE}/isaac:latest-ubuntu${UBUNTU_VERSION} -# default-jre: Java runtime needed for minifying Pannellum web files -# hugin: pano stitching tools (and hsi Python interface) -# libvips-tools: convert images to multires, zoomable in OpenSeaDragon -# python3-pip: for installing Python packages later in this Dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ - default-jre \ hugin \ - libvips-tools \ python3-pip \ - gfortran libopenblas-dev libfftw3-dev \ && rm -rf /var/lib/apt/lists/* -# pandas: pulled in as pyshtools dependency but install breaks if not mentioned explicitly (?) -# pyshtools: used during Pannellum multires generation -# snakemake: modern build system based on Python, manages stitching workflows - -# Install Jupyter explicitly first -RUN pip3 install --no-cache-dir --upgrade pip && \ - pip3 install --no-cache-dir jupyter - -# Install other Python packages: jupyter package needs to be installed before attempting to build pyshtools -RUN pip3 install --no-cache-dir pandas pyshtools snakemake pulp==2.7 --ignore-installed PyYAML - -# pannellum: library for viewing/navigating panorama tours -RUN mkdir -p /opt \ - && cd /opt \ - && git clone --quiet --depth 1 --branch standalone_load_event --single-branch --no-tags https://github.com/trey0/pannellum.git \ - && cd /opt/pannellum/utils/build \ - && python3 build.py - -# openseadragon: library for viewing high-res SciCam source images efficiently with zoom -RUN cd /tmp \ - && wget --quiet https://github.com/openseadragon/openseadragon/releases/download/v4.0.0/openseadragon-bin-4.0.0.tar.gz \ - && tar xfz openseadragon-bin-4.0.0.tar.gz \ - && rm openseadragon-bin-4.0.0.tar.gz \ - && mv openseadragon-bin-4.0.0 /opt/openseadragon - -# annotorious-openseadragon: image annotation plugin for OpenSeaDragon -RUN cd /tmp \ - && wget --quiet https://github.com/recogito/annotorious-openseadragon/releases/download/v2.7.10/annotorious-openseadragon-2.7.10.zip \ - && mkdir -p /tmp/anno \ - && cd /tmp/anno \ - && unzip -q ../annotorious-openseadragon-2.7.10.zip \ - && cd /tmp \ - && rm annotorious-openseadragon-2.7.10.zip \ - && mv anno /opt/annotorious-openseadragon - -# annotorious-selectorpack: additional geometry types for annotorious-seadragon -RUN cd /tmp \ - && wget --quiet https://github.com/recogito/annotorious-selector-pack/releases/download/v0.5.1/annotorious-selectorpack-0.5.1.zip \ - && mkdir -p /tmp/anno \ - && cd /tmp/anno \ - && unzip -q ../annotorious-selectorpack-0.5.1.zip \ - && cd /tmp \ - && rm annotorious-selectorpack-0.5.1.zip \ - && mv anno /opt/annotorious-selectorpack - -# annotorious-toolbar: toolbar for adding annotorious annotations -RUN mkdir -p /opt/annotorious-toolbar \ - && cd /opt/annotorious-toolbar \ - && wget --quiet https://cdn.jsdelivr.net/npm/@recogito/annotorious-toolbar@1.1.0/dist/annotorious-toolbar.min.js +RUN pip3 install --no-cache-dir --upgrade pip \ + && pip3 install --no-cache-dir snakemake RUN echo 'source "/src/isaac/devel/setup.bash"\nexport ASTROBEE_CONFIG_DIR="/src/astrobee/src/astrobee/config"' >> "${HOME}/.bashrc" - -# Enables rosrun for pano packages. Can likely take this out -# once new pano folder is merged into develop and official docker -# images are updated. -RUN echo 'export ROS_PACKAGE_PATH="${ROS_PACKAGE_PATH}:/src/isaac/src/pano/pano_stitch::/src/isaac/src/pano/pano_view"' >> "${HOME}/.bashrc" diff --git a/pano/pano_stitch/docker/build.sh b/pano/pano_stitch/docker/build.sh deleted file mode 100755 index 38f817e9..00000000 --- a/pano/pano_stitch/docker/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -x -cd ${HOME}/isaac/src -docker build . -f pano/pano_stitch/docker/pano_stitch.Dockerfile -t isaac/pano_stitch diff --git a/pano/pano_stitch/docker/exec.sh b/pano/pano_stitch/docker/exec.sh deleted file mode 100755 index c13138b8..00000000 --- a/pano/pano_stitch/docker/exec.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -docker exec -it isaac_pano_stitch /bin/bash -ic "$*" diff --git a/pano/pano_stitch/docker/pano_stitch.Dockerfile b/pano/pano_stitch/docker/pano_stitch.Dockerfile deleted file mode 100644 index d6771894..00000000 --- a/pano/pano_stitch/docker/pano_stitch.Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ - -ARG UBUNTU_VERSION=20.04 -ARG REMOTE=ghcr.io/nasa - -FROM ${REMOTE}/isaac:latest-ubuntu${UBUNTU_VERSION} - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - hugin \ - python3-pip \ - && rm -rf /var/lib/apt/lists/* - -RUN pip3 install --no-cache-dir --upgrade pip \ - && pip3 install --no-cache-dir snakemake - -RUN echo 'source "/src/isaac/devel/setup.bash"\nexport ASTROBEE_CONFIG_DIR="/src/astrobee/src/astrobee/config"' >> "${HOME}/.bashrc" diff --git a/pano/pano_stitch/docker/run.sh b/pano/pano_stitch/docker/run.sh deleted file mode 100755 index 58080162..00000000 --- a/pano/pano_stitch/docker/run.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -if [ "$ISAAC_PANO_INPUT" = "" ] || [ ! -d "$ISAAC_PANO_INPUT" ]; then - echo "ISAAC_PANO_INPUT env var must point to input folder" - exit 1 -fi -if [ "$ISAAC_PANO_STITCH" = "" ] || [ ! -d "$ISAAC_PANO_STITCH" ]; then - echo "ISAAC_PANO_STITCH env var must point to output folder" - exit 1 -fi - -set -x - -cd ${HOME}/isaac/src -docker run \ - -it --rm \ - --name isaac_pano_stitch \ - --mount type=bind,source=${ISAAC_PANO_INPUT},target=/input,readonly \ - --mount type=bind,source=${ISAAC_PANO_STITCH},target=/stitch \ - --mount type=bind,source=$(pwd),target=/src/isaac/src \ - isaac/pano_stitch diff --git a/pano/pano_stitch/scripts/Snakefile b/pano/pano_stitch/scripts/Snakefile index 26d199f7..190ca32b 100644 --- a/pano/pano_stitch/scripts/Snakefile +++ b/pano/pano_stitch/scripts/Snakefile @@ -22,11 +22,14 @@ Snakemake rules for batch stitching multiple panoramas. from dot_dict import DotDict -configfile: "/stitch/pano_meta.yaml" +# When using this Snakefile, use the --directory option to snakemake to set the +# desired output directory (e.g., "/output"). + +configfile: "pano_meta.yaml" rule all: input: - expand("/stitch/{scene_id}/pano.png", scene_id=config["scenes"].keys()) + expand("{scene_id}/pano.png", scene_id=config["scenes"].keys()) # We set up an explicit check if the output pano exists before # restitching. This avoids unwanted restitching of all panos based on @@ -35,7 +38,7 @@ rule all: rule stitch: output: - "/stitch/{scene_id}/pano.png" + "{scene_id}/pano.png" params: s=lambda wildcards: DotDict(config["scenes"][wildcards.scene_id]) shell: @@ -45,5 +48,5 @@ rule stitch: " --robot={params.s.robot}" " --images-dir={params.s.images_dir}" " {params.s.bag_path}" - " --output-dir=/stitch/{wildcards.scene_id}" + " --output-dir={wildcards.scene_id}" " {params.s.extra_stitch_args}") diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index 1729c7ca..5e2477f9 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -57,7 +57,6 @@ def detect_pano_meta(in_folder): pano metadata. """ - bags = {} sci_cam_images = {} for dirname, subdirs, files in os.walk(in_folder): @@ -148,7 +147,7 @@ def main(): "--out-yaml", type=str, help="output path for YAML pano stitch config", - default="/stitch/pano_meta.yaml", + default="/output/pano_meta.yaml", required=False, ) parser.add_argument( From 438692c377148b6b345ab677766440469cd2bbdf Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Tue, 24 Jan 2023 07:25:16 -0800 Subject: [PATCH 05/35] make GitHub isort happy --- pano/README.md | 17 +++++++++-------- pano/pano_stitch/scripts/config_panos.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pano/README.md b/pano/README.md index 649be57b..7feafd1d 100644 --- a/pano/README.md +++ b/pano/README.md @@ -6,7 +6,7 @@ This document describes how to stitch panoramas collected by an Astrobee robot f We follow an approach using a Docker container that should be highly repeatable. Much of this process can also be run natively in your host OS if needed, but we don't cover that. -## Install the Docker container for panorama stitching +## Install the Docker image for panorama stitching ### Prerequisites @@ -14,7 +14,7 @@ We follow an approach using a Docker container that should be highly repeatable. - Check out the source code from the `isaac` repo and set the `ISAAC_WS` environment variable following the [Instructions on installing and using the ISAAC Software](https://nasa.github.io/isaac/html/md_INSTALL.html) -### Build the container +### Build the Docker image Run: ```bash @@ -26,8 +26,8 @@ $ISAAC_WS/src/pano/docker/build.sh ### Input requirements The expected input data for stitching a single panorama is: -- `bag_path`: An Astrobee telemetry bag file recorded during an ISAAC panorama-style survey. The only SciCam image timestamps in the bag should be from within a single panorama. -- `images_dir`: A folder containing the full-resolution SciCam image JPEG files saved on the HLP at the same time when the bag was recorded. Note that it's typical and no problem if a single image folder contains SciCam images that span multiple panoramas (and multiple bag files). However, the stitching script does assume all SciCam images for any given bag are in the same folder. +- `bag_path`: Points to an Astrobee telemetry bag file recorded during an ISAAC panorama-style survey. The only SciCam image timestamps in the bag should be from within a single panorama. +- `images_dir`: Points to a folder containing the full-resolution SciCam image JPEG files saved on the HLP at the same time when the bag was recorded. Note that it's typical and no problem if a single image folder contains SciCam images that span multiple panoramas (and multiple bag files). However, the stitching script does assume all SciCam images for any given bag are in the same folder. Multiple panoramas can be stitched in a single batch job. @@ -35,7 +35,7 @@ Multiple panoramas can be stitched in a single batch job. Here are some typical pre-processing steps you might need before stitching when working with real Astrobee data: -- The documentation on [Using Astrobee Robot Telemetry Logs](https://nasa.github.io/astrobee/html/using_telemetry.html) applies. For example, if using bags that have obsolete message types, you might need to run the `rosbag_fix_all.py` script. +- The documentation on [Using Astrobee Robot Telemetry Logs](https://nasa.github.io/astrobee/html/using_telemetry.html) applies. For example, if processing older bags that have obsolete message types, you might need to run the `rosbag_fix_all.py` script. - If you have a bag that includes telemetry from multiple panoramas (or other SciCam image timestamps), you should split it up so that each bag contains one panorama and no other SciCam data. - You may find it more convenient to work with filtered telemetry bags that contains only the messages required by the panorama stitching. This is particularly useful if you need to transfer the bags to a different host before stitching (minimizing transfer data volume and storage required on the stitching host). It also speeds up stitching slightly. You can generate filtered bags like this: ```bash @@ -45,6 +45,7 @@ Here are some typical pre-processing steps you might need before stitching when ### Configure folders for processing +Run: ```bash # choose your own folders in the host OS export ISAAC_PANO_INPUT="$HOME/pano_input" @@ -109,15 +110,15 @@ scenes: extra_stitch_args: '' ``` -Ideally, the `config_panos.py` script will set all the config fields correctly, but it is not especially smart and could get fooled in some situations. It fills many of the later fields by attempting to parse the `bag_path`. You can help it by renaming your bags to filenames that follow the conventions in the example. If it fails to auto-configure a field, its value will be set to `null`. +Ideally, the `config_panos.py` script will set all the config fields correctly, but it is not especially smart and could get fooled in some situations. It fills many of the later fields by attempting to parse the `bag_path`. If you want its auto-configure to work better, you can rename your bags in advance to filenames that follow the conventions in the example. If a field is not detected, its value will be set to `null`. Here's what to check in the config file: - Each entry in the `scenes` list defines a single panorama to stitch. All of your panoramas should appear in the list. - The header line for each panorama defines its `scene_id`. This id is normally not important, but it does control the name of the output subfolder for that panorama, as well as its scene id in the Pannellum tour. If you find a meaningful id helpful for debugging, you can set it to whatever you like, as long as every panorama has a unique id. -- The `bag_path` and `images_dir` fields should contain valid paths that specify the location of the input data for that panorama. They should match, in that the SciCam image timestamps in the bag should correspond to SciCam images in the folder. +- The `bag_path` and `images_dir` fields should contain valid paths that specify the location of the input data for that panorama. They should match, in that all of the SciCam image timestamps in the bag should refer to SciCam images in the folder. - The `robot` field should correctly specify the robot that collected the panorama. This field is used to look up the correct SciCam lens calibration parameters to use during stitching. - The `activity`, `module`, and `bay` fields don't affect the stitching step but should be filled in correctly because they are displayed to users in the final output tour. -- The `extra_stitch_args` field provides a way for advanced users to modify the stitching parameters on a per-panorama basis. It would typically be used for debugging and tuning when the stitching process fails. +- The `extra_stitch_args` field provides a way for advanced users to pass extra options to the `stitch_panorama.py` script on a per-panorama basis. It would typically be used for debugging and tuning when the stitching process fails or produces a low-quality result. ### Stitch the panoramas diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index 5e2477f9..e6478720 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -26,9 +26,9 @@ import re import sys +import pano_image_meta import yaml -import pano_image_meta SCI_CAM_IMG_REGEX = re.compile(r"\d{10}\.\d{3}\.jpg$") From a5222ced6d41350970cb2b96638b0701e5f31cde Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Tue, 24 Jan 2023 07:29:46 -0800 Subject: [PATCH 06/35] make GitHub isort happy, take 2 --- pano/pano_stitch/scripts/config_panos.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index e6478720..c5d4a3aa 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -29,7 +29,6 @@ import pano_image_meta import yaml - SCI_CAM_IMG_REGEX = re.compile(r"\d{10}\.\d{3}\.jpg$") ROBOT_REGEX = re.compile(r"(\b|_)(?Phoney|bumble|queen)(\b|_)", re.IGNORECASE) From 25e393233777a0d4183caf23f9c933808e491732 Mon Sep 17 00:00:00 2001 From: Trey Smith Date: Wed, 25 Jan 2023 14:55:50 -0800 Subject: [PATCH 07/35] add pano_view --- pano/README.md | 12 +- pano/docker/pano.Dockerfile | 24 +- pano/pano_stitch/scripts/Snakefile | 52 ---- pano/pano_stitch/scripts/config_panos.py | 24 +- pano/pano_stitch/scripts/dot_dict.py | 31 --- pano/pano_view/package.xml | 17 -- pano/pano_view/scripts/Snakefile | 15 +- pano/pano_view/scripts/generate_tour.py | 298 ++--------------------- pano/pano_view/static/css/isaac_pano.css | 86 +------ pano/pano_view/static/js/isaac_pano.js | 112 +-------- pano/pano_view/templates/index.html | 7 +- pano/pano_view/templates/pannellum.htm | 11 - 12 files changed, 79 insertions(+), 610 deletions(-) delete mode 100644 pano/pano_stitch/scripts/Snakefile delete mode 100644 pano/pano_stitch/scripts/dot_dict.py diff --git a/pano/README.md b/pano/README.md index 7feafd1d..1855707a 100644 --- a/pano/README.md +++ b/pano/README.md @@ -84,7 +84,7 @@ $ISAAC_WS/src/pano/docker/exec.sh mycmd arg1 arg2 ... Inside the container, run: ```bash -/src/isaac/src/pano/scripts/config_panos.py +rosrun pano_stitch scripts/config_panos.py ``` This will create the panorama config file `pano_meta.yaml` in the output folder. You should verify the config file looks correct by opening `$ISAAC_PANO_OUTPUT/pano_meta.yaml` in your favorite editor in the host OS. @@ -117,24 +117,20 @@ Here's what to check in the config file: - The header line for each panorama defines its `scene_id`. This id is normally not important, but it does control the name of the output subfolder for that panorama, as well as its scene id in the Pannellum tour. If you find a meaningful id helpful for debugging, you can set it to whatever you like, as long as every panorama has a unique id. - The `bag_path` and `images_dir` fields should contain valid paths that specify the location of the input data for that panorama. They should match, in that all of the SciCam image timestamps in the bag should refer to SciCam images in the folder. - The `robot` field should correctly specify the robot that collected the panorama. This field is used to look up the correct SciCam lens calibration parameters to use during stitching. -- The `activity`, `module`, and `bay` fields don't affect the stitching step but should be filled in correctly because they are displayed to users in the final output tour. +- The `activity`, `module`, `bay`, `position`, `start_time`, and `end_time` fields don't affect the stitching step but should be set correctly so they can be displayed to users in the final output tour. - The `extra_stitch_args` field provides a way for advanced users to pass extra options to the `stitch_panorama.py` script on a per-panorama basis. It would typically be used for debugging and tuning when the stitching process fails or produces a low-quality result. -### Stitch the panoramas +### Stitch the panoramas and generate the tour Inside the container, run: ```bash -snakemake -s /src/isaac/src/pano/pano_stitch/scripts/Snakefile -d /output -c1 +snakemake -s /src/isaac/src/pano/pano_view/scripts/Snakefile -d /output -c1 ``` This will trigger the `snakemake` build system to stitch the panoramas. There is one job per panorama in the config file, and `snakemake` will try to run these jobs in parallel up to the number of cores specified in the `-c` argument. (When `-c` is specified with no arguments, it will use the number of cores allocated to the container.) Note that some of the individual steps within each panorama stitch (e.g., `enblend`) run multi-threaded, and each individually tries to use all available cores, which could cause problems when stitching multiple panoramas in parallel. Both `snakemake` and `enblend` provide ways to manage this, which could be an area for future work. -### Generate the tour - -TODO - ### View the tour TODO \ No newline at end of file diff --git a/pano/docker/pano.Dockerfile b/pano/docker/pano.Dockerfile index d6771894..a2c842e6 100644 --- a/pano/docker/pano.Dockerfile +++ b/pano/docker/pano.Dockerfile @@ -4,13 +4,35 @@ ARG REMOTE=ghcr.io/nasa FROM ${REMOTE}/isaac:latest-ubuntu${UBUNTU_VERSION} +# default-jre: Java runtime needed for minifying Pannellum web files +# hugin: pano stitching tools (and hsi Python interface) +# python3-pip: for installing Python packages later in this Dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ + default-jre \ hugin \ python3-pip \ && rm -rf /var/lib/apt/lists/* +# pandas: pulled in as pyshtools dependency but install breaks if not mentioned explicitly (?) +# pyshtools: used during Pannellum multires generation +# snakemake: modern build system based on Python, manages stitching workflows RUN pip3 install --no-cache-dir --upgrade pip \ - && pip3 install --no-cache-dir snakemake + && pip3 install --no-cache-dir \ + pandas \ + pyshtools \ + snakemake + +# pannellum: library for viewing/navigating panorama tours +RUN mkdir -p /opt \ + && cd /opt \ + && git clone --quiet --depth 1 --single-branch --no-tags https://github.com/mpetroff/pannellum.git \ + && cd /opt/pannellum/utils/build \ + && python3 build.py RUN echo 'source "/src/isaac/devel/setup.bash"\nexport ASTROBEE_CONFIG_DIR="/src/astrobee/src/astrobee/config"' >> "${HOME}/.bashrc" + +# Enables rosrun for pano packages. Can likely take this out +# once new pano folder is merged into develop and official docker +# images are updated. +RUN echo 'export ROS_PACKAGE_PATH="${ROS_PACKAGE_PATH}:/src/isaac/src/pano/pano_stitch::/src/isaac/src/pano/pano_view"' >> "${HOME}/.bashrc" diff --git a/pano/pano_stitch/scripts/Snakefile b/pano/pano_stitch/scripts/Snakefile deleted file mode 100644 index 190ca32b..00000000 --- a/pano/pano_stitch/scripts/Snakefile +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2017, United States Government, as represented by the -# Administrator of the National Aeronautics and Space Administration. -# -# All rights reserved. -# -# The Astrobee platform is licensed under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with the -# License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Snakemake rules for batch stitching multiple panoramas. -""" - -from dot_dict import DotDict - -# When using this Snakefile, use the --directory option to snakemake to set the -# desired output directory (e.g., "/output"). - -configfile: "pano_meta.yaml" - -rule all: - input: - expand("{scene_id}/pano.png", scene_id=config["scenes"].keys()) - -# We set up an explicit check if the output pano exists before -# restitching. This avoids unwanted restitching of all panos based on -# irrelevant changes to the Snakefile or configfile. Explicitly delete -# the output pano.png file if the pano needs to be restitched. - -rule stitch: - output: - "{scene_id}/pano.png" - params: - s=lambda wildcards: DotDict(config["scenes"][wildcards.scene_id]) - shell: - ("[ -f {output} ] ||" - " /src/isaac/src/pano/pano_stitch/scripts/stitch_panorama.py" - " --no-lens" - " --robot={params.s.robot}" - " --images-dir={params.s.images_dir}" - " {params.s.bag_path}" - " --output-dir={wildcards.scene_id}" - " {params.s.extra_stitch_args}") diff --git a/pano/pano_stitch/scripts/config_panos.py b/pano/pano_stitch/scripts/config_panos.py index c5d4a3aa..8eebd500 100755 --- a/pano/pano_stitch/scripts/config_panos.py +++ b/pano/pano_stitch/scripts/config_panos.py @@ -22,10 +22,12 @@ """ import argparse +import datetime import os import re import sys +import numpy as np import pano_image_meta import yaml @@ -50,6 +52,23 @@ } +def get_scene_position(bag_meta): + pos_data = np.zeros((len(bag_meta), 3)) + for i, image_meta in enumerate(bag_meta): + pos_data[i, :] = [image_meta["x"], image_meta["y"], image_meta["z"]] + median_pos = np.median(pos_data, axis=0) + return { + "x": float(median_pos[0]), + "y": float(median_pos[1]), + "z": float(median_pos[2]), + } + + +def get_image_timestamp(image_meta): + timestamp = datetime.datetime.utcfromtimestamp(image_meta["timestamp"]) + return timestamp.isoformat() + "Z" + + def detect_pano_meta(in_folder): """ Detect panoramas (bag files and associated SciCam images). Return @@ -62,7 +81,7 @@ def detect_pano_meta(in_folder): for f in files: if f.endswith(".bag"): bag_path = os.path.join(dirname, f) - bags[bag_path] = pano_image_meta.get_image_meta(bag_path, 1) + bags[bag_path] = pano_image_meta.get_image_meta(bag_path) elif SCI_CAM_IMG_REGEX.search(f): sci_cam_images[f] = dirname @@ -80,6 +99,9 @@ def detect_pano_meta(in_folder): "activity": None, "module": None, "bay": None, + "position": get_scene_position(bag_meta), + "start_time": get_image_timestamp(bag_meta[0]), + "end_time": get_image_timestamp(bag_meta[-1]), "extra_stitch_args": "", } scene_meta["images_dir"] = sci_cam_images.get(bag_meta[0]["img_path"]) diff --git a/pano/pano_stitch/scripts/dot_dict.py b/pano/pano_stitch/scripts/dot_dict.py deleted file mode 100644 index 1c669b57..00000000 --- a/pano/pano_stitch/scripts/dot_dict.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2017, United States Government, as represented by the -# Administrator of the National Aeronautics and Space Administration. -# -# All rights reserved. -# -# The Astrobee platform is licensed under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with the -# License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Convenience wrapper for dict so you can access members with dot notation. -""" - - -class DotDict(dict): - """ - Convenience wrapper for dict so you can access members with dot notation. - """ - - def __getattr__(self, attr): - if attr in self: - return self[attr] - return super(DotDict, self).__getattribute__(attr) diff --git a/pano/pano_view/package.xml b/pano/pano_view/package.xml index da78eeb8..fd26dbe8 100644 --- a/pano/pano_view/package.xml +++ b/pano/pano_view/package.xml @@ -12,21 +12,4 @@ ISAAC Flight Software - - catkin - roscpp - rosbag - ff_msgs - camera - inspection - roscpp - rosbag - ff_msgs - camera - inspection - roscpp - rosbag - ff_msgs - camera - inspection diff --git a/pano/pano_view/scripts/Snakefile b/pano/pano_view/scripts/Snakefile index 5253d49f..ef89ea05 100644 --- a/pano/pano_view/scripts/Snakefile +++ b/pano/pano_view/scripts/Snakefile @@ -43,7 +43,7 @@ rule stitch: scene=lambda wildcards: DotDict(config["scenes"][wildcards.scene_id]) shell: ("[ -f {output} ] ||" - " rosrun pano_stitch stitch_panorama.py" + " rosrun pano_stitch scripts/stitch_panorama.py" " --no-lens" " --robot={params.scene.robot}" " --images-dir={params.scene.images_dir}" @@ -71,19 +71,10 @@ rule tile: " {input}" " -o html/scenes/{wildcards.scene_id}") -rule prep_source_images: - input: - expand("stitch/{scene_id}/pano.png", scene_id=config["scenes"].keys()) - output: - touch("html/source_images/completed.txt") - shell: - "rosrun pano_view prep_source_images.py" - rule tour: input: - expand("html/scenes/{scene_id}/config.json", scene_id=config["scenes"].keys()), - "html/source_images/completed.txt" + expand("html/scenes/{scene_id}/config.json", scene_id=config["scenes"].keys()) output: "html/tour.json" shell: - "rosrun pano_view generate_tour.py" + "rosrun pano_view scripts/generate_tour.py" diff --git a/pano/pano_view/scripts/generate_tour.py b/pano/pano_view/scripts/generate_tour.py index a0d7b836..612f0aaf 100755 --- a/pano/pano_view/scripts/generate_tour.py +++ b/pano/pano_view/scripts/generate_tour.py @@ -23,22 +23,10 @@ import argparse import json import os -import re -import numpy as np -import scipy.sparse.csgraph -import scipy.spatial.distance import yaml PANO_VIEW_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -PACKAGES = [ - "pannellum", - "openseadragon", - "annotorious-openseadragon", - "annotorious-selectorpack", - "annotorious-toolbar", -] -DEFAULT_PACKAGE_PATHS = {pkg: "/opt/" + pkg for pkg in PACKAGES} TOUR_DEFAULT_INIT = { "sceneFadeDuration": 1000, @@ -56,27 +44,6 @@ "author": "NASA ISAAC Project", } -SCENE_LINK_HOT_SPOT_TEXT = "{module} {bay}" - -# Pick a default starting yaw for each module. Pointing along the centerline -# to start feels more natural. -CENTERLINE_YAW = { - "jem": 90, - "nod2": 180, - "col": 90, - "usl": 180, - "nod1": 180, -} - -MULTI_SPACE_REGEX = re.compile(r" +") - -DEGREES_PER_RADIAN = 180 / np.pi - -# This is very roughly calibrated. Note: Overview map may not be exactly to scale! -OVERVIEW_PX_PER_METER = 8.63 -OVERVIEW_X0_PX = 118 + OVERVIEW_PX_PER_METER * 1.64 -OVERVIEW_Y0_PX = 118 - def dosys(cmd, exit_on_error=True): print("+ " + cmd) @@ -92,21 +59,10 @@ def dosys(cmd, exit_on_error=True): def install_glob(src_glob, tgt_folder): if not os.path.isdir(tgt_folder): dosys("mkdir -p %s" % tgt_folder) - dosys("cp -r %s %s" % (src_glob, tgt_folder)) - - -def install_file(src_path, tgt_folder, tgt_name=None): - if not os.path.isdir(tgt_folder): - dosys("mkdir -p %s" % tgt_folder) - if tgt_name is None: - tgt_path = tgt_folder - else: - tgt_path = os.path.join(tgt_folder, tgt_name) - dosys("cp -r %s %s" % (src_path, tgt_path)) + dosys("cp %s %s" % (src_glob, tgt_folder)) -def install_static_files(out_folder, package_paths): - pannellum_path = package_paths["pannellum"] +def install_static_files(out_folder, pannellum_path): install_glob( os.path.join(pannellum_path, "build/pannellum.js"), os.path.join(out_folder, "js"), @@ -123,47 +79,6 @@ def install_static_files(out_folder, package_paths): os.path.join(pannellum_path, "src/standalone/standalone.css"), os.path.join(out_folder, "css"), ) - - openseadragon_path = package_paths["openseadragon"] - install_glob( - os.path.join(openseadragon_path, "openseadragon.min.js*"), - os.path.join(out_folder, "js"), - ) - install_glob( - os.path.join(openseadragon_path, "images/*"), - os.path.join(out_folder, "media/openseadragon"), - ) - - anno_path = package_paths["annotorious-openseadragon"] - install_glob( - os.path.join(anno_path, "*.js"), - os.path.join(out_folder, "js"), - ) - install_glob( - os.path.join(anno_path, "*.js.map"), - os.path.join(out_folder, "js"), - ) - install_glob( - os.path.join(anno_path, "*.css"), - os.path.join(out_folder, "css"), - ) - - sel_path = package_paths["annotorious-selectorpack"] - install_glob( - os.path.join(sel_path, "*.js"), - os.path.join(out_folder, "js"), - ) - install_glob( - os.path.join(sel_path, "*.js.map"), - os.path.join(out_folder, "js"), - ) - - toolbar_path = package_paths["annotorious-toolbar"] - install_glob( - os.path.join(toolbar_path, "annotorious-toolbar.min.js"), - os.path.join(out_folder, "js"), - ) - install_glob( os.path.join(PANO_VIEW_ROOT, "static/js/*"), os.path.join(out_folder, "js") ) @@ -179,11 +94,6 @@ def install_static_files(out_folder, package_paths): # with templates to provide greater flexibility, which would require # template rendering. install_glob(os.path.join(PANO_VIEW_ROOT, "templates/pannellum.htm"), out_folder) - install_file( - os.path.join(PANO_VIEW_ROOT, "templates/isaac_source_image.html"), - os.path.join(out_folder, "src"), - "index.html", - ) def get_display_scene_meta(scene_id, config_scene_meta): @@ -210,151 +120,6 @@ def get_display_scene_meta(scene_id, config_scene_meta): return scene_meta -def fill_field(tmpl, display_scene_meta): - # Replace template {patterns} with variable values from display_scene_meta - val = tmpl.format(**display_scene_meta) - # Collapse multiple spaces to single space and strip leading and - # trailing whitespace. (Templates have space-separated fields and - # some fields may be empty... removing extra spaces looks better.) - return MULTI_SPACE_REGEX.sub(" ", val).strip() - - -def get_angles0(p_from, p_to): - """ - Return yaw and pitch angles that will point a camera at p_from to - a target at p_to. Both arguments are 3D points. - """ - d = p_to - p_from - return { - "yaw": np.arctan2(d[1], d[0]) * DEGREES_PER_RADIAN, - "pitch": np.arctan2(d[2], np.sqrt(d[0] ** 2 + d[1] ** 2)) * DEGREES_PER_RADIAN, - } - - -def get_angles(p_from, p_to, module_from, force_centerline=False): - """ - Return yaw and pitch angles that will point a camera at p_from to - a target at p_to. Both arguments are 3D points. If - force_centerline is True, a module is defined for p_from, and a - centerline yaw is defined for that module, force the hot spot - angles to exactly align with the module centerline in whichever - direction is nearer to the target yaw. - """ - angles = get_angles0(p_from, p_to) - module_yaw = CENTERLINE_YAW.get(module_from) - - if module_yaw is None or not force_centerline: - return angles - - diff = abs(angles["yaw"] - module_yaw) - if diff < 180: - yaw = module_yaw - else: - yaw = (module_yaw + 180) % 360 - return { - "yaw": yaw, - "pitch": 0, - } - - -def get_overview_map_position(scene_meta): - p = scene_meta["position"] - x = p["x"] - y = p["y"] - return ( - OVERVIEW_Y0_PX + y * OVERVIEW_PX_PER_METER, - OVERVIEW_X0_PX - x * OVERVIEW_PX_PER_METER, - ) - - -def link_scenes(config, tour_scenes): - # Collect positions of scenes - n = len(config["scenes"]) - pos = np.zeros((n, 3)) - for i, config_scene_meta in enumerate(config["scenes"].values()): - p = config_scene_meta["position"] - pos[i, :] = (p["x"], p["y"], p["z"]) - - # Calculate Euclidean distance cost matrix M between scenes. - # M_ij is the Euclidean distance between scene i and scene j. - cost_matrix = scipy.spatial.distance.cdist(pos, pos, "euclidean") - - # Calculate a minimum spanning tree that spans all scenes. Each - # edge in the MST between two scenes will turn into a two-way - # hot-spot link between the scenes. Because the MST spans all - # scenes, you should be able to use the links to reach any scene - # from any other scene. Because we tend to capture panoramas on - # ISS module centerlines and the centerlines form a tree, with - # luck using the MST as a heuristic will give us a topology that - # matches the ISS module topology and will seem natural to users. - tree = scipy.sparse.csgraph.minimum_spanning_tree(cost_matrix) - - scene_id_lookup = list(config["scenes"].keys()) - - for j1, j2 in np.transpose(np.nonzero(tree)): - for j_from, j_to in ((j1, j2), (j2, j1)): - scene_id_from = scene_id_lookup[j_from] - config_scene_meta_from = config["scenes"][scene_id_from] - tour_scene_from = tour_scenes[scene_id_from] - - scene_id_to = scene_id_lookup[j_to] - config_scene_meta_to = config["scenes"][scene_id_to] - scene_meta_to = get_display_scene_meta(scene_id_to, config_scene_meta_to) - tour_scene_to = tour_scenes[scene_id_to] - - angles = get_angles( - pos[j_from, :], pos[j_to, :], config_scene_meta_from.get("module") - ) - hot_spot = { - "type": "scene", - "id": scene_id_to, - "sceneId": scene_id_to, - "text": fill_field(SCENE_LINK_HOT_SPOT_TEXT, scene_meta_to), - "yaw": angles["yaw"] - tour_scene_from.get("northOffset", 0), - "pitch": angles["pitch"], - "targetYaw": angles["yaw"] - tour_scene_to.get("northOffset", 0), - "targetPitch": angles["pitch"], - } - - hot_spots = tour_scene_from.setdefault("hotSpots", []) - hot_spots.append(hot_spot) - - -def link_source_images(config, tour_scenes, out_folder): - for scene_id, config_scene_meta in config["scenes"].items(): - tour_scene = tour_scenes[scene_id] - hot_spots = tour_scene.setdefault("hotSpots", []) - - src_images_meta_path = os.path.join( - out_folder, "source_images", scene_id, "meta.json" - ) - - if not os.path.exists(src_images_meta_path): - print("warning: no source images prepped for scene %s" % scene_id) - continue - - with open(src_images_meta_path, "r") as src_images_meta_stream: - src_images_meta = json.load(src_images_meta_stream) - - img_ids = sorted(src_images_meta.keys()) - for i, img_id in enumerate(img_ids): - img_meta = src_images_meta[img_id] - hot_spots.append( - { - "type": "info", - "id": i, - "text": "Image %d" % i, - "yaw": img_meta["yaw"] - tour_scene.get("northOffset", 0), - "pitch": img_meta["pitch"], - "URL": "src/#scene=%s&imageId=%s" % (scene_id, img_id), - "cssClass": "isaac-source-image pnlm-hotspot pnlm-sprite", - "attributes": { - "target": "_blank", - }, - } - ) - - def generate_tour_json(config, out_folder): tour_default = TOUR_DEFAULT_INIT.copy() tour_default["firstScene"] = next(iter(config["scenes"].keys())) @@ -364,15 +129,6 @@ def generate_tour_json(config, out_folder): tour["scenes"] = tour_scenes for scene_id, config_scene_meta in config["scenes"].items(): - # Read tiler scene metadata - tiler_meta_path = os.path.join(out_folder, "scenes", scene_id, "config.json") - config_scene_meta["tiled"] = os.path.isfile(tiler_meta_path) - if not config_scene_meta["tiled"]: - print("warning: skipping %s (%s not found)" % (scene_id, tiler_meta_path)) - continue - with open(tiler_meta_path, "r") as tiler_meta_stream: - tiler_meta = json.load(tiler_meta_stream) - scene_meta = get_display_scene_meta(scene_id, config_scene_meta) tour_scene = TOUR_SCENE_INIT.copy() @@ -381,9 +137,16 @@ def generate_tour_json(config, out_folder): tour_scene_updates = {} for field, val in tour_scene.items(): if isinstance(val, str) and "{" in val: - tour_scene_updates[field] = fill_field(val, scene_meta) + val = val.format(**scene_meta) + val = val.replace(" ", " ") + tour_scene_updates[field] = val tour_scene.update(tour_scene_updates) + # Read more scene metadata from the tiler + tiler_meta_path = os.path.join(out_folder, "scenes", scene_id, "config.json") + with open(tiler_meta_path, "r") as tiler_meta_stream: + tiler_meta = json.load(tiler_meta_stream) + # Copy tiler metadata into scene. Paths output by the tiler # start with "/" but are actually relative to the tiler output # dir, so we need to strip the leading slash and prepend the @@ -397,22 +160,9 @@ def generate_tour_json(config, out_folder): ) tour_scene["multiRes"] = multi_res_meta - tour_scene["yaw"] = CENTERLINE_YAW.get(config_scene_meta["module"], 0) - tour_scene["overviewMapPosition"] = get_overview_map_position(scene_meta) - - extra_tour_params = config_scene_meta.get("extra_tour_params", {}) - tour_scene.update(extra_tour_params) - - # We need to adjust the yaw for northOffset if it is used. - north_offset = tour_scene.get("northOffset", 0) - tour_scene["yaw"] -= north_offset - # Add scene to the tour. tour_scenes[scene_id] = tour_scene - link_scenes(config, tour_scenes) - link_source_images(config, tour_scenes, out_folder) - out_path = os.path.join(out_folder, "tour.json") with open(out_path, "w") as out: json.dump(tour, out, indent=4) @@ -423,18 +173,10 @@ def generate_scene_index(config, out_folder): # Can improve this to be grouped by modules and bays sorted in increasing order. index = ["
    "] for scene_id, config_scene_meta in config["scenes"].items(): - if not config_scene_meta["tiled"]: - continue - scene_meta = get_display_scene_meta(scene_id, config_scene_meta) index.append( - fill_field( - ( - '
  • ' - "{module} {bay}" - "
  • " - ), - scene_meta, + '
  • {module} {bay}
  • '.format( + **scene_meta ) ) index.append("
") @@ -452,11 +194,11 @@ def generate_scene_index(config, out_folder): print("wrote to %s" % out_path) -def generate_tour(config_path, out_folder, package_paths): +def generate_tour(config_path, out_folder, pannellum_path): with open(config_path, "r") as config_stream: config = yaml.safe_load(config_stream) - install_static_files(out_folder, package_paths) + install_static_files(out_folder, pannellum_path) generate_tour_json(config, out_folder) generate_scene_index(config, out_folder) dosys("chmod a+rX %s" % out_folder) @@ -489,20 +231,16 @@ def main(): required=False, ) parser.add_argument( - "--package-paths", + "--pannellum", type=str, - help="comma-separated list of package paths to override formatted like 'openseadragon=/opt/openseadragon'", - default=None, + help="path where pannellum source is checked out (will install files from here)", + default="/opt/pannellum", required=False, ) args = parser.parse_args() - package_paths = DEFAULT_PACKAGE_PATHS.copy() - if args.package_paths is not None: - updates = [assn.split("=", 1) for assn in args.package_paths.split(",")] - package_paths.update(dict(updates)) - generate_tour(args.config, args.out_folder, package_paths) + generate_tour(args.config, args.out_folder, args.pannellum) if __name__ == "__main__": diff --git a/pano/pano_view/static/css/isaac_pano.css b/pano/pano_view/static/css/isaac_pano.css index dfa5a280..acba1b8c 100644 --- a/pano/pano_view/static/css/isaac_pano.css +++ b/pano/pano_view/static/css/isaac_pano.css @@ -17,15 +17,7 @@ * under the License. */ -/* Default to sans-serif where no font is specified. */ - -body { - font-family: sans-serif; -} - -/********************************************************************** - * Overview map - **********************************************************************/ +/* Implement overview map styling for ISAAC panoramic tour. */ .pnlm-overview-map { position: absolute; @@ -70,79 +62,3 @@ body { transform: translate(-50%, -50%); z-index: 5; } - -/********************************************************************** - * Source image custom hot spot and tune Pannellum hot spot style - **********************************************************************/ - -.isaac-source-image { - background-image: url('../media/camera.png') !important; - background-position: 0 0px !important; -} - -/* make this wider so scene link tooltips don't have to wrap */ -div.pnlm-tooltip span { - max-width: 300px; -} - -/********************************************************************** - * Nav bar and view options dropdown menu - **********************************************************************/ - -.isaac-navbar { - padding: 5px; -} - -.isaac-drop-button { - background-color: #ddd; - color: #666; - padding: 6px; - font-size: 12px; - cursor: pointer; - border: 1px solid #666; -} - -.isaac-drop-button:hover, .isaac-drop-button:focus { - background-color: #ddd; - color: black; -} - -.isaac-dropdown { - position: relative; - display: inline-block; -} - -.isaac-dropdown-content { - display: none; - position: absolute; - background-color: #fff; - min-width: 200px; - overflow: auto; - border: 1px solid black; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1; -} - -.isaac-dropdown-content .isaac-toggle-entry { - font-size: 12px; - color: black; - padding: 3px 4px; - text-decoration: none; - display: block; -} - -.isaac-dropdown .isaac-toggle-entry:hover { - background-color: #ddd; -} - -.isaac-dropdown .isaac-toggle-entry .checkbox:before { - content: "\2610"; -} - -.isaac-dropdown .isaac-toggle-entry.checked .checkbox:before { - content: "\2611"; -} - -.isaac-dropdown .show { - display: block; -} diff --git a/pano/pano_view/static/js/isaac_pano.js b/pano/pano_view/static/js/isaac_pano.js index bca475a8..7bcb6990 100644 --- a/pano/pano_view/static/js/isaac_pano.js +++ b/pano/pano_view/static/js/isaac_pano.js @@ -17,9 +17,7 @@ * under the License. */ -/********************************************************************** - * Overview map for panoramic tour - **********************************************************************/ +/* Implement overview map logic for ISAAC panoramic tour. */ function getDist(pos0, pos1) { return Math.sqrt((pos0[0] - pos1[0]) ** 2 + (pos0[1] - pos1[1]) ** 2); @@ -132,111 +130,3 @@ function initIsaacPano(event) { } document.addEventListener('pannellumloaded', initIsaacPano, false); - -/********************************************************************** - * View options dropdown menu - **********************************************************************/ - -function isaacSleep(secs) { - return new Promise(resolve => setTimeout(resolve, secs * 1000)); -} - -/* When the user clicks on the button, toggle between hiding and showing the dropdown content */ -function isaacToggleDropDown() { - document.getElementById("isaac-view-dropdown").classList.toggle("show"); -} - -function isaacSetVisibility(className, visibility) { - var elts = document.getElementsByClassName(className) - var newDisplayStyle = visibility ? 'block' : 'none'; - for (const elt of elts) { - elt.style.display = newDisplayStyle; - } -} - -function isaacShowNavControls(visibility) { - isaacSetVisibility('pnlm-controls-container', visibility); -} - -function isaacShowOverviewMap(visibility) { - isaacSetVisibility('pnlm-overview-map', visibility); -} - -function isaacShowHotSpotType(hotSpotType, visibility) { - var config = window.viewer.getConfig(); - var currentSceneId = window.viewer.getScene(); - - // Update hotSpots for all scenes. That way the visibility change - // will persist when the scene changes. - for (let [sceneId, scene] of Object.entries(config.scenes)) { - // back up original complete hotSpots array if needed - if (!scene.hasOwnProperty('initialHotSpots')) { - scene.initialHotSpots = [...scene.hotSpots]; - } - - if (visibility) { - // add hotSpots matching type - for (const hotSpot of scene.initialHotSpots) { - if (hotSpot.type == hotSpotType) { - // console.log("window.viewer.addHotSpot(" + hotSpot.id + "," + sceneId + ");"); - window.viewer.addHotSpot(hotSpot, sceneId); - } - } - } else { - // remove hotSpots matching type - var hotSpotsCopy = [...scene.hotSpots]; - for (const hotSpot of hotSpotsCopy) { - if (hotSpot.type == hotSpotType) { - // console.log("window.viewer.removeHotSpot(" + hotSpot.id + "," + sceneId + ");"); - window.viewer.removeHotSpot(hotSpot.id, sceneId); - } - } - } - } -} - -function isaacShowSceneLinks(visibility) { - isaacShowHotSpotType("scene", visibility); -} - -function isaacShowSourceImageLinks(visibility) { - isaacShowHotSpotType("info", visibility); -} - -const ISAAC_CHANGE_VISIBILITY_HANDLERS = { - "isaac-show-nav-controls": isaacShowNavControls, - "isaac-show-overview-map": isaacShowOverviewMap, - "isaac-show-scene-links": isaacShowSceneLinks, - "isaac-show-source-image-links": isaacShowSourceImageLinks, -}; - -function isaacToggleEntryCheckBox(event) { - var elt = event.srcElement; - elt.classList.toggle("checked"); - var visibility = elt.classList.contains("checked"); - ISAAC_CHANGE_VISIBILITY_HANDLERS[elt.id](visibility); -} - -function isaacInitViewDropDown() { - document.getElementsByClassName("isaac-drop-button")[0].onclick = isaacToggleDropDown; - - var toggleEntries = document.getElementsByClassName("isaac-toggle-entry"); - for (const entry of toggleEntries) { - entry.onclick = isaacToggleEntryCheckBox; - } - - // Close the dropdown if the user clicks outside of it - window.onclick = async function(event) { - if (!event.target.matches('.isaac-drop-button')) { - var dropdowns = document.getElementsByClassName("isaac-dropdown-content"); - for (const dropdown of dropdowns) { - if (dropdown.classList.contains('show')) { - await isaacSleep(0.4); - dropdown.classList.remove('show'); - } - } - } - } -} - -isaacInitViewDropDown(); diff --git a/pano/pano_view/templates/index.html b/pano/pano_view/templates/index.html index f3eb9416..377b8ccd 100644 --- a/pano/pano_view/templates/index.html +++ b/pano/pano_view/templates/index.html @@ -1,12 +1,17 @@ ISAAC Astrobee Panoramas +

ISAAC Astrobee Panoramas

-

[Start exploring here]

+

[Start exploring here]

Or jump straight to a specific bay:

diff --git a/pano/pano_view/templates/pannellum.htm b/pano/pano_view/templates/pannellum.htm index 45ce62c9..d2585784 100644 --- a/pano/pano_view/templates/pannellum.htm +++ b/pano/pano_view/templates/pannellum.htm @@ -9,17 +9,6 @@ -
-
- -
-
Navigation controls
-
Overview map
-
Scene links
-
Source image links
-
-
-