Skip to content

Commit

Permalink
Merge pull request #591 from MinaEnayat/flipping
Browse files Browse the repository at this point in the history
Add Horizontal and Vertical Image Flipping Functionality
  • Loading branch information
will-moore authored Nov 27, 2024
2 parents f0bc7ec + 339e655 commit 043c645
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 17 deletions.
71 changes: 69 additions & 2 deletions omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,18 @@ def panel_to_page_coords(self, shape_x, shape_y):
Also includes 'inPanel' key - True if point within
the cropped panel region
"""

# Apply flip transformations to the shape coordinates
h_flip = self.panel.get('horizontal_flip', False)
v_flip = self.panel.get('vertical_flip', False)
if h_flip:
shape_x = self.crop['width'] - shape_x + 2*self.crop['x']
if v_flip:
shape_y = self.crop['height'] - shape_y + 2*self.crop['y']

rotation = self.panel['rotation']
if v_flip != h_flip:
rotation = -rotation
if rotation != 0:
# img coords: centre of rotation
cx = self.crop['x'] + (self.crop['width'] / 2)
Expand Down Expand Up @@ -504,7 +515,21 @@ def draw_ellipse(self, shape):
cy = self.page_height - c['y']
rx = shape['radiusX'] * self.scale
ry = shape['radiusY'] * self.scale
rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1

rotation = shape.get('rotation', 0)
h_flip = self.panel.get('horizontal_flip', False)
v_flip = self.panel.get('vertical_flip', False)

if v_flip:
rotation = - rotation
if h_flip:
rotation = 180 - rotation

if v_flip != h_flip:
rotation = (rotation - self.panel['rotation']) * -1
else:
rotation = (rotation + self.panel['rotation']) * -1

r, g, b, a = self.get_rgba(shape['strokeColor'])
self.canvas.setStrokeColorRGB(r, g, b, alpha=a)

Expand Down Expand Up @@ -569,7 +594,18 @@ def get_panel_coords(self, shape_x, shape_y):
x, y point around the centre of the cropped region
and scaling appropriately
"""
h_flip = self.panel.get('horizontal_flip', False)
v_flip = self.panel.get('vertical_flip', False)

# Apply flip transformations to the shape coordinates
if h_flip:
shape_x = self.crop['width'] - shape_x + 2*self.crop['x']
if v_flip:
shape_y = self.crop['height'] - shape_y + 2*self.crop['y']

rotation = self.panel['rotation']
if v_flip != h_flip:
rotation = -rotation
if rotation != 0:
# img coords: centre of rotation
cx = self.crop['x'] + (self.crop['width'] / 2)
Expand Down Expand Up @@ -816,7 +852,20 @@ def draw_ellipse(self, shape):
cy = ctr['y']
rx = self.scale * shape['radiusX']
ry = self.scale * shape['radiusY']
rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1

rotation = shape.get('rotation', 0)
h_flip = self.panel.get('horizontal_flip', False)
v_flip = self.panel.get('vertical_flip', False)

if v_flip:
rotation = - rotation
if h_flip:
rotation = 180 - rotation

if v_flip != h_flip:
rotation = (rotation - self.panel['rotation']) * -1
else:
rotation = (rotation + self.panel['rotation']) * -1

width = int((rx * 2) + w)
height = int((ry * 2) + w)
Expand Down Expand Up @@ -2216,6 +2265,15 @@ def draw_scalebar_line(self, x, y, x2, y2, width, rgb):
def paste_image(self, pil_img, img_name, panel, page, dpi):
""" Adds the PIL image to the PDF figure. Overwritten for TIFFs """

# Apply flip transformations before drawing the image
h_flip = panel.get('horizontal_flip', False)
v_flip = panel.get('vertical_flip', False)

if h_flip:
pil_img = pil_img.transpose(Image.FLIP_LEFT_RIGHT)
if v_flip:
pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM)

x = panel['x']
y = panel['y']
width = panel['width']
Expand Down Expand Up @@ -2315,6 +2373,15 @@ def add_page_color(self):
def paste_image(self, pil_img, img_name, panel, page, dpi=None):
""" Add the PIL image to the current figure page """

# Apply flip transformations before drawing the image
h_flip = panel.get('horizontal_flip', False)
v_flip = panel.get('vertical_flip', False)

if h_flip:
pil_img = pil_img.transpose(Image.FLIP_LEFT_RIGHT)
if v_flip:
pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM)

