Skip to content

Commit

Permalink
Merge pull request #560 from will-moore/max_projection_bytes
Browse files Browse the repository at this point in the history
Max projection bytes
  • Loading branch information
will-moore authored Sep 2, 2024
2 parents cbdd365 + 996699f commit b540416
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 16 deletions.
24 changes: 21 additions & 3 deletions omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import os
from os import path
import zipfile
from math import atan2, atan, sin, cos, sqrt, radians, floor, ceil
from math import atan2, atan, sin, cos, sqrt, radians, floor, ceil, log2
from copy import deepcopy
import re

Expand Down Expand Up @@ -1678,11 +1678,29 @@ def get_panel_image(self, image, panel, orig_name=None):
t = panel['theT']
size_x = image.getSizeX()
size_y = image.getSizeY()
size_z = image.getSizeZ()
size_c = image.getSizeC()

if 'z_projection' in panel and panel['z_projection']:
if 'z_start' in panel and 'z_end' in panel:
image.setProjection('intmax')
image.setProjectionRange(panel['z_start'], panel['z_end'])
# check max_projection_bytes
pixel_range = image.getPixelRange()
bytes_per_pixel = ceil(log2(pixel_range[1]) / 8.0)
proj_bytes = (size_z * size_x * size_y
* size_c * bytes_per_pixel)

cfg = self.conn.getConfigService()
max_bytes = int(cfg.getConfigValue(
'omero.pixeldata.max_projection_bytes'))

if proj_bytes <= max_bytes:
image.setProjection('intmax')
image.setProjectionRange(panel['z_start'], panel['z_end'])
else:
print(f'projected_bytes {proj_bytes} exceeds '
f'MAX_PROJECTED_BYTES limit: {max_bytes}')
# Turn off all channels to render a black panel
image.setActiveChannels([])

# If big image, we don't want to render the whole plane
if self.is_big_image(image):
Expand Down
10 changes: 9 additions & 1 deletion omero_figure/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from omeroweb.webgateway import views as webgateway_views
from . import views
from django.urls import path
from django.urls import path, re_path


urlpatterns = [
Expand All @@ -33,6 +33,11 @@
path('imgData/<int:image_id>/', views.img_data_json,
name='figure_imgData'),

re_path(r'^max_projection_range_exceeded/'
r'(?P<iid>[0-9]+)/(?:(?P<z>[0-9]+)/)?(?:(?P<t>[0-9]+)/)?$',
views.max_projection_range_exceeded,
name='max_projection_range_exceeded'),

# Send json to OMERO to create pdf using scripting service
path('make_web_figure/', views.make_web_figure,
name='make_web_figure'),
Expand Down Expand Up @@ -74,6 +79,9 @@
# Use query ?image=1&image=2
path('timestamps/', views.timestamps, name='figure_timestamps'),

# Get pixelsType for images. Use query ?image=1&image=2
path('pixels_type/', views.pixels_type, name='figure_pixels_type'),

# Get Z scale for images
# Use query ?image=1&image=2
path('z_scale/', views.z_scale, name='figure_z_scale'),
Expand Down
58 changes: 58 additions & 0 deletions omero_figure/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def index(request, file_id=None, conn=None, **kwargs):
max_w, max_h = conn.getMaxPlaneSize()
max_plane_size = max_w * max_h
length_units = get_length_units()
cfg = conn.getConfigService()
max_bytes = cfg.getConfigValue('omero.pixeldata.max_projection_bytes')
is_public_user = "false"
if (hasattr(settings, 'PUBLIC_USER')
and settings.PUBLIC_USER == user.getOmeName()):
Expand All @@ -117,6 +119,9 @@ def index(request, file_id=None, conn=None, **kwargs):
'const MAX_PLANE_SIZE = %s;' % max_plane_size)
html = html.replace('const LENGTH_UNITS = LENGTHUNITS;',
'const LENGTH_UNITS = %s;' % json.dumps(length_units))
if max_bytes:
html = html.replace('const MAX_PROJECTION_BYTES = 1024 * 1024 * 256;',
'const MAX_PROJECTION_BYTES = %s;' % max_bytes)
if export_enabled:
html = html.replace('const EXPORT_ENABLED = false;',
'const EXPORT_ENABLED = true;')
Expand All @@ -136,6 +141,39 @@ def index(request, file_id=None, conn=None, **kwargs):
return HttpResponse(html)


