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

[WIP] Adding bounding boxes and fixing notebooks #25

Open
wants to merge 5 commits into
base: master
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 65 additions & 2 deletions cornerstone_widget/cs_widget.py
Expand Up @@ -6,7 +6,8 @@
import traitlets as tr
from IPython.display import display

from .utils import encode_numpy_b64, button_debounce
from .utils import encode_numpy_b64, button_debounce, get_bbox_handles, \
inject_dict

MIN_RANGE = 1 # the minimum max-min value (prevent dividing by 0)

Expand Down Expand Up @@ -108,8 +109,69 @@ def get_tool_state(self):
return {}

def set_tool_state(self, state):
# type: (Dict) -> None
"""A method for feeding data into the widget"""
self._tool_state_in = json.dumps(state)
self._tool_state_out = json.dumps(state)
Copy link
Contributor

Choose a reason for hiding this comment

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

why the two tool states?

Copy link
Collaborator Author

@kmader kmader Oct 2, 2018

Choose a reason for hiding this comment

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

so this isn't a very elegant approach, but one is for setting the state (_in) from Python and one is for reading the state (_out) from Cornerstone. The state itself isn't a traitlet and the traitlet is only updated on a mouseup event


def get_bbox(self):
# type: () -> List[Dict[str, List[float]]]
"""
Get all bounding boxes for the widget
:return:
>>> cs = CornerstoneWidget()
>>> cs.get_bbox()
[]
"""
return get_bbox_handles(self.get_tool_state())

def add_bbox(self,
bbox # type: Dict[str, List[float]]
):
# type: (...) -> Dict
"""
Add a bounding box to the current display
:param bbox: bounding box (same format as get_bbox
:return:
>>> cs = CornerstoneWidget()
>>> out_state = cs.add_bbox({'x': [0, 5], 'y': [6, 10]})
>>> cs.get_bbox()
[{'x': [0, 5], 'y': [6, 10]}]
"""
if len(bbox.get('x', [])) != 2:
raise ValueError('Invalid x for bounding box: {}'.format(bbox))
if len(bbox.get('y', [])) != 2:
raise ValueError('Invalid y for bounding box: {}'.format(bbox))
# standard fields in a rectRoi Object (if created in cs
# visible -> True
# active -> False
# invalidated -> False
# handles ->
# {'start': {'x': , 'y': , 'highlight': True, 'active': False}
# {'end': {'x': , 'y': , 'highlight': True, 'active': False}
# 'textBox': {'active': False, 'hasMoved': False,
# 'movesIndependently': False, 'drawnIndependently': True,
# 'allowedOutsideImage': True, 'hasBoundingBox': True, 'x':, 'y':,
# 'boundingBox': {'width': , 'height': , 'left': , 'top'}}

# the elements used here are by trial and error as the minimum set
# for javascript not to complain
n_bbox = [{
'visible': True,
'handles': {'start': {'x': min(bbox['x']),
'y': min(bbox['y'])},
'end': {'x': max(bbox['x']),
'y': max(bbox['y'])}},
'textBox': {'hasMoved': False}
}]
old_state = self.get_tool_state()
new_state = inject_dict(old_state, ['imageIdToolState',
'',
'rectangleRoi',
'data'],
n_bbox)
self.set_tool_state(new_state)
return new_state


class WidgetObject:
Expand Down Expand Up @@ -181,6 +243,7 @@ def _first_click(button):
refresh_but.on_click(_first_click)

self._toolbar = [] # type: List[widgets.Widget]

if not show_reset:
self._toolbar += [refresh_but]

Expand All @@ -195,7 +258,7 @@ def _callback(button):

