Skip to content

Commit

Permalink
feat: add vips straighten
Browse files Browse the repository at this point in the history
Differences to pixbuf:
- plasma-straighten-positive-5 - MAE (Mean Average Error): 0.0117613
- plasma-straighten-on-vertical-image - MAE: 0.0159686
- plasma-straighten-negative-20 - MAE: 0.01176
Visually all the images appear to be shifted by 1px. Since I didn't spot any issues
with maths, I'm inclined to assume that it's due to rounding/trimming during computation.
  • Loading branch information
knarewski committed Nov 29, 2024
1 parent 865de1a commit da1b983
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Vips colour filters
- Vips crop
- Vips rotate
- Vips straighten

### Removed
- [BREAKING] dropped support for a broken 'dominant' border colour
Expand Down
60 changes: 60 additions & 0 deletions lib/morandi/operation/vips_straighten.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Morandi
module Operation
# Straighten operation
# Does a small (ie. not 90,180,270 deg) rotation and zooms to avoid cropping
# @!visibility private
class VipsStraighten < ImageOperation
# Colour for filling background post-rotation. It can bleed into the edge pixels during resize.
# Setting it to gray minimises the average impact
ROTATION_BACKGROUND_FILL_COLOUR = 127
ROTATION_BACKGROUND_FILL_ALPHA = 255

def self.rotation_background_fill_colour(channels_count:, alpha:)
return [ROTATION_BACKGROUND_FILL_COLOUR] * channels_count unless alpha # Eg [127, 127, 127] for RGB

# Eg [127, 127, 127, 255] for RGBA
([ROTATION_BACKGROUND_FILL_COLOUR] * (channels_count - 1)) + [ROTATION_BACKGROUND_FILL_ALPHA]
end

attr_accessor :angle

def call(img)
return img if angle.zero?

original_width = img.width
original_height = img.height

# It is possible to first rotate, then fetch width/height of resulting image to calculate scale,
# but that would make us lose precision which degrades cropping accuracy
rotation_value_rad = angle * (Math::PI / 180)
post_rotation_bounding_box_width = (img.height.to_f * Math.sin(rotation_value_rad).abs) +
(img.width.to_f * Math.cos(rotation_value_rad).abs)
post_rotation_bounding_box_height = (img.width.to_f * Math.sin(rotation_value_rad).abs) +
(img.height.to_f * Math.cos(rotation_value_rad).abs)

# Calculate scaling required to fit the original width/height within rotated image without including background
scale = [post_rotation_bounding_box_width / original_width,
post_rotation_bounding_box_height / original_height].max

background_fill_colour = self.class.rotation_background_fill_colour(channels_count: img.bands,
alpha: img.has_alpha?)
img = img.similarity(angle: angle, scale: scale, background: background_fill_colour)

# Better precision than img.width/img.height due to fractions preservation
post_scale_bounding_box_width = post_rotation_bounding_box_width * scale
post_scale_bounding_box_height = post_rotation_bounding_box_height * scale

width_diff = post_scale_bounding_box_width - original_width
height_diff = post_scale_bounding_box_height - original_height

# Round to nearest integer to reduce risk of ROTATION_BACKGROUND_FILL_COLOUR being visible in the corner
crop_x = (width_diff / 2).round
crop_y = (height_diff / 2).round

img.crop(crop_x, crop_y, original_width, original_height)
end
end
end
end
5 changes: 5 additions & 0 deletions lib/morandi/vips_image_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'vips'

require 'morandi/srgb_conversion'
require 'morandi/operation/vips_straighten'

module Morandi
# An alternative to ImageProcessor which is based on libvips for concurrent and less memory-intensive processing
Expand Down Expand Up @@ -97,6 +98,10 @@ def apply_rotate!
else raise('"angle" option only accepts multiples of 90')
end

unless @options['straighten'].to_f.zero?
@img = Morandi::Operation::VipsStraighten.new_from_hash(angle: @options['straighten'].to_f).call(@img)
end

@image_width = @img.width
@image_height = @img.height
end
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions spec/morandi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
end
end

describe 'when given a straighten option', vips_wip: processor_name == 'vips' do
describe 'when given a straighten option' do
let(:options) { { 'straighten' => 5 } }

it 'straightens images' do
Expand All @@ -248,7 +248,7 @@
expect(File).to exist(file_out)
expect(processed_image_type).to eq('jpeg')

expect(file_out).to match_reference_image('plasma-straighten-positive-5')
expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-straighten-positive-5')
end

context 'with a negative straighten value' do
Expand All @@ -260,7 +260,7 @@
expect(File).to exist(file_out)
expect(processed_image_type).to eq('jpeg')

expect(file_out).to match_reference_image('plasma-straighten-negative-20')
expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-straighten-negative-20')
end
end

Expand All @@ -274,7 +274,7 @@
expect(File).to exist(file_out)
expect(processed_image_type).to eq('jpeg')

expect(file_out).to match_reference_image('plasma-straighten-on-vertical-image')
expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-straighten-on-vertical-image')
end
end
end
Expand Down

0 comments on commit da1b983

Please sign in to comment.