@login_required()
def max_projection_range_exceeded(request, iid, z=None, t=None,
conn=None, **kwargs):
"""
The app will use this URL instead of `render_image/` if the
requested Z-projection range exceeds the maximum projected
bytes (given the number of active channels)
This returns a placeholder image with suitable message
"""

from PIL import Image, ImageDraw, ImageFont

font20 = ImageFont.load_default(20)
msg = "Max Z projection disabled"
msg_size = font20.getbbox(msg)
txt_w = msg_size[2]
txt_h = msg_size[3]

image_size = txt_w + 10

im = Image.new("RGB", (image_size, image_size), (5, 0, 0))
draw = ImageDraw.Draw(im)
text_y = im.size[1]/2 - txt_h/2
draw.text((im.size[0]/2 - txt_w/2, text_y), msg,
font=font20,
fill=(256, 256, 256))

rv = BytesIO()
im.save(rv, "jpeg", quality=90)
return HttpResponse(rv.getvalue(), content_type="image/jpeg")


@login_required()
def img_data_json(request, image_id, conn=None, **kwargs):

Expand Down Expand Up @@ -191,6 +229,26 @@ def timestamps(request, conn=None, **kwargs):
return JsonResponse(data)


@login_required()
def pixels_type(request, conn=None, **kwargs):

iids = request.GET.getlist('image')
data = {}
for iid in iids:
try:
iid = int(iid)
except ValueError:
pass
else:
image = conn.getObject('Image', iid)
if image is not None:
data[image.id] = {
"pixelsType": image.getPixelsType(),
"pixel_range": image.getPixelRange()
}
return JsonResponse(data)


@login_required()
def z_scale(request, conn=None, **kwargs):

Expand Down
5 changes: 0 additions & 5 deletions src/css/figure.css
Original file line number Diff line number Diff line change
Expand Up @@ -646,11 +646,6 @@
.z-projection {
padding: 1px 5px 5px;
}
.z-projection span{
background-image: url("../images/projection20.png");
width: 20px;
height: 20px;
}
.crop-btn span{
background-image: url("../images/crop20.png");
width: 20px;
Expand Down
1 change: 1 addition & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
const IS_PUBLIC_USER = false;
// Images larger than this are 'big' tiled images
const MAX_PLANE_SIZE = 10188864;
const MAX_PROJECTION_BYTES = 1024 * 1024 * 256;

const LOCAL_STORAGE_RECOVERED_FIGURE = "recoveredFigure" + USER_ID;

Expand Down
29 changes: 28 additions & 1 deletion src/js/models/figure_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

// Version of the json file we're saving.
// This only needs to increment when we make breaking changes (not linked to release versions.)
var VERSION = 7;
var VERSION = 8;


// ------------------------- Figure Model -----------------------------------
Expand Down Expand Up @@ -280,6 +280,31 @@
}
});
}

if (v < 8) {
console.log("Transforming to VERSION 8");
// need to load pixelsType.
var iids = json.panels.map(p => p.imageId);
console.log('Load pixelsType for images', iids);
if (iids.length > 0) {
var ptUrl = BASE_WEBFIGURE_URL + 'pixels_type/';
ptUrl += '?image=' + iids.join('&image=');
getJson(ptUrl).then(data => {
// Update all panels
// NB: By the time that this callback runs, the panels will have been created
self.panels.forEach(function(p){
var iid = p.get('imageId');
if (data[iid]) {
p.set({
'pixelsType': data[iid].pixelsType,
'pixel_range': data[iid].pixel_range
});
}
});
});
}
}

return json;
},

