Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add prefixed panels and custom points cloud sample size #2

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
.vscode/*
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# ROSboard

## Customize
- Custom image encoding
Choose the encoding image dimension within the `compress_image()` and the crispy image within the `encode_jpeg()` function

- Custom point cloud subsample size
Choose the number of sub-samples every scan within the `compress_point_cloud2()` function.

--------

ROS node that runs a web server on your robot.
Run the node, point your web browser at http://your-robot-ip:8888/ and you get nice visualizations.

Expand Down
4 changes: 3 additions & 1 deletion rosboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
__version__ = "1.2.1"
__version__ = "1.2.1"

__img_quality__ = 50
48 changes: 33 additions & 15 deletions rosboard/compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import io
import numpy as np
from rosboard.cv_bridge import imgmsg_to_cv2

import time
try:
import simplejpeg
except ImportError:
Expand All @@ -28,31 +28,33 @@ def decode_jpeg(input_bytes):
return np.asarray(Image.open(io.BytesIO(input_bytes)))

def encode_jpeg(img):
# use img_quality to change image quality between 0 and 100
__img_quality__ = 50
if simplejpeg:
if len(img.shape) == 2:
img = np.expand_dims(img, axis=2)
if not img.flags['C_CONTIGUOUS']:
img = img.copy(order='C')
return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = 50)
return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = img_quality)
elif len(img.shape) == 3:
if not img.flags['C_CONTIGUOUS']:
img = img.copy(order='C')
if img.shape[2] == 1:
return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = 50)
return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = img_quality)
elif img.shape[2] == 4:
return simplejpeg.encode_jpeg(img, colorspace = "RGBA", quality = 50)
return simplejpeg.encode_jpeg(img, colorspace = "RGBA", quality = img_quality)
elif img.shape[2] == 3:
return simplejpeg.encode_jpeg(img, colorspace = "RGB", quality = 50)
return simplejpeg.encode_jpeg(img, colorspace = "RGB", quality = img_quality)
else:
return b''
elif cv2:
if len(img.shape) == 3 and img.shape[2] == 3:
img = img[:,:,::-1]
return cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50])[1].tobytes()
return cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, img_quality])[1].tobytes()
elif PIL:
pil_img = Image.fromarray(img)
buffered = io.BytesIO()
pil_img.save(buffered, format="JPEG", quality = 50)
pil_img.save(buffered, format="JPEG", quality = img_quality)
return buffered.getvalue()

_PCL2_DATATYPES_NUMPY_MAP = {
Expand Down Expand Up @@ -150,9 +152,8 @@ def compress_compressed_image(msg, output):
output["_error"] = "Error: %s" % str(e)
output["_data_jpeg"] = base64.b64encode(img_jpeg).decode()
output["_data_shape"] = list(original_shape)


def compress_image(msg, output):
def compress_image(msg, output, max_height = 800, max_width = 800):
output["data"] = []
output["__comp"] = ["data"]

Expand All @@ -173,8 +174,9 @@ def compress_image(msg, output):
cv2_img = np.stack((cv2_img[:,:,0], cv2_img[:,:,1], np.zeros(cv2_img[:,:,0].shape)), axis = -1)

# enforce <800px max dimension, and do a stride-based resize
if cv2_img.shape[0] > 800 or cv2_img.shape[1] > 800:
stride = int(np.ceil(max(cv2_img.shape[0] / 800.0, cv2_img.shape[1] / 800.0)))
# Edit: choose a smaller dimension for quicker encoding and larger dimension vice versa.
if cv2_img.shape[0] > max_height or cv2_img.shape[1] > max_width:
stride = int(np.ceil(max(cv2_img.shape[0] / max_height*1.0, cv2_img.shape[1] / max_width*1.0)))
cv2_img = cv2_img[::stride,::stride]

# if image format isn't already uint8, make it uint8 for visualization purposes
Expand All @@ -193,8 +195,13 @@ def compress_image(msg, output):
cv2_img = np.clip(cv2_img * 255, 0, 255).astype(np.uint8)

try:
start = time.time()
img_jpeg = encode_jpeg(cv2_img)

end = time.time()
output["_warn"] = "encodejpeg elapsed = " + str(end - start) + "sec"
output["_data_jpeg"] = base64.b64encode(img_jpeg).decode()
output["_warn"] += "\nlength of encoded jpeg string = " + str(len(base64.b64encode(img_jpeg).decode()))
output["_data_shape"] = original_shape
except OSError as e:
output["_error"] = str(e)
Expand All @@ -221,7 +228,10 @@ def compress_occupancy_grid(msg, output):
except Exception as e:
output["_error"] = str(e)
try:
start = time.time()
img_jpeg = encode_jpeg(cv2_img)
end = time.time()
output["_warn"] = (end - start)
output["_data_jpeg"] = base64.b64encode(img_jpeg).decode()
except OSError as e:
output["_error"] = str(e)
Expand All @@ -237,7 +247,8 @@ def compress_occupancy_grid(msg, output):
8: np.float64,
}

def compress_point_cloud2(msg, output):
def compress_point_cloud2(msg, output, sample_size = 65535):
# sample_size (int): custom sample size, default 65535
# assuming fields are ('x', 'y', 'z', ...),
# compression scheme is:
# msg['_data_uint16'] = {
Expand All @@ -249,6 +260,13 @@ def compress_point_cloud2(msg, output):
# 65535 represents the max value in the dataset, and bounds: [...] holds information on those bounds so the
# client can decode back to a float

### EDIT: CUSTOM COMPRESSION
# choose point cloud sample size

if (sample_size < 0):
output["_warn"] = "sample size < 0, set to default 65535"
sample_size = 65535

output["data"] = []
output["__comp"] = ["data"]

Expand All @@ -268,9 +286,9 @@ def compress_point_cloud2(msg, output):
except AssertionError as e:
output["_error"] = "PointCloud2 error: %s" % str(e)

if points.size > 65536:
output["_warn"] = "Point cloud too large, randomly subsampling to 65536 points."
idx = np.random.randint(points.size, size=65536)
if points.size > sample_size:
output["_warn"] = "Point cloud too large, randomly subsampling to {} points.".format(sample_size)
idx = np.random.randint(points.size, size=sample_size)
points = points[idx]

xpoints = points['x'].astype(np.float32)
Expand Down
21 changes: 20 additions & 1 deletion rosboard/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
import types
import uuid

from . import __version__
from . import __version__, __img_quality__

# class PostHandler(tornado.web.RequestHandler):
# def post(self):
# data = json.loads(self.request.body)
# self.write("Set image quality to: ", data)

# def set_img_quality(img_quality):
# __img_quality__ = img_quality

class NoCacheStaticFileHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
Expand All @@ -18,6 +26,8 @@ def set_extra_headers(self, path):
class ROSBoardSocketHandler(tornado.websocket.WebSocketHandler):
sockets = set()
joy_msg = None
highcmd_msg = None
img_quality_msg = 50

def initialize(self, node):
# store the instance of the ROS node that created this WebSocketHandler so we can access it later
Expand Down Expand Up @@ -192,6 +202,13 @@ def on_message(self, message):
# Joy
elif argv[0] == ROSBoardSocketHandler.JOY_MSG:
ROSBoardSocketHandler.joy_msg = argv[1]
# HighCmd
elif argv[0] == ROSBoardSocketHandler.HIGHCMD_MSG:
ROSBoardSocketHandler.highcmd_msg = argv[1]
# image quality
elif argv[0] == ROSBoardSocketHandler.IMG_QUALITY:
# ROSBoardSocketHandler.img_quality_msg = argv[1]
__img_quality__ = argv[1]


ROSBoardSocketHandler.MSG_PING = "p";
Expand All @@ -207,3 +224,5 @@ def on_message(self, message):
ROSBoardSocketHandler.PONG_TIME = "t";

ROSBoardSocketHandler.JOY_MSG = "j";
ROSBoardSocketHandler.HIGHCMD_MSG = "h";
ROSBoardSocketHandler.IMG_QUALITY = "i";
66 changes: 59 additions & 7 deletions rosboard/html/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,47 @@ input, textarea, *[contenteditable=true] {
background:#202020;
}

.float-button {
position: fixed;
bottom: 90px;
right: 50px;
}

/* .grid::after {
content: '';
display: block;
clear: both;
} */

