diff --git a/scripts/nerfcapture2nerf.py b/scripts/nerfcapture2nerf.py
new file mode 100644
index 0000000000000000000000000000000000000000..55ddf85813c670c6c834252583c1b6e60fa39ac1
--- /dev/null
+++ b/scripts/nerfcapture2nerf.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+# Streaming/Dataset capture script for the NeRFCapture iOS App
+
+import argparse
+import cv2
+from pathlib import Path
+import json
+import time
+import shutil
+from PIL import Image
+
+import cyclonedds.idl as idl
+import cyclonedds.idl.annotations as annotate
+import cyclonedds.idl.types as types
+from dataclasses import dataclass
+from cyclonedds.domain import DomainParticipant, Domain
+from cyclonedds.core import Qos, Policy
+from cyclonedds.sub import DataReader
+from cyclonedds.topic import Topic
+from cyclonedds.util import duration
+
+from common import *
+import pyngp as ngp  # noqa
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--stream", action="store_true", help="Stream images directly to InstantNGP.")
+    parser.add_argument("--n_frames", default=10, type=int, help="Number of frames before saving the dataset. Also used as the number of cameras to remember when streaming.")
+    parser.add_argument("--save_path", required='--stream' not in sys.argv, type=str, help="Path to save the dataset.")
+    parser.add_argument("--depth_scale", default=4.0, type=float, help="Depth scale used when saving depth. Only used when saving dataset.")
+    parser.add_argument("--overwrite", action="store_true", help="Rewrite over dataset if it exists.")
+    return parser.parse_args()
+
+
+# DDS
+# ==================================================================================================
+@dataclass
+@annotate.final
+@annotate.autoid("sequential")
+class NeRFCaptureFrame(idl.IdlStruct, typename="NeRFCaptureData.NeRFCaptureFrame"):
+    id: types.uint32
+    annotate.key("id")
+    timestamp: types.float64
+    fl_x: types.float32
+    fl_y: types.float32
+    cx: types.float32
+    cy: types.float32
+    transform_matrix: types.array[types.float32, 16]
+    width: types.uint32
+    height: types.uint32
+    image: types.sequence[types.uint8]
+    has_depth: bool
+    depth_width: types.uint32
+    depth_height: types.uint32
+    depth_scale: types.float32
+    depth_image: types.sequence[types.uint8]
+
+
+dds_config = """<?xml version="1.0" encoding="UTF-8" ?> \
+<CycloneDDS xmlns="https://cdds.io/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://cdds.io/config https://raw.githubusercontent.com/eclipse-cyclonedds/cyclonedds/master/etc/cyclonedds.xsd"> \
+    <Domain id="any"> \
+        <Internal> \
+            <MinimumSocketReceiveBufferSize>10MB</MinimumSocketReceiveBufferSize> \
+        </Internal> \
+        <Tracing> \
+            <Verbosity>config</Verbosity> \
+            <OutputFile>stdout</OutputFile> \
+        </Tracing> \
+    </Domain> \
+</CycloneDDS> \
+"""
+# ==================================================================================================
+
+def set_frame(testbed, frame_idx: int, rgb: np.ndarray, depth: np.ndarray, depth_scale: float, X_WV: np.ndarray, fx: float, fy: float, cx: float, cy: float): 
+    testbed.nerf.training.set_image(frame_idx = frame_idx, img=rgb, depth_img=depth, depth_scale=depth_scale*testbed.nerf.training.dataset.scale)
+    testbed.nerf.training.set_camera_extrinsics(frame_idx=frame_idx, camera_to_world=X_WV)
+    testbed.nerf.training.set_camera_intrinsics(frame_idx=frame_idx, fx=fx, fy=fy, cx=cx, cy=cy)
+
+
+def live_streaming_loop(reader: DataReader, max_cameras: int):
+    # Start InstantNGP
+    testbed = ngp.Testbed(ngp.TestbedMode.Nerf)
+    testbed.init_window(640, 480)
+    testbed.reload_network_from_file(f"configs/nerf/base.json")
+    testbed.visualize_unit_cube = True
+    testbed.nerf.visualize_cameras = True
+
+    camera_index = 0  # Current camera index we are replacing in InstantNGP 
+    total_frames = 0 # Total frames received
+
+    # Create Empty Dataset
+    testbed.create_empty_nerf_dataset(max_cameras, aabb_scale=1)  
+    testbed.nerf.training.n_images_for_training = 0
+    testbed.up_dir = np.array([1.0, 0.0, 0.0])
+
+    # Start InstantNGP and DDS Loop
+    while testbed.frame():
+        sample = reader.read_next() # Get frame from NeRFCapture
+        if sample:
+            print(f"Frame received")
+
+            # RGB
+            image = np.asarray(sample.image, dtype=np.uint8).reshape(
+                (sample.height, sample.width, 3)).astype(np.float32)/255.0
+            image = np.concatenate(
+                [image, np.zeros((sample.height, sample.width, 1), dtype=np.float32)], axis=-1)
+
+            # Depth if avaiable
+            depth = None
+            if sample.has_depth:
+                depth = np.asarray(sample.depth_image, dtype=np.uint8).view(
+                    dtype=np.float32).reshape((sample.depth_height, sample.depth_width))
+                depth = cv2.resize(depth, dsize=(
+                    sample.width, sample.height), interpolation=cv2.INTER_NEAREST)
+
+
+            # Transform
+            X_WV = np.asarray(sample.transform_matrix,
+                            dtype=np.float32).reshape((4, 4)).T[:3, :]
+
+            # Add frame to InstantNGP
+            set_frame(testbed,
+                    frame_idx=camera_index,
+                    rgb=srgb_to_linear(image),
+                    depth=depth,
+                    depth_scale=1,
+                    X_WV=X_WV,
+                    fx=sample.fl_x,
+                    fy=sample.fl_y,
+                    cx=sample.cx,
+                    cy=sample.cy)
+
+            # Update index
+            total_frames += 1
+            testbed.nerf.training.n_images_for_training = min(total_frames, max_cameras) 
+            camera_index = (camera_index + 1) % max_cameras
+
+            if total_frames == 1:
+                testbed.first_training_view()
+                testbed.render_groundtruth = True
+
+def dataset_capture_loop(reader: DataReader, save_path: Path, overwrite: bool, n_frames: int):
+
+    if save_path.exists():
+        if overwrite:
+            # Prompt user to confirm deletion
+            res = input("Warning, directory exists already. Press Y to delete anyway: ")
+            if res == 'Y':
+                shutil.rmtree(save_path)
+            else:
+                exit()
+        else:
+            print("save_path already exists")
+            exit()
+    
+    # Make directory
+    images_dir = save_path.joinpath("images")
+
+    manifest = {
+        "fl_x":  0.0,
+        "fl_y":  0.0,
+        "cx": 0.0, 
+        "cy": 0.0,
+        "w": 0.0,
+        "h": 0.0,
+        "frames": []
+    }
+
+    total_frames = 0 # Total frames received
+
+    # Start DDS Loop
+    while True:
+        time.sleep(0.001)
+        sample = reader.read_next() # Get frame from NeRFCapture
+        if sample:
+            print(f"Frame received")
+
+            if total_frames == 0:
+                save_path.mkdir(parents=True)
+                images_dir.mkdir()
+                manifest["w"] = sample.width
+                manifest["h"] = sample.height
+                manifest["cx"] = sample.cx
+                manifest["cy"] = sample.cy
+                manifest["fl_x"] = sample.fl_x
+                manifest["fl_y"] = sample.fl_y
+                manifest["integer_depth_scale"] = float(args.depth_scale)/65535.0
+
+            # RGB
+            image = np.asarray(sample.image, dtype=np.uint8).reshape(
+                (sample.height, sample.width, 3))
+            image = np.concatenate(
+                [image, 255*np.ones((sample.height, sample.width, 1), dtype=np.uint8)], axis=-1)
+            Image.fromarray(image).save(images_dir.joinpath(f"{total_frames}.png"))
+
+            # Depth if avaiable
+            depth = None
+            if sample.has_depth:
+                depth = np.asarray(sample.depth_image, dtype=np.uint8).view(
+                    dtype=np.float32).reshape((sample.depth_height, sample.depth_width))
+                depth = (depth*65535/float(args.depth_scale)).astype(np.uint16)
+                depth = cv2.resize(depth, dsize=(
+                    sample.width, sample.height), interpolation=cv2.INTER_NEAREST)
+                Image.fromarray(depth).save(images_dir.joinpath(f"{total_frames}.depth.png"))
+
+
+            # Transform
+            X_WV = np.asarray(sample.transform_matrix,
+                              dtype=np.float32).reshape((4, 4)).T
+
+            
+            frame = {
+                "transform_matrix": X_WV.tolist(),
+                "file_path": f"images/{total_frames}",
+                "fl_x": sample.fl_x,
+                "fl_y": sample.fl_y,
+                "cx": sample.cx,
+                "cy": sample.cy,
+                "w": sample.width,
+                "h": sample.height
+            }
+
+            if depth is not None:
+                frame["depth_path"] = f"images/{total_frames}.depth.png"
+
+            manifest["frames"].append(frame)
+
+            # Update index
+            total_frames += 1
+            if total_frames == n_frames:
+                # Write manifest as json
+                manifest_json = json.dumps(manifest, indent=4)
+                with open(save_path.joinpath("transforms.json"), "w") as f:
+                    f.write(manifest_json)
+                print("Done")
+                exit()
+
+
+if __name__ == "__main__":
+    args = parse_args()
+
+    # Setup DDS
+    domain = Domain(domain_id=0, config=dds_config)
+    participant = DomainParticipant()
+    qos = Qos(Policy.Reliability.Reliable(
+        max_blocking_time=duration(seconds=1)))
+    topic = Topic(participant, "Frames", NeRFCaptureFrame, qos=qos)
+    reader = DataReader(participant, topic)
+
+    if args.stream:
+        live_streaming_loop(reader, args.n_frames)
+    else:
+        dataset_capture_loop(reader, Path(args.save_path), args.overwrite, args.n_frames)
+