Expand Down Expand Up @@ -573,6 +598,8 @@
'pixel_size_x_unit': data.pixel_size.unitX,
'pixel_size_z_unit': data.pixel_size.unitZ,
'deltaT': data.deltaT,
'pixelsType': data.meta.pixelsType,
'pixel_range': data.pixel_range,
};
if (baseUrl) {
n.baseUrl = baseUrl;
Expand Down
29 changes: 28 additions & 1 deletion src/js/models/panel_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
// we replace these attributes...
var newData = {'imageId': data.imageId,
'name': data.name,
'pixelsType': data.pixelsType,
'pixel_range': data.pixel_range,
'sizeZ': data.sizeZ,
'sizeT': data.sizeT,
'orig_width': data.orig_width,
Expand Down Expand Up @@ -558,6 +560,22 @@
}
},

// Does sizeXYZ, pixelType and Channels exceed MAX_PROJECTION_BYTES?
isMaxProjectionBytesExceeded: function() {
let bytes_per_pixel = 4;
if (this.get("pixel_range")) {
bytes_per_pixel = Math.ceil(Math.log2(this.get("pixel_range")[1]) / 8.0);
}
let sizeC = this.get("channels").length;
let sizeXYZ = this.get('orig_width') * this.get('orig_height') * this.get('sizeZ');
let total_bytes = bytes_per_pixel * sizeC * sizeXYZ;
return total_bytes > MAX_PROJECTION_BYTES;
},

isMaxProjectionExceeded: function() {
return this.get('z_projection') && this.isMaxProjectionBytesExceeded();
},

// When a multi-select rectangle is drawn around several Panels
// a resize of the rectangle x1, y1, w1, h1 => x2, y2, w2, h2
// will resize the Panels within it in proportion.
Expand Down Expand Up @@ -723,7 +741,11 @@