for name in tools:
if name == 'reset':
self._toolbar = [refresh_but]
self._toolbar += [refresh_but]
else:
if name not in self.TOOLS:
raise NotImplementedError(
Expand Down
33 changes: 32 additions & 1 deletion cornerstone_widget/utils.py
@@ -1,6 +1,6 @@
import base64
from functools import wraps
from typing import Callable
from typing import Callable, Any, Dict, List

import ipywidgets as ipw
import numpy as np
Expand Down Expand Up @@ -102,7 +102,38 @@ def get_nested(a_dict, *args, default_value=None):
return value


def inject_dict(in_dict, # type: Dict[Any, Any]
key_list, # type: List[Any]
value # type: List[Any]
):
# type: (...) -> Dict[Any, Any]
"""
Add elements to an existing dictionary, useful for updating the
state of the cornerstone widget without changing anything else
:param in_dict:
:param key_list:
:param value:
:return:
>>> inject_dict({}, ['a', 'b'], [1, 2])
{'a': {'b': [1, 2]}}
>>> inject_dict({'a': {'b': [1, 2], 'c': 'hey'}}, ['a', 'b'], [3, 4])
{'a': {'b': [1, 2, 3, 4], 'c': 'hey'}}
"""
new_dict = in_dict.copy()
# apparently python works with pointers
n_pointer = new_dict
for c_key in key_list[:-1]:
if c_key not in n_pointer:
n_pointer[c_key] = {}
n_pointer = n_pointer[c_key]
cur_val = get_nested(in_dict, *key_list, default_value=[])
cur_val += value
n_pointer[key_list[-1]] = cur_val
return new_dict


def get_bbox_handles(in_view_dict):
# type: (Dict[str, Any]) -> List[Dict[str, List[float]]]
"""
the bounding box info is buried in a lot of dictionaries
:param in_view_dict: a dictionary with the list inside of it
Expand Down
19 changes: 11 additions & 8 deletions js/lib/cs_widget.js
Expand Up @@ -82,15 +82,17 @@ var CornerstoneView = widgets.DOMWidgetView.extend({
this.model.on('change:_selected_tool', this.activate_tool, this);
var my_viewer = this.viewer;
var my_model = this.model;
var state_save_callback = function (e) {
var appState = ctools.appState.save([my_viewer]);
var appStr = JSON.stringify(appState);
console.log('State is:' + appStr);
my_model.set('_tool_state_out', appStr);
my_model.save_changes();
};
// save the cornerstone state on mouseup to catch both clicks and drags
this.viewer.addEventListener('mouseup',
function (e) {
var appState = ctools.appState.save([my_viewer]);
var appStr = JSON.stringify(appState);
console.log('State is:' + appStr);
my_model.set('_tool_state_out', appStr);
my_model.save_changes();
});
this.viewer.addEventListener('mouseup', state_save_callback);
// save the state now
state_save_callback(0);
},
parse_image: function (imageB64Data, width, height, min_val, max_val, color) {
var pixelDataAsString = window.atob(imageB64Data, width, height);
Expand Down Expand Up @@ -224,6 +226,7 @@ var CornerstoneView = widgets.DOMWidgetView.extend({
console.log('updating state:' + new_state_json + ', ' + new_state_json.length);
ctools.appState.restore(appState);
}
this.model.set('_tool_state_out', new_state_json);
},
zoom_changed: function () {
this.viewport.scale = this.model.get('img_scale');
Expand Down
43 changes: 38 additions & 5 deletions notebooks/demo.ipynb
Expand Up @@ -8,6 +8,7 @@
"source": [
"import ipywidgets as ipw\n",
"from cornerstone_widget import CornerstoneToolbarWidget\n",
"from cornerstone_widget.utils import get_bbox_handles\n",
"import numpy as np"
]
},
Expand All @@ -23,7 +24,7 @@
" cs_obj.update_image(img_maker(c_wid))\n",
"\n",
"def zoom_viewer(cs_obj, zf):\n",
" cs_obj.img_scale+=zf"
" cs_obj.cur_image_view.img_scale+=zf"
]
},
{
Expand All @@ -32,7 +33,7 @@
"metadata": {},
"outputs": [],
"source": [
"cs_view = CornerstoneToolbarWidget()"
"cs_view = CornerstoneToolbarWidget(tools = ['zoom', 'probe', 'bbox', 'reset'])"
]
},
{
Expand Down Expand Up @@ -62,15 +63,21 @@
"\n",
"cgradient_img_but.on_click(lambda *args: show_image(cs_view, cgradient_image))\n",
"\n",
"\n",
"half_img_but = ipw.Button(description='Half Image')\n",
"half_image = lambda x: np.eye(x)[:x//2]\n",
"half_img_but.on_click(lambda *args: show_image(cs_view, half_image))\n",
"\n",
"zoom_in_but = ipw.Button(description='Zoom In')\n",
"zoom_in_but.on_click(lambda *args: zoom_viewer(cs_view, 0.25))\n",
"zoom_out_but = ipw.Button(description='Zoom Out')\n",
"zoom_out_but.on_click(lambda *args: zoom_viewer(cs_view, -0.25))"
"zoom_out_but.on_click(lambda *args: zoom_viewer(cs_view, -0.25))\n",
"\n",
"add_bbox = ipw.Button(description='Add Bounding Box')\n",
"add_bbox.on_click(lambda *args: cs_view.cur_image_view.add_bbox({'x': [0, 100], \n",
" 'y': [100, 150]}))\n",
"\n",
"print_bbox = ipw.Button(description='Print Bounding Boxes')\n",
"print_bbox.on_click(lambda *args: print(cs_view.cur_image_view.get_bbox()))"
]
},
{
Expand All @@ -84,11 +91,37 @@
" ipw.HBox([\n",
" size_scroller,\n",
" ipw.VBox([noisy_img_but, half_img_but, gradient_img_but, cgradient_img_but]),\n",
" ipw.VBox([zoom_in_but, zoom_out_but])\n",
" ipw.VBox([zoom_in_but, zoom_out_but, add_bbox, print_bbox])\n",
" ])\n",
"])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if False: \n",
" # manually create bounding boxes nice debugging code\n",
" from cornerstone_widget.utils import inject_dict\n",
" self = cs_view.cur_image_view\n",
" bbox = {'x': [0, 100], 'y': [100, 150]}\n",
" n_bbox = [{'visible': True,\n",
" 'handles': {'start': {'x': min(bbox['x']),\n",
" 'y': min(bbox['y'])},\n",
" 'end': {'x': max(bbox['x']),\n",
" 'y': max(bbox['y'])}}}]\n",
" old_state = self.get_tool_state()\n",
" print(old_state)\n",
" new_state = inject_dict(old_state, ['imageIdToolState',\n",
" '',\n",
" 'rectangleRoi',\n",
" 'data'],\n",
" n_bbox)\n",
" self.set_tool_state(new_state)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
17 changes: 17 additions & 0 deletions tests/test_roi.py
@@ -1,6 +1,7 @@
import json

from cornerstone_widget import get_bbox_handles
from cornerstone_widget.utils import inject_dict

_test_bbox_json = """
{"imageIdToolState": {"": {"rectangleRoi": {"data": [{"visible": true, "active": false, "invalidated": false, "handles": {"start": {"x": 553.3138489596392, "y": 449.722433543228, "highlight": true, "active": false}, "end": {"x": 835.5569648554714, "y": 705.8887398182495, "highlight": true, "active": false}, "textBox": {"active": false, "hasMoved": false, "movesIndependently": false, "drawnIndependently": true, "allowedOutsideImage": true, "hasBoundingBox": true, "x": 835.5569648554714, "y": 577.8055866807388, "boundingBox": {"width": 150.8333282470703, "height": 65, "left": 312.93333435058605, "top": 195.39999389648438}}}, "meanStdDev": {"count": 72731, "mean": 137.81189589033562, "variance": 484.0080783665253, "stdDev": 22.00018359847311}, "area": 72301.17647058812}]}}}, "elementToolState": {}, "elementViewport": {}, "viewing_time": 77.17544794082642}
Expand All @@ -20,3 +21,19 @@ def test_bbox_parser():
assert len(b_bbox) == 2
assert b_bbox[0]['x'][0] < 200
assert b_bbox[0]['x'][1] > 450


def test_inject():
n_dict = json.loads(_test_bbox_json)
n_bbox = [{'handles': {'start': {'x': 0, 'y': 5},
'end': {'x': 0, 'y': 5}}}]
m_dict = inject_dict(n_dict, ['imageIdToolState',
'',
'rectangleRoi',
'data'],
n_bbox)
m_bbox = get_bbox_handles(m_dict)
print(m_bbox)

assert len(m_bbox) == 2
assert m_bbox[1]['x'][0] == 0
7 changes: 6 additions & 1 deletion tests/test_widget.py
Expand Up @@ -2,7 +2,6 @@
import pytest
from ipywidgets.embed import embed_snippet


from cornerstone_widget import CornerstoneWidget, CornerstoneToolbarWidget
from cornerstone_widget.cs_widget import encode_numpy_b64
from cornerstone_widget.utils import _virtual_click_button
Expand Down Expand Up @@ -72,6 +71,12 @@ def test_notoolbar():
assert start_but.comm is None, 'Should be a dead button'


def test_toolbar_w_reset():
cs_view = CornerstoneToolbarWidget(tools=['zoom',
'probe', 'bbox', 'reset'])
assert len(cs_view._toolbar) == 4


def test_invalid_toolbar():
with pytest.raises(NotImplementedError):
CornerstoneToolbarWidget(tools=['Magic_Lasso'])