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 jpeg_quality parameter to log_image #2418

Merged
merged 20 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
35 changes: 22 additions & 13 deletions crates/re_data_ui/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn tensor_ui(
verbosity: UiVerbosity,
entity_path: &re_data_store::EntityPath,
annotations: &Annotations,
_encoded_tensor: &Tensor,
original_tensor: &Tensor,
tensor: &DecodedTensor,
) {
// See if we can convert the tensor to a GPU texture.
Expand Down Expand Up @@ -100,18 +100,25 @@ fn tensor_ui(
}

ui.label(format!(
"{} x {}",
"{} x {}{}",
tensor.dtype(),
format_tensor_shape_single_line(tensor.shape())
format_tensor_shape_single_line(tensor.shape()),
if original_tensor.data.is_compressed_image() {
" (compressed)"
} else {
""
}
))
.on_hover_ui(|ui| tensor_summary_ui(ctx.re_ui, ui, tensor, &tensor_stats));
.on_hover_ui(|ui| {
tensor_summary_ui(ctx.re_ui, ui, original_tensor, tensor, &tensor_stats);
});
});
}

UiVerbosity::All | UiVerbosity::Reduced => {
ui.vertical(|ui| {
ui.set_min_width(100.0);
tensor_summary_ui(ctx.re_ui, ui, tensor, &tensor_stats);
tensor_summary_ui(ctx.re_ui, ui, original_tensor, tensor, &tensor_stats);

if let Some(texture) = &texture_result {
let max_size = ui
Expand Down Expand Up @@ -145,9 +152,9 @@ fn tensor_ui(

// TODO(emilk): support copying and saving images on web
#[cfg(not(target_arch = "wasm32"))]
if _encoded_tensor.data.is_compressed_image() || tensor.could_be_dynamic_image()
if original_tensor.data.is_compressed_image() || tensor.could_be_dynamic_image()
{
copy_and_save_image_ui(ui, tensor, _encoded_tensor);
copy_and_save_image_ui(ui, tensor, original_tensor);
}

if let Some([_h, _w, channels]) = tensor.image_height_width_channels() {
Expand Down Expand Up @@ -204,16 +211,17 @@ fn show_image_at_max_size(
pub fn tensor_summary_ui_grid_contents(
re_ui: &re_ui::ReUi,
ui: &mut egui::Ui,
tensor: &Tensor,
original_tensor: &Tensor,
tensor: &DecodedTensor,
tensor_stats: &TensorStats,
) {
let Tensor {
tensor_id: _,
shape,
data,
data: _,
meaning,
meter,
} = tensor;
} = tensor.inner();

re_ui
.grid_left_hand_label(ui, "Data type")
Expand Down Expand Up @@ -256,7 +264,7 @@ pub fn tensor_summary_ui_grid_contents(
ui.end_row();
}

match data {
match &original_tensor.data {
re_components::TensorData::U8(_)
| re_components::TensorData::U16(_)
| re_components::TensorData::U32(_)
Expand Down Expand Up @@ -309,13 +317,14 @@ pub fn tensor_summary_ui_grid_contents(
pub fn tensor_summary_ui(
re_ui: &re_ui::ReUi,
ui: &mut egui::Ui,
tensor: &Tensor,
original_tensor: &Tensor,
tensor: &DecodedTensor,
tensor_stats: &TensorStats,
) {
egui::Grid::new("tensor_summary_ui")
.num_columns(2)
.show(ui, |ui| {
tensor_summary_ui_grid_contents(re_ui, ui, tensor, tensor_stats);
tensor_summary_ui_grid_contents(re_ui, ui, original_tensor, tensor, tensor_stats);
});
}

Expand Down
2 changes: 1 addition & 1 deletion crates/re_space_view_tensor/src/space_view_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl PerTensorState {
ctx.re_ui
.selection_grid(ui, "tensor_selection_ui")
.show(ui, |ui| {
tensor_summary_ui_grid_contents(ctx.re_ui, ui, tensor, &tensor_stats);
tensor_summary_ui_grid_contents(ctx.re_ui, ui, tensor, tensor, &tensor_stats);
self.texture_settings.ui(ctx.re_ui, ui);
self.color_mapping.ui(ctx.render_ctx, ctx.re_ui, ui);
});
Expand Down
4 changes: 2 additions & 2 deletions examples/python/arkitscenes/download_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def download_file(url: str, file_name: str, dst: Path) -> bool:
return False
os.rename(filepath + ".tmp", filepath)
else:
print(f"WARNING: skipping download of existing file: {filepath}")
pass # skipping download of existing file
return True


Expand Down Expand Up @@ -274,7 +274,7 @@ def download_data(
if not file_name.endswith(".zip") or not os.path.isdir(dst_path[: -len(".zip")]):
download_file(url, dst_path, dst_dir)
else:
print(f"WARNING: skipping download of existing zip file: {dst_path}")
pass # skipping download of existing zip file
if file_name.endswith(".zip") and os.path.isfile(dst_path):
unzip_file(file_name, dst_dir, keep_zip)

Expand Down
6 changes: 3 additions & 3 deletions examples/python/arkitscenes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def log_annotated_bboxes(annotation: dict[str, Any]) -> tuple[npt.NDArray[np.flo
# Generate a color per object that can be reused across both 3D obb and their 2D projections
# TODO(pablovela5620): Once #1581 or #1728 is resolved this can be removed
color_positions = np.linspace(0, 1, num_objects)
colormap = plt.cm.get_cmap("viridis")
colormap = plt.colormaps["viridis"]
colors = [colormap(pos) for pos in color_positions]

for i, label_info in enumerate(annotation["data"]):
Expand Down Expand Up @@ -396,7 +396,7 @@ def log_arkit(recording_path: Path, include_highres: bool) -> None:
colors_list,
)

rr.log_image(f"{lowres_posed_entity_id}/rgb", rgb)
rr.log_image(f"{lowres_posed_entity_id}/rgb", rgb, jpeg_quality=95)
rr.log_depth_image(f"{lowres_posed_entity_id}/depth", depth, meter=1000)

# log the high res camera
Expand All @@ -420,7 +420,7 @@ def log_arkit(recording_path: Path, include_highres: bool) -> None:
highres_depth = cv2.imread(f"{depth_dir}/{video_id}_{frame_timestamp}.png", cv2.IMREAD_ANYDEPTH)

highres_rgb = cv2.cvtColor(highres_bgr, cv2.COLOR_BGR2RGB)
rr.log_image(f"{highres_entity_id}/rgb", highres_rgb)
rr.log_image(f"{highres_entity_id}/rgb", highres_rgb, jpeg_quality=75)
rr.log_depth_image(f"{highres_entity_id}/depth", highres_depth, meter=1000)


Expand Down
4 changes: 1 addition & 3 deletions examples/python/colmap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ def read_and_log_sparse_reconstruction(dataset_path: Path, filter_output: bool,
if resize:
img = cv2.imread(str(image_file))
img = cv2.resize(img, resize)
jpeg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), 75]
_, encimg = cv2.imencode(".jpg", img, jpeg_quality)
rr.log_image_file("camera/image", img_bytes=encimg)
rr.log_image("camera/image", img, jpeg_quality=75)
Comment on lines 161 to +163
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a BGR vs RGB bug

else:
rr.log_image_file("camera/image", img_path=dataset_path / "images" / image.name)

Expand Down
2 changes: 1 addition & 1 deletion examples/python/mp_pose/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def track_pose(video_path: str, segment: bool) -> None:
rgb = cv.cvtColor(bgr_frame.data, cv.COLOR_BGR2RGB)
rr.set_time_seconds("time", bgr_frame.time)
rr.set_time_sequence("frame_idx", bgr_frame.idx)
rr.log_image("video/rgb", rgb)
rr.log_image("video/rgb", rgb, jpeg_quality=75)

results = pose.process(rgb)
h, w, _ = rgb.shape
Expand Down
2 changes: 1 addition & 1 deletion examples/python/nyud/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def log_nyud_data(recording_path: Path, subset_idx: int = 0) -> None:
if f.filename.endswith(".ppm"):
buf = archive.read(f)
img_rgb = read_image_rgb(buf)
rr.log_image("world/camera/image/rgb", img_rgb)
rr.log_image("world/camera/image/rgb", img_rgb, jpeg_quality=95)

elif f.filename.endswith(".pgm"):
buf = archive.read(f)
Expand Down
4 changes: 2 additions & 2 deletions examples/python/tracking_hf_opencv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def detect_objects_to_track(self, rgb: npt.NDArray[np.uint8], frame_idx: int) ->
_, _, scaled_height, scaled_width = inputs["pixel_values"].shape
scaled_size = (scaled_width, scaled_height)
rgb_scaled = cv.resize(rgb, scaled_size)
rr.log_image("image/scaled/rgb", rgb_scaled)
rr.log_image("image/scaled/rgb", rgb_scaled, jpeg_quality=95)
rr.log_disconnected_space("image/scaled") # Note: Haven't implemented 2D transforms yet.

logging.debug("Pass image to detection network")
Expand Down Expand Up @@ -334,7 +334,7 @@ def track_objects(video_path: str) -> None:
break

rgb = cv.cvtColor(bgr, cv.COLOR_BGR2RGB)
rr.log_image("image/rgb", rgb)
rr.log_image("image/rgb", rgb, jpeg_quality=95)

if not trackers or frame_idx % 40 == 0:
detections = detector.detect_objects_to_track(rgb=rgb, frame_idx=frame_idx)
Expand Down
7 changes: 6 additions & 1 deletion rerun_py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Visualization",
]
dependencies = ["deprecated", "numpy>=1.23", "pyarrow==10.0.1"]
dependencies = [
"deprecated",
"numpy>=1.23",
"pillow>=0.9.0,<0.10", # Used for JPEG encoding
"pyarrow==10.0.1",
]
description = "The Rerun Logging SDK"
keywords = ["computer-vision", "logging", "rerun"]
name = "rerun-sdk"
Expand Down
27 changes: 27 additions & 0 deletions rerun_py/rerun_sdk/rerun/log/image.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

from io import BytesIO
from typing import Any

import numpy as np
import numpy.typing as npt
from PIL import Image

from rerun import bindings
from rerun.log.error_utils import _send_warning
from rerun.log.file import ImageFormat, log_image_file
from rerun.log.log_decorator import log_decorator
from rerun.log.tensor import Tensor, _log_tensor, _to_numpy
from rerun.recording_stream import RecordingStream
Expand All @@ -27,6 +30,7 @@ def log_image(
ext: dict[str, Any] | None = None,
timeless: bool = False,
recording: RecordingStream | None = None,
jpeg_quality: int | None = None,
) -> None:
"""
Log a gray or color image.
Expand Down Expand Up @@ -59,6 +63,13 @@ def log_image(
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].
jpeg_quality:
If set, encode the image as a JPEG to save storage space.
Higher quality = larger file size.
A quality of 95 still saves a lot of space, but is visually very similar.
JPEG compression works best for photographs.
Only RGB images are supported.
Note that compressing to JPEG costs a bit of CPU time.

"""

Expand Down Expand Up @@ -90,6 +101,22 @@ def log_image(
if interpretable_as_image and num_non_empty_dims != len(shape):
image = np.squeeze(image)

if jpeg_quality is not None:
try:
pil_image = Image.fromarray(image)
emilk marked this conversation as resolved.
Show resolved Hide resolved
output = BytesIO()
pil_image.save(output, format="JPEG", quality=jpeg_quality)
jpeg_bytes = output.getvalue()
output.close()

# TODO(draw_order)
log_image_file(
entity_path=entity_path, img_bytes=jpeg_bytes, img_format=ImageFormat.JPEG, timeless=timeless
)
return
except ImportError:
_send_warning("Ignoring jpeg_quality because opencv-python is not installed", 1, recording=recording)
emilk marked this conversation as resolved.
Show resolved Hide resolved

_log_tensor(entity_path, image, draw_order=draw_order, ext=ext, timeless=timeless, recording=recording)


Expand Down