// If BIG image, render scaled region
var region = "";
if (this.is_big_image()) {
if (this.isMaxProjectionExceeded()) {
// if we're trying to do Z-projection with too many planes,
// this figure URL renders a suitable error message
baseUrl = BASE_WEBFIGURE_URL + 'max_projection_range_exceeded/'
} else if (this.is_big_image()) {
baseUrl = BASE_WEBFIGURE_URL + 'render_scaled_region/';
var rect = this.getViewportAsRect();
// Render a region that is 1.5 x larger
Expand Down Expand Up @@ -768,6 +790,11 @@
// offset of the img within it's frame
get_vp_img_css: function(zoom, frame_w, frame_h, x, y) {

if (this.isMaxProjectionExceeded()) {
// We want the warning placeholder image shown full width, mid-height
return {'left':0, 'top':(frame_h - frame_w)/2, 'width':frame_w, 'height': frame_w}
}

// For non-big images, we have the full plane in hand
// css just shows the viewport region
if (!this.is_big_image()) {
Expand Down
3 changes: 2 additions & 1 deletion src/js/views/info_panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ var InfoPanelView = Backbone.View.extend({
} else {
json.name = title;
// compare json summary so far with this Panel
var attrs = ["imageId", "orig_width", "orig_height", "sizeT", "sizeZ", "x", "y", "width", "height", "dpi", "min_export_dpi", "max_export_dpi"];
var attrs = ["imageId", "orig_width", "orig_height", "sizeT", "sizeZ", "x", "y",
"width", "height", "dpi", "min_export_dpi", "max_export_dpi", "pixelsType"];
_.each(attrs, function(a){
if (json[a] != this_json[a]) {
if (a === 'x' || a === 'y' || a === 'width' || a === 'height') {
Expand Down
2 changes: 2 additions & 0 deletions src/js/views/modal_views.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ import { hideModal } from "./util";
var newImg = {
'imageId': data.id,
'name': data.meta.imageName,
'pixelsType': data.meta.pixelsType,
'pixel_range': data.pixel_range,
// 'width': data.size.width,
// 'height': data.size.height,
'sizeZ': data.size.z,
Expand Down
2 changes: 1 addition & 1 deletion src/js/views/panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
this.render_layout);
this.listenTo(this.model, 'change:scalebar change:pixel_size_x', this.render_scalebar);
this.listenTo(this.model,
'change:zoom change:dx change:dy change:width change:height change:channels change:theZ change:theT change:z_start change:z_end change:z_projection change:min_export_dpi',
'change:zoom change:dx change:dy change:width change:height change:channels change:theZ change:theT change:z_start change:z_end change:z_projection change:min_export_dpi change:pixel_range',
this.render_image);
this.listenTo(this.model,
'change:channels change:zoom change:dx change:dy change:width change:height change:rotation change:labels change:theT change:deltaT change:theZ change:deltaZ change:z_projection change:z_start change:z_end',
Expand Down
5 changes: 5 additions & 0 deletions src/js/views/right_panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -1181,9 +1181,11 @@
sum_sizeZ = 0,
rotation,
z_projection,
projection_bytes_exceeded = [],
zp;
if (this.models) {
this.models.forEach(function(m, i){
projection_bytes_exceeded.push(m.isMaxProjectionBytesExceeded())
rotation = m.get('rotation');
max_rotation = Math.max(max_rotation, rotation);
sum_rotation += rotation;
Expand All @@ -1198,6 +1200,7 @@
}
}
});
let proj_bytes_exceeded = projection_bytes_exceeded.some(b => b);
var avg_rotation = sum_rotation / this.models.length;
if (avg_rotation === max_rotation) {
rotation = avg_rotation;
Expand All @@ -1213,6 +1216,8 @@
const z_projection_disabled = ((sum_sizeZ === this.models.length) || anyBig);

html = this.template({
max_projection_bytes: MAX_PROJECTION_BYTES,
proj_bytes_exceeded: proj_bytes_exceeded,
projectionIconUrl,
'z_projection_disabled': z_projection_disabled,
'rotation': rotation,
Expand Down
3 changes: 2 additions & 1 deletion src/js/views/scalebar_form_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ var ScalebarFormView = Backbone.View.extend({
} else {
let pix_sze = m.get('pixel_size_x');
// account for floating point imprecision when comparing
pix_sze = pix_sze ? pix_sze.toFixed(10) : pix_sze;
if (json.pixel_size_x != '-' &&
json.pixel_size_x.toFixed(10) != pix_sze.toFixed(10)) {
json.pixel_size_x.toFixed(10) != pix_sze) {
json.pixel_size_x = '-';
}
if (json.pixel_size_symbol != m.get('pixel_size_x_symbol')) {
Expand Down
6 changes: 4 additions & 2 deletions src/templates/image_display_options.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@


<div class="btn-group image-display-options"
title="Maximum intensity Z-projection (choose range with 2 handles on Z-slider)">
title="Maximum intensity Z-projection <% if (proj_bytes_exceeded) { print('exceeds MAX_PROJECTION_BYTES: ' + max_projection_bytes ) }
else { %>(choose range with 2 handles on Z-slider)<% } %>">
<button type="button" class="btn btn-outline-secondary btn-sm z-projection
<% if(z_projection) { %>zp-btn-down<% } else if (typeof z_projection == 'boolean') { %><% } else { %>ch-btn-flat<% }%>"
<% if (z_projection_disabled) { %>disabled="disabled"<% } %>
<% if ((!z_projection && proj_bytes_exceeded) || z_projection_disabled) { %>disabled="disabled"<% } %>
>

<img src="<%= projectionIconUrl %>" />
</button>
</div>
2 changes: 2 additions & 0 deletions src/templates/info_panel.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
print(_.escape(c)); print((i < channels.length-1) ? ", " : "");
}); %>
</small></div>
<div class="col-sm-5"><small><strong>Pixels Type</strong>:</small></div>
<div class="col-sm-7"><small><%= pixelsType %></small></div>
</td></tr>
</tbody>
</table>

0 comments on commit b540416

Please sign in to comment.