.grid-sizer,
.card {
width: 20%;
}

.card {
border-radius:5pt;
overflow:hidden;
margin-left:20pt;
overflow:scroll;
margin-left:5pt;
margin-top:20pt;
background:#303030;
color:#c0c0c0;
box-shadow:3pt 3pt 3pt rgba(0,0,0,0.2);
width:calc(100% - 40pt);
/* width:100%; */
/* width:calc(100% - 10pt); */
}

.card--width-pc { width:calc(60% - 10pt); }
.card--width-odom { width:calc(40% - 10pt); }
.card--width-img { width:calc(20% - 10pt); }
.card--width-state { width:calc(25% - 10pt); }
.card--width-highcmd { width:200px; }

.card--height-pc { height:500px; }
.card--height-odom { height:200px; }
.card--height-img { height:200px; }
.card--height-state { height: 600px; scroll-behavior: smooth;}
.card--height-highcmd { height: 100px;}

.card-title {
font-family:Titillium Web;
font-size:12pt;
Expand Down Expand Up @@ -153,6 +183,7 @@ input, textarea, *[contenteditable=true] {

.card-buttons {
position:absolute;
/* position:auto; */
display:flex;
top:0;
right:5pt;
Expand All @@ -176,6 +207,27 @@ input, textarea, *[contenteditable=true] {
cursor:pointer;
}

#stand-up-button{
background-color: white;
color: black;
border: 2px solid #4CAF50;
}

#stand-up-button:hover{
background-color: #4CAF50;
color: white;
}

#sit-down-button{
background-color: white;
color: black;
border: 2px solid #555555;
}

