Skip to content

Commit

Permalink
ENH: Terminal CLI output
Browse files Browse the repository at this point in the history
A few tweaks are also made to the cli flags for useability.

Requires a terminal that supports the iterm2 inline image protocal, e.g.
wezterm, iterm2, VSCode Terminal.
  • Loading branch information
thewtex committed Jul 21, 2023
1 parent 38f9b75 commit fee85ac
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 24 deletions.
27 changes: 25 additions & 2 deletions itkwidgets/standalone/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
}
async function setupViewerForImJoy(server) {
const version = itkVtkViewer.version

await server.registerService({
name: 'itkwidgets-service',
id: 'itkwidgets_service',
Expand All @@ -59,13 +60,16 @@
cover: ['https://kitware.github.io/itk-vtk-viewer/docs/howToUse.jpg', 'https://kitware.github.io/itk-vtk-viewer/docs/imjoy.png'],

});

itkVtkViewer.imJoyCodecs.forEach((codec) => {
server.registerCodec(codec)
})

const remoteID = `${workspace}/itkwidgets-server`;
let remoteService = await server.getService(`${remoteID}:itkwidgets-input-obj`);
const data = await remoteService.inputObject();
const viewer = await itkVtkViewer.createViewer(container, data.data);

await server.registerService({
name: "itk_vtk_viewer",
id: "itk-vtk-viewer",
Expand All @@ -75,8 +79,26 @@
require_context: false,
run_in_executor: true,
},
viewer: () => viewer
viewer: () => viewer,
capture_screenshot: async () => {
const viewProxy = viewer.getViewProxy();
const representationProxy = viewProxy.getRepresentations()[0];
let mapper = null
if (representationProxy && representationProxy.getClassName() === 'vtkVolumeRepresentationProxy') {
mapper = representationProxy.getMapper();
mapper.setAutoAdjustSampleDistances(false)
}

const capturedImage = await viewer.captureImage()

if (representationProxy && representationProxy.getClassName() === 'vtkVolumeRepresentationProxy') {
mapper.setAutoAdjustSampleDistances(true)
}

return capturedImage
},
});

await server.registerService({
name: "setLabelImage",
id: "set-label-image",
Expand All @@ -89,9 +111,10 @@
set_label_image: async (uuid) => {
remoteService = await server.getService(`${remoteID}:fetch-zarr-store`);
let labelImage = await remoteService.fetchZarrStore(uuid);
await viewer.setLabelImage(labelImage);
viewer.setLabelImage(labelImage);
}
});

remoteService = await server.getService(`${remoteID}:itkwidgets-viewer-ready`);
await remoteService.viewerReady(viewer);
}
Expand Down
143 changes: 121 additions & 22 deletions itkwidgets/standalone_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
import sys
import time
from pathlib import Path

from base64 import b64decode
import webbrowser

from playwright.sync_api import sync_playwright
import imjoy_rpc
import numpy as np
import PIL
from imgcat import imgcat

from imjoy_rpc.hypha import connect_to_server_sync
from itkwidgets.standalone.config import SERVER_HOST, SERVER_PORT, VIEWER_HTML
Expand All @@ -31,6 +35,7 @@
from urllib3 import PoolManager, exceptions

logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger("websocket-client").setLevel(logging.ERROR)


def find_port(port=SERVER_PORT):
Expand All @@ -47,6 +52,7 @@ def find_port(port=SERVER_PORT):
OPTS = None
EVENT = threading.Event()
ZARR_STORE = {}
BROWSER = None


def standalone_viewer(url):
Expand All @@ -71,6 +77,18 @@ def input_dict():
data = build_init_data(user_input)
ui = user_input.get("ui", "reference")
data["config"] = build_config(ui)

if data["view_mode"] is not None:
vm = data["view_mode"]
if vm == "x":
data["view_mode"] = "XPlane"
elif vm == "y":
data["view_mode"] = "YPlane"
elif vm == "z":
data["view_mode"] = "ZPlane"
elif vm == "v":
data["view_mode"] = "Volume"

return {"data": data}


Expand All @@ -83,7 +101,11 @@ def read_files():
if reader:
reader = ConversionBackend(reader)
else:
reader = detect_cli_io_backend(input)
reader = detect_cli_io_backend([input])
if not input.find('://') == -1 and not Path(input).exists():
sys.stderr.write(f"File not found: {input}\n")
# hack
raise KeyboardInterrupt
ngff_image = cli_input_to_ngff_image(reader, [input])
user_input[param] = ngff_image
return user_input
Expand Down Expand Up @@ -179,11 +201,7 @@ def start_viewer(server_url):
}
)

workspace = server.config.workspace
token = server.generate_token()
params = urlencode({"workspace": workspace, "token": token})
webbrowser.open_new_tab(f"{server_url}/itkwidgets/index.html?{params}")
return server
return server, input_obj


def main():
Expand Down Expand Up @@ -222,19 +240,82 @@ def main():
timeout -= 0.1
time.sleep(0.1)

server = start_viewer(server_url)
server, input_obj = start_viewer(server_url)
workspace = server.config.workspace
token = server.generate_token()
params = urlencode({"workspace": workspace, "token": token})
url = f"{server_url}/itkwidgets/index.html?{params}"

# Updates for resolution progression
rate = 1.0
fast_rate = 0.05
if OPTS.rotate:
rate = fast_rate

