Skip to content

Commit ab04da5

Browse files
committed
[improvement] project structure optimization
1 parent 8a65361 commit ab04da5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+20261
-0
lines changed

viewers/nerf_viewer/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Nerf Viewer Quick Start
2+
3+
## 1 Client
4+
5+
### 1.1 Install Dependencies
6+
7+
#### 1.1.1 Install NVM
8+
9+
Please follow the instructions in:
10+
11+
https://juejin.cn/post/7120267871950733320/
12+
13+
#### 1.1.2 Install Node
14+
15+
Once you have nvm correctly configured, install node 18.15.0 using the commands below:
16+
17+
```shell
18+
# install node 18.15.0
19+
nvm install 18.15.0
20+
21+
# activate node 18.15.0
22+
nvm use 18.15.0
23+
```
24+
25+
#### 1.1.3 Install Node Packages
26+
27+
28+
```shell
29+
# make sure that your current working directory is 'client'
30+
cd ./client
31+
32+
# install node packages using configuration in client/package.json
33+
npm install
34+
```
35+
36+
### 1.2 Start Viewer
37+
38+
Build and run the viewer using:
39+
40+
```shell
41+
# make sure that your current working directory is 'client'
42+
cd ./client
43+
44+
# start the viewer
45+
npm start
46+
```
47+
48+
Then, check whether viewer is successfully compiled:
49+
50+
![alt viewer_compile_success](./doc/viewer_compile_success.png)
51+
52+
Visit http://localhost:3000/ in the browser:
53+
54+
![alt viewer](./doc/viewer.png)
55+
56+
## 2 Bridge Server
57+
58+
### 2.1 Install Dependencies
59+
60+
```shell
61+
62+
cd ./bridge_server
63+
64+
# create a virtual environment
65+
conda create -n BridgeServer python=3.7
66+
67+
conda activate BridgeServer
68+
69+
# install dependencies
70+
pip install -r ./requirements.txt
71+
```
72+
73+
#### 2.2 Start Server
74+
75+
```
76+
python ./run_viewer.py
77+
```
78+
79+
Check whether server is successfully deployed:
80+
81+
![alt run_viewer](./doc/run_viewer.png)
82+
83+
Open the browser and visit http://localhost:3000/. Then, check whether the server is connected with the viewer:
84+
85+
![alt viewer_connected](./doc/viewer_connected.png)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Actions are a series of keys that determine how data flows
3+
"""
4+
5+
"""for viewer"""
6+
7+
UPDATE_CAMERA_TRANSLATION = "UPDATE_CAMERA_TRANSLATION"
8+
9+
UPDATE_CAMERA_ROTATION = "UPDATE_CAMERA_ROTATION"
10+
11+
UPDATE_CAMERA_FOV = "UPDATE_CAMERA_FOV"
12+
13+
UPDATE_RESOLUTION = "UPDATE_RESOLUTION"
14+
15+
UPDATE_RENDER_TYPE = "UPDATE_RENDER_TYPE"
16+
17+
18+
"""for nerf backend"""
19+
20+
# update the scene state
21+
UPDATE_STATE = "UPDATE_STATE"
22+
23+
UPDATE_RENDER_RESULT = "UPDATE_RENDER_RESULT"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import atexit
2+
import os
3+
import signal
4+
import socket
5+
import subprocess
6+
import sys
7+
import threading
8+
import time
9+
10+
import server
11+
12+
from typing import Optional
13+
14+
15+
def is_port_open(port: int):
16+
# check whether the given port is open
17+
try:
18+
sock = socket.socket()
19+
sock.bind(("", port))
20+
sock.close()
21+
22+
return True
23+
except OSError:
24+
return False
25+
26+
27+
def get_free_port(default_port: int = None):
28+
if default_port:
29+
if is_port_open(default_port):
30+
return default_port
31+
sock = socket.socket()
32+
sock.bind(("", 0))
33+
port = sock.getsockname()[1]
34+
35+
return port
36+
37+
38+
def run_bridge_server_as_subprocess(
39+
websocket_port: int,
40+
zmq_port: Optional[int] = None,
41+
ip_address: str = "127.0.0.1",
42+
):
43+
# run bridge server as a sub-process
44+
args = [sys.executable, "-u", "-m", server.__name__]
45+
46+
# find an available port for zmq
47+
if zmq_port is None:
48+
zmq_port = get_free_port()
49+
print(f"Using ZMQ port: {zmq_port}")
50+
51+
args.append("--zmq-port")
52+
args.append(str(zmq_port))
53+
args.append("--websocket-port")
54+
args.append(str(websocket_port))
55+
args.append("--ip-address")
56+
args.append(str(ip_address))
57+
58+
process = subprocess.Popen(args, start_new_session=True)
59+
60+
def cleanup(process):
61+
process.kill()
62+
process.wait()
63+
64+
def poll_process():
65+
"""
66+
Continually check to see if the viewer bridge server process is still running and has not failed.
67+
If it fails, alert the user and exit the entire program.
68+
"""
69+
while process.poll() is None:
70+
time.sleep(0.5)
71+
72+
message = "The bridge server subprocess failed."
73+
cleanup(process)
74+
75+
# windows system do not have signal.SIGKILL
76+
# os.kill(os.getpid(), signal.SIGKILL)
77+
os.kill(os.getpid(), signal.SIGINT)
78+
79+
watcher_thread = threading.Thread(target=poll_process)
80+
watcher_thread.daemon = True
81+
watcher_thread.start()
82+
# clean up process when it has shut down
83+
atexit.register(cleanup, process)
84+
85+
return zmq_port
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pyzmq==25.0.2
2+
pyngrok==5.2.1
3+
tornado
4+
umsgpack==0.1.0
5+
opencv-contrib-python==4.5.3.56
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from viewer_state import ViewerState
2+
3+
4+
def run_viewer():
5+
"""
6+
start the viewer
7+
"""
8+
viewer_state = ViewerState()
9+
while True:
10+
viewer_state.update_scene()
11+
12+
13+
if __name__ == "__main__":
14+
run_viewer()
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import pickle
2+
import sys
3+
from typing import List, Optional, Tuple
4+
5+
# for web networking
6+
import tornado.gen
7+
import tornado.ioloop
8+
import tornado.web
9+
import tornado.websocket
10+
11+
# for data conversion
12+
import umsgpack
13+
14+
# ZeroMQ: for messaging
15+
import zmq
16+
import zmq.eventloop.ioloop
17+
from zmq.eventloop.zmqstream import ZMQStream
18+
19+
from pyngrok import ngrok
20+
21+
from actions import \
22+
UPDATE_CAMERA_FOV, UPDATE_CAMERA_ROTATION, UPDATE_CAMERA_TRANSLATION, \
23+
UPDATE_RENDER_TYPE, UPDATE_RESOLUTION, UPDATE_STATE, UPDATE_RENDER_RESULT
24+
25+
from state import State
26+
27+
28+
class WebSocketHandler(tornado.websocket.WebSocketHandler): # pylint: disable=abstract-method
29+
"""for receiving and sending commands from/to the viewer"""
30+
31+
def __init__(self, *args, **kwargs):
32+
self.bridge = kwargs.pop("bridge")
33+
super().__init__(*args, **kwargs)
34+
35+
def check_origin(self, origin: str) -> bool:
36+
return True
37+
38+
def open(self, *args: str, **kwargs: str):
39+
self.bridge.websocket_pool.add(self)
40+
print("opened:", self, file=sys.stderr)
41+
42+
async def on_message(self, message: bytearray): # pylint: disable=invalid-overridden-method
43+
"""parses the message from viewer and calls the appropriate function"""
44+
data = message
45+
unpacked_message = umsgpack.unpackb(message)
46+
47+
type = unpacked_message["type"]
48+
data = unpacked_message["data"]
49+
# type_ = m["type"]
50+
# path = list(filter(lambda x: len(x) > 0, m["path"].split("/")))
51+
52+
if type == UPDATE_CAMERA_TRANSLATION:
53+
self.bridge.state.camera_translation = data
54+
elif type == UPDATE_CAMERA_ROTATION:
55+
self.bridge.state.camera_rotation = data
56+
elif type == UPDATE_RENDER_TYPE:
57+
self.bridge.state.render_type = data
58+
elif type == UPDATE_CAMERA_FOV:
59+
self.bridge.state.camera_fov = data
60+
elif type == UPDATE_RESOLUTION:
61+
self.bridge.state.resolution = data
62+
else:
63+
# TODO: handle exception
64+
pass
65+
66+
def on_close(self) -> None:
67+
self.bridge.websocket_pool.remove(self)
68+
print("closed: ", self, file=sys.stderr)
69+
70+
71+
class ZMQWebSocketBridge:
72+
73+
context = zmq.Context() # pylint: disable=abstract-class-instantiated
74+
75+
def __init__(self, zmq_port: int, websocket_port: int, ip_address: str):
76+
self.zmq_port = zmq_port
77+
self.websocket_pool = set()
78+
self.app = self.make_app()
79+
self.ioloop = tornado.ioloop.IOLoop.current()
80+
81+
# zmq
82+
zmq_url = f"tcp://{ip_address}:{self.zmq_port:d}"
83+
self.zmq_socket, self.zmq_stream, self.zmq_url = self.setup_zmq(zmq_url)
84+
85+
# websocket
86+
listen_kwargs = {"address" : "0.0.0.0"}
87+
self.app.listen(websocket_port, **listen_kwargs)
88+
self.websocket_port = websocket_port
89+
self.websocket_url = f"0.0.0.0:{self.websocket_port}"
90+
91+
# state
92+
self.state = State()
93+
94+
def __str__(self) -> str:
95+
class_name = self.__class__.__name__
96+
return f'{class_name} using zmq_port="{self.zmq_port}" and websocket_port="{self.websocket_port}"'
97+
98+
def make_app(self):
99+
# create an application for the websocket server
100+
return tornado.web.Application([(r"/", WebSocketHandler, {"bridge": self})])
101+
102+
def handle_zmq(self, frames: List[bytes]):
103+
104+
type_ = frames[0].decode("utf-8")
105+
data = frames[1]
106+
107+
if type_ == UPDATE_RENDER_RESULT:
108+
self.forward_to_websockets(frames)
109+
self.zmq_socket.send(umsgpack.packb(b"ok"))
110+
elif type_ == UPDATE_STATE:
111+
serialized = pickle.dumps(self.state)
112+
self.zmq_socket.send(serialized)
113+
elif type_ == "ping":
114+
self.zmq_socket.send(umsgpack.packb(b"ping received"))
115+
else:
116+
print("type: " + str(type_))
117+
self.zmq_socket.send(umsgpack.packb(b"error: unknown command"))
118+
119+
def forward_to_websockets(
120+
self,
121+
frames: Tuple[str, str, bytes],
122+
websocket_to_skip: Optional[WebSocketHandler] = None
123+
):
124+
"""forward a zmq message to all websockets"""
125+
"""nerf backend -> viewer"""
126+
_type, _data = frames # cmd, data
127+
for websocket in self.websocket_pool:
128+
if websocket_to_skip and websocket == websocket_to_skip:
129+
pass
130+
else:
131+
websocket.write_message(_data, binary=True)
132+
133+
def setup_zmq(self, url: str):
134+
"""setup a zmq socket and connect it to the given url"""
135+
zmq_socket = self.context.socket(zmq.REP) # pylint: disable=no-member
136+
zmq_socket.bind(url)
137+
zmq_stream = ZMQStream(zmq_socket)
138+
zmq_stream.on_recv(self.handle_zmq)
139+
140+
return zmq_socket, zmq_stream, url
141+
142+
def run(self):
143+
"""starts and runs the websocet bridge"""
144+
self.ioloop.start()
145+
146+
147+
def run_bridge_server(
148+
zmq_port: int = 6000,
149+
websocket_port: int = 4567,
150+
ip_address: str = "127.0.0.1",
151+
use_ngrok: bool = False
152+
):
153+
# whether expose the zmq port
154+
if use_ngrok:
155+
http_tunnel = ngrok.connect(addr=str(zmq_port), proto="tcp")
156+
print(http_tunnel)
157+
158+
bridge = ZMQWebSocketBridge(
159+
zmq_port=zmq_port,
160+
websocket_port=websocket_port,
161+
ip_address=ip_address
162+
)
163+
164+
print(bridge)
165+
166+
try:
167+
bridge.run()
168+
except KeyboardInterrupt:
169+
pass
170+
171+
172+
if __name__ == "__main__":
173+
run_bridge_server()

0 commit comments

Comments
 (0)