#sit-down-button:hover{
background-color: #555555;
color: white;
}
.card-content {
-webkit-transition: opacity 0.3s ease;
-moz-transition: opacity 0.3s ease;
Expand All @@ -187,21 +239,21 @@ input, textarea, *[contenteditable=true] {
@media screen and (min-width: 900px) {
.card {
display:inline-block;
width:calc(50% - 40pt);
/* width:calc(50% - 40pt); */
}
}

@media screen and (min-width: 1200px) {
.card {
display:inline-block;
width:calc(33% - 40pt);
/* width:calc(33% - 40pt); */
}
}

@media screen and (min-width: 1800px) {
.card {
display:inline-block;
width:calc(25% - 40pt);
/* width:calc(25% - 40pt); */
}
}

Expand Down Expand Up @@ -331,7 +383,7 @@ input, textarea, *[contenteditable=true] {

/* width */
::-webkit-scrollbar {
width: 10px;
width: 20px;
}

/* Track */
Expand Down
10 changes: 9 additions & 1 deletion rosboard/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<link rel="stylesheet" href="css/leaflet.css">
<link href="css/index.css" media="all" rel="stylesheet" type="text/css">

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/nipplejs/0.8.7/nipplejs.js"></script>
<script type="text/javascript" src="js/nipplejs.js"></script>
<script type="text/javascript" src="js/json5.min.js"></script>
<script type="text/javascript" src="js/uPlot.iife.min.js"></script>
<script type="text/javascript" src="js/jquery.transit.min.js"></script>
Expand Down Expand Up @@ -46,7 +46,15 @@
<main class="mdl-layout__content">
<div class="page-content">
<div class="grid">
<div class="grid-sizer"></div>
<!-- <div class="card card-pc--width"></div> -->
</div>
<!-- <div class="float-button">
<button id="stand-up" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-color--accent mdl-color-text--accent-contrast" onclick="standUp()">STAND UP</button>
<button id="sit-down" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-color--accent mdl-color-text--accent-contrast" onclick="sitDown()">SIT DOWN</button>

</div> -->

</div>
</main>
</div>
Expand Down
Loading