if OPTS.browser:
sys.stdout.write(f"Viewer url:\n\n {url}\n\n")
webbrowser.open_new_tab(f"{server_url}/itkwidgets/index.html?{params}")
else:
playwright = sync_playwright().start()
args = [
"--enable-unsafe-webgpu",
]
browser = playwright.chromium.launch(args=args)
BROWSER = browser
page = browser.new_page()

terminal_size = os.get_terminal_size()
width = terminal_size.columns * 10
is_tmux = 'TMUX' in os.environ and 'tmux' in os.environ['TMUX']
# https://github.com/tmux/tmux/issues/1502
if is_tmux:
width = min(width, 400)
else:
width = min(width, 1024)
height = width
page.set_viewport_size({"width": width, "height": height})

response = page.goto(url, timeout=0, wait_until="load")
assert response.status == 200, (
"Failed to start browser app instance, "
f"status: {response.status}, url: {url}"
)

input_data = input_obj["data"]
if not input_data["use2D"]:
if input_data["x_slice"] is None and input_data["view_mode"] == "XPlane":
page.locator('label[itk-vtk-tooltip-content="X plane play scroll"]').click()
rate = fast_rate
elif input_data["y_slice"] is None and input_data["view_mode"] == "YPlane":
page.locator('label[itk-vtk-tooltip-content="Y plane play scroll"]').click()
rate = fast_rate
elif input_data["y_slice"] is None and input_data["view_mode"] == "ZPlane":
page.locator('label[itk-vtk-tooltip-content="Z plane play scroll"]').click()
rate = fast_rate

EVENT.wait() # Wait until viewer is created before launching REPL
workspace = server.config.workspace
svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer")
viewer = view(itk_viewer=svc.viewer(), server=server)
if not OPTS.browser:
terminal_height = min(terminal_size.lines - 1, terminal_size.columns // 3)


while True:
png_bin = b64decode(svc.capture_screenshot()[22:])
imgcat(png_bin, height=terminal_height)
time.sleep(rate)
CSI = b'\033['
sys.stdout.buffer.write(CSI + str(terminal_height).encode() + b"F")

if OPTS.repl:
EVENT.wait() # Wait until viewer is created before launching REPL
workspace = server.config.workspace
svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer")
viewer = view(itk_viewer=svc.viewer(), server=server)
banner = f"""
Welcome to the itkwidgets command line tool! Press CTRL+D or
run `exit()` to terminate the REPL session. Use the `viewer`
object to manipulate the viewer.
"""
exitmsg = "Exiting REPL. Press CTRL+C to teminate CLI tool."
code.interact(banner=banner, local={"viewer": viewer}, exitmsg=exitmsg)
exitmsg = "Exiting REPL. Press CTRL+C to exit the viewer."
code.interact(banner=banner, local={"viewer": viewer, "svc": svc, "server": server}, exitmsg=exitmsg)


def cli_entrypoint():
Expand All @@ -243,11 +324,11 @@ def cli_entrypoint():
parser = argparse.ArgumentParser()

parser.add_argument("data", nargs="?", type=str, help="Path to a data file.")
parser.add_argument("--image", type=str, help="Path to an image data file.")
parser.add_argument("-i", "--image", dest="image", type=str, help="Path to an image data file.")
parser.add_argument(
"--label-image", type=str, help="Path to a label image data file."
"-l", "--label-image", dest="label_image", type=str, help="Path to a label image data file."
)
parser.add_argument("--point-set", type=str, help="Path to a point set data file.")
parser.add_argument("-p", "--point-set", dest="point_set", type=str, help="Path to a point set data file.")
parser.add_argument(
"--use2D", dest="use2D", action="store_true", default=False, help="Image is 2D."
)
Expand All @@ -264,6 +345,13 @@ def cli_entrypoint():
default=False,
help="Print all log messages to stdout.",
)
parser.add_argument(
"-b", "--browser",
dest="browser",
action="store_true",
default=False,
help="Render to a browser tab instead of the terminal.",
)
parser.add_argument(
"--repl",
dest="repl",
Expand All @@ -273,7 +361,7 @@ def cli_entrypoint():
)
# General Interface
parser.add_argument(
"--rotate",
"-r", "--rotate",
dest="rotate",
action="store_true",
default=False,
Expand Down Expand Up @@ -307,6 +395,7 @@ def cli_entrypoint():
"--bg-color",
type=tuple,
nargs="+",
default=(0.0, 0.0, 0.0),
help="Background color: (red, green, blue) tuple, components from 0.0 to 1.0.",
)
# Images
Expand Down Expand Up @@ -400,9 +489,9 @@ def cli_entrypoint():
help="Whether to used gradient-based shadows in the volume rendering.",
)
parser.add_argument(
"--view-mode",
"-m", "--view-mode",
type=str,
choices=["XPlane", "YPlane", "ZPlane", "Volume"],
choices=["x", "y", "z", "v"],
help="Only relevant for 3D scenes.",
)
parser.add_argument(
Expand All @@ -427,8 +516,18 @@ def cli_entrypoint():

OPTS = parser.parse_args()

main()
try:
main()
except KeyboardInterrupt:
if BROWSER:
BROWSER.close()
if not OPTS.browser:
# Clear `^C%`
CSI = b'\033['
sys.stdout.buffer.write(CSI + b"1K")
sys.stdout.buffer.write(b"\n")
sys.exit(0)


if __name__ == "__main__":
cli_entrypoint()
cli_entrypoint()
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ cli = [
"hypha >= 0.15.28",
"IPython >= 8.4.0",
"itk-io >= 5.3.0",
"ngff-zarr[cli]",
"playwright",
]

notebook = [
Expand Down

0 comments on commit fee85ac

Please sign in to comment.