x = panel['x']
y = panel['y']
width = panel['width']
Expand Down
14 changes: 14 additions & 0 deletions src/css/figure.css
Original file line number Diff line number Diff line change
Expand Up @@ -1338,3 +1338,17 @@
.form-inline{
cursor: move;
}

.flipping button{
margin: 2px;
}

.flipping .btn.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}

.flipping .btn.active i {
color: white;
}
1 change: 1 addition & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
/>

</head>
<body id="body">
<!-- Welcome splash-screen Modal -->
Expand Down
9 changes: 6 additions & 3 deletions src/js/models/panel_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
pixel_size_x_unit: 'MICROMETER',
rotation_symbol: '\xB0',
max_export_dpi: 1000,

vertical_flip: false,
horizontal_flip: false,
// 'export_dpi' optional value to resample panel on export
// model includes 'scalebar' object, e.g:
// scalebar: {length: 10, position: 'bottomleft', color: 'FFFFFF',
Expand Down Expand Up @@ -909,15 +910,17 @@
if (rotation == undefined) {
rotation = this.get('rotation') || 0;
}
var vertical_flip = this.get('vertical_flip') ? -1 : 1;
var horizontal_flip = this.get('horizontal_flip') ? -1 : 1;

var css = {'left':img_x,
'top':img_y,
'width':img_w,
'height':img_h,
'-webkit-transform-origin': transform_x + '% ' + transform_y + '%',
'transform-origin': transform_x + '% ' + transform_y + '%',
'-webkit-transform': 'rotate(' + rotation + 'deg)',
'transform': 'rotate(' + rotation + 'deg)'
'-webkit-transform': 'scaleX(' + horizontal_flip + ') ' + 'scaleY(' + vertical_flip + ') ' + 'rotate(' + rotation + 'deg)',
'transform': 'scaleX(' + horizontal_flip + ') ' + 'scaleY(' + vertical_flip + ') ' + 'rotate(' + rotation + 'deg)'
};
return css;
},
Expand Down
4 changes: 2 additions & 2 deletions src/js/views/panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
// we render on Changes in the model OR selected shape etc.
this.model.on('destroy', this.remove, this);
this.listenTo(this.model,
'change:x change:y change:width change:height change:zoom change:dx change:dy change:rotation',
'change:x change:y change:width change:height change:zoom change:dx change:dy change:rotation change:vertical_flip change:horizontal_flip',
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:pixel_range',
'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 change:vertical_flip change:horizontal_flip',
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
75 changes: 72 additions & 3 deletions src/js/views/right_panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@

this.models.forEach(function(m){
self.listenTo(m,
'change:width change:height change:rotation change:z_projection change:z_start change:z_end change:min_export_dpi',
'change:width change:height change:rotation change:z_projection change:z_start change:z_end change:min_export_dpi change:vertical_flip change:horizontal_flip',
self.render);
self.listenTo(m,
'change:channels change:theZ change:theT',
Expand Down Expand Up @@ -973,6 +973,18 @@
// TODO: Update each panel separately.
update_img_css: function(zoom, dx, dy, save) {

const vertical_flip = this.models.some(m => m.get('vertical_flip'));
const horizontal_flip = this.models.some(m => m.get('horizontal_flip'));
// Check if vertical rotation is enabled, then invert dy
if (vertical_flip) {
dy = -dy;
}

// Check if horizontal rotation is enabled, then invert dx
if (horizontal_flip){
dx = -dx;
}

var scaled_dx = dx / (zoom/100);
var scaled_dy = dy / (zoom/100);

Expand Down Expand Up @@ -1179,11 +1191,13 @@
resetZoomShape: function(event) {
event.preventDefault();
this.models.forEach(function(m){
m.set('vertical_flip', false);
m.set('horizontal_flip', false);
m.cropToRoi({
'x': 0,
'y': 0,
'width': m.get('orig_width'),
'height': m.get('orig_height')
'height': m.get('orig_height'),
});
});
},
Expand Down Expand Up @@ -1254,19 +1268,29 @@
var self = this;
this.models.forEach(function(m){
self.listenTo(m, 'change:channels change:z_projection', self.render);
self.listenTo(m, 'change:vertical_flip change:horizontal_flip', self.loadButtonState);
});
},

events: {
"click .show-rotation": "show_rotation",
"click .z-projection": "z_projection",
"click .flipping_vertical": "flipping_vertical",
"click .flipping_horizontal": "flipping_horizontal",
"input .rotation-slider": "rotation_input",
"change .rotation-slider": "rotation_change",
},

rotation_input: function(event) {
let val = parseInt(event.target.value);
$(".vp_img").css({'transform':'rotate(' + val + 'deg)'});
this.models.forEach(function(m) {
const verticalFlip = m.get('vertical_flip') ? -1 : 1;
const horizontalFlip = m.get('horizontal_flip') ? -1 : 1;
// Update the CSS transform property for each image
$(".vp_img").css({
'transform': 'scaleX(' + horizontalFlip + ') scaleY(' + verticalFlip + ') rotate(' + val + 'deg)'
});
});
$(".rotation_value").text(val);
},

Expand All @@ -1278,6 +1302,50 @@
});
},

flipping_vertical: function(event) {
const $button = $(event.currentTarget);
$button.toggleClass('active');

const isVerticalFlipped = $button.hasClass('active');

this.models.forEach(function(m) {
m.save('vertical_flip', isVerticalFlipped);
});
},

flipping_horizontal: function(event) {
const $button = $(event.currentTarget);
$button.toggleClass('active');

const ishorizontalFlipped = $button.hasClass('active');

this.models.forEach(function(m) {
m.save('horizontal_flip', ishorizontalFlipped);
});
},

loadButtonState: function() {
const $verticalButton = this.$(".flipping_vertical");
const $horizontalButton = this.$(".flipping_horizontal");

// Ensure the buttons reflect the model state
this.models.forEach(function(m) {
// Set vertical button state
if (m.get('vertical_flip')) {
$verticalButton.addClass('active');
} else {
$verticalButton.removeClass('active');
}

// Set horizontal button state
if (m.get('horizontal_flip')) {
$horizontalButton.addClass('active');
} else {
$horizontalButton.removeClass('active');
}
});
},

z_projection:function(e) {
// 'flat' means that some panels have z_projection on, some off
var flat = $(e.currentTarget).hasClass('ch-btn-flat');
Expand Down Expand Up @@ -1346,6 +1414,7 @@
'z_projection': z_projection});
this.$el.html(html);
}
this.loadButtonState();
return this;
}
});
Expand Down
10 changes: 6 additions & 4 deletions src/js/views/roi_modal_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ export const RoiModalView = Backbone.View.extend({
// Clone the 'first' selected panel as our reference for everything
self.m = self.model.getSelected().head().clone();

// We don't support Shape editing when rotated!
self.rotated = self.m.get('rotation') !== 0;
// We don't support Shape editing when rotated or flipped!
self.rotated = self.m.get('rotation') !== 0 || self.m.get('vertical_flip') || self.m.get('horizontal_flip');
self.m.set('rotation', 0);
self.m.set('vertical_flip', false);
self.m.set('horizontal_flip', false);

self.shapeManager.setState("SELECT");
self.shapeManager.deleteAllShapes();
Expand Down Expand Up @@ -455,8 +457,8 @@ export const RoiModalView = Backbone.View.extend({
tip;
if (this.rotated) {
tip = "<span class='badge text-bg-primary'>Warning</span> " +
"This image panel is rotated in the figure, but this ROI editor can't work with rotated images. " +
"The image is displayed here <b>without</b> rotation, but the ROIs you add will be applied " +
"This image panel is rotated or flipped in the figure, but this ROI editor can't work with rotated/flipped images. " +
"The image is displayed here <b>without</b> rotation/flipping, but the ROIs you add will be applied " +
"correctly to the image panel in the figure.";
} else {
tip = "<span class='badge text-bg-primary'>Tip</span> " + tips[parseInt(Math.random() * tips.length, 10)];
Expand Down
11 changes: 8 additions & 3 deletions src/templates/image_display_options.template.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<div class="image-display-options rotation-controls">
<button type="button" class="btn btn-outline-secondary btn-sm show-rotation" title="Rotate image">
<span class="glyphicon glyphicon-repeat"></span>
Expand All @@ -8,8 +7,6 @@

<input type="range" class="rotation-slider" max="360" value="<%= rotation %>"></input>
</div>


<div class="btn-group image-display-options"
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)<% } %>">
Expand All @@ -21,3 +18,11 @@
<img src="<%= projectionIconUrl %>" />
</button>
</div>
<div class="flipping">
<button type="button" class="btn btn-group btn-outline-secondary btn-sm flipping_vertical" title="Flipping Vertical">
<i class="bi bi-arrow-down-up" style="font-size: 14px"></i>
</button>
<button type="button" class="btn btn-group btn-outline-secondary btn-sm flipping_horizontal" title="Flipping Horizontal">
<i class="bi bi-arrow-left-right" style='font-size:14px'></i>
</button>
</div>

0 comments on commit 043c645

Please